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 01/87] .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 02/87] 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 03/87] 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 04/87] 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 05/87] .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 06/87] .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 07/87] .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 08/87] .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 09/87] .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 10/87] .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 11/87] .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 12/87] .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 13/87] .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 14/87] .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 15/87] .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 16/87] .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 zcmeEtgO?;r&~KL8oSq(Ich;EYU1Qd^ZQHhObJpfv+qP#hX1lwrmwVs){)l(Z*Lk{J zQIV06k&(ZM?9{SZvu49E&^@JF(_v$$Bv=3d2rLWg!-Kg1puhs!wCvm^3ZAxX*Q|ZZ z&ds`*;BlLQ(}q>AS+T}H6)RV&1cnTpG7vy2|NHx23H+}F{#OG3D}n!CC4fPTkH9|) zFo47V-}ApCf~qR8{NGyry{p2X1h4$vN~#RY|LkRW^?#24-uZj>KljD|jzoAxghhsb z9u^iB?!P|+w|}qvy~e}4!hhxOy}wWVE&umT>Hj_R_X@2tuyFrT{7dsMpAzKsFVBC! z`*)850+9a|;=fw`tpWJ2hJVL8;=g+Stt}4A-zWY)s{s_y0~0U+GaLhLu!O$fIiWID!0%i2KE&j^&?~wrI=2v~HM|G5s4vuRN`*2t1xlovsccd9E8i4J>8oy6m#MGRZ)z?qD*SE|=nLPz z3s!+^;1#$CmV=?-ESLmlfeBzOXbLh^El5{ysTt}W^{~24ovBWSUl^gbgh$6z47ef3 z6nLQ)s&?2C%|K_+8;k-yK?5jzC%jS@GzaZKIZzU?YAlqO3~SD)Hh5kVN<9|js_~!{ ze7ZiU4u*hnU=HM`8b|@Hz%aVLSir9Xe16wq-Qr zvngl;=|w||7SIHAg0g4?za0m)Ar@Y*2=5Pq?+t}tstF1pCx5SZ2jwB<^03WhD0u)m zR8$MB(P#Ch`T*AV4Xjl<)a)YIMj_Zf`Ec)13)DRIx0(f?x~pDPFQ`XhISZdnSJTv& z>Sgtyx&un)iuy>s3h$nQGFz{1RgXZLZy^mol=)Xkq3}PsIiQ3RV9gNVg;(FfuTcMK zwiXns_teGe6m_Ax64H3BeuVt&g~z+#*?p)je|6`N`cyrjE>#DrQ{mpNmVk8}4_mVj z=m@&NvILw0@4##D9P9vtVa-#Z6uLk;)deL$5K_FTZi1~iMO_2=J+JPBR3@p@)pJlk zli(8(kdsDW7x(}&Kpxb-&#<#sM;aq>NE$c-HiI=_5tt0ARe=3S2SSjmEs*1DPm*t?kO*oPs$tRm@-6h zDuuFJb}Buf-o97#YI(J(IsnqX0X4P-q#Z zOFN}C(sZf01f+N3Me&2!LV6*!lU?#^B?-1uJ#ZSxpb_#7*@ceA2)rRa5Z{Ua!n+ce zh+o7zVmHx+h#?l?Pp~-b5_$mLh7LgUkgCWMs5$N7*MER9ND`Wh&c|M1Y1m!tIo29y z@C4#F-Ui=}eLyL+I#L60$|d=NbW>a?==t;PI%XHmgcgLT(9&Rw;I?3$P(f$`-IHm{ zw&LdT&xJJ6Emc!?tAD_Cq$T zeGwX&jjHH8tR`Na@DuaMY2<415cz<7P3|Nk$SH&upNI1pg*`_OgG{xH%0O#UUim2( z$z7Fa(D;*TF?F|E1>6A9h!yz*PC#vF1J0|*U@P2`PK%3$I{ZfV3tcZ16>J`8=&#}% z;Z5_TdOmoXdDFdJd}00(fx=*8dL(n6t$Hxf;V z_5_X3gl*)*5#k1{wF!6PCGmbx5}j}tb_kn>^}^nwOJG~2qXE=}X|P-9LUbAGLEB;L zunE{wY%Vqn`-lz1UHB40Pu?aMQ3X^<&1}tR&0bAN6Q!-JeXSX(k*MX=AM!2n0zZu9 zqWjU==r^Q3vKHEzENHW?!yc-t&A}&79TA{SD^gFY^I_lfDixKX@)~Ka*i?AIm0%yz z#pzL@^}(ZoI{ql%F>hCI74LBGRPQhEUEc%$`@s9)>d<+*3VWUl@cqPzQXeSMr|J_h z192lgQ3job6<|a0fARPD2b>^`#7lfPeg*%Ihv1-j4_}Ut!)xHx@!@crjBm!%@FKiC zQH`iWbR^b6?bVQ#$>HRF@&I|CJV$OP-6TrYq8?E@sdbc>`a?aWPEtpxD5^dcOZ`Kc zs5PXINFsLOuQ4{Zoz<}UCp%glu zIm#a7K5||8aKR=Hljg{C6pN|{$>3j*1Ii=!kQr!atR!sljrb~jEMAuwLHr~Jk@;jL z>NqutT1joC!YG#Nqlwp+*Y?of)lAdu)S%inn%!DeGe)~Y6Q(Vry{;*#Ijwm`VcMCR z2+a@8G^(+t7}Z4+scA!IQQ@RQZXk1rUc_hOJ30%Ourug0^aT=wltHr94r*KVvs_k5 zlvCt?rLt0r*i=jqD~mIQc)`aPadml#TgeUMmT-V;!wuldv$t7{9mH;68?ejRN^D7{ z4|ACQ$^__TjDdO2IGN{6XQm(1nC(obad!4Qbf_M3A#RwEDy$a1h_$3YQg?Z47)7Lw)3Upkz; zO^wv;pnj6`b(N?>vajwZ8BLX=+GrDr^~4pe9e+xmAheqH*mI&5br0)D7LgawPJ|Dy zj&8@ElWz4Hegc1`cE>g%_d!`u0hy>yK<>ygyC8x`ixb<>H-nQCtefk z3QfhAl7@Sr`q%|h9<i; zi0jmQn8e0t_KDBH6Lf;y9Y=sg$;2um?Lb|n37G-M*h%<&upHZs=s_M9k5^I&A_nV& zTJY=Ya(q43ANhjrA=fLtwR_a!ptkO_)J5A%DI^e5Clk7gsu3GS98qf^5&9o|AI)p5 zrI@YhF1N;X_y@kSexX=^b;8bx8>vNdbFiLR#oILBMH#6;Uf`=~N=lW$d#t}&Q@w;- zQkyD$&=@HTOOXUwL1nH6`BBJ~S75l%S+0jHkQ*p-#o|Q1@KM@`>4h3fO|*jWMGYYa zUWVhqeQ|}72F41Nu{Qi{e2Dl;8K*9hh|>o2>pqFz(R33MTnp9lOjg_WUo*qq-jW5q>pkEs~~2{5UZWXt|DhDjg=?(UIE|+c{X}S zSqSL4*YFD{DUO}C!EJ2T`xZDxHD`zV;@SVJuI>tZ3VwD5nBuG+D zp=Fdva7`_Vyp>gSlC+P&xkbbnbSJlkS|O#VO_4!L4bTYPAym{;uqJXAIyheRzNDRb(1kPiH! ziHs3810$KjAlhzxf1<2%ATp@)aXJCt_$^$_Ho-EWQljSk$5#*=3NO_NZ zK@^EV_oyGiMmYi<3BF35$z0J4MiPZgp|%!xgSyPkr@jbh;iyqtt$|*Wz7j|Ij>Hnh zrF=%V0$HAcgpghQP|X5yI(UR;34N$V%qGoJsSH>L_MrLF3S>XnF4sYDJfGdHtH?V5 ziUXlL86l5G$0)tjWB5{jIvFMo!^f#vQh)rDI!8`G&dOVGuT(=_gPoR7p}2BeeU5z= zJ|Z81R*k^#2sGXYj%v@*i&8jx0BI;W$>V$%@-|t|N!Dl_*jUS90+4QfKtG+!K8zcL5`i06(ME9=Er@}`ubdO)sx9K9qI$G<8-@!|8B zk@}i!Z*&S$T1v)F$OKkLe2aaSnyL+uN74$kwJM0s$uzz>x<VQ5_UV@g&G_^9aMZOAn&_cPTj)k*HlQJ8$Mf%9Ukq*iW&{pjU&Wb5y zxNr(|!XkxX_+t4x_!o{sF-m)MvfK&XD^}Wby7yDUBMp3iHwlGk*C=< z_%kp*+#Gli8(;vL1E*dlojdRr+3J(LUR5&4sHff&dyCAabSh*-(5c=46oAF89! z0NtYOLwAVF(J!E!^aqY@W0ei)aJdfJRbGiLRuWYjIi;>q3XmAkSl&bq;3wgW!DDeS zd4|76ycHKAzmX)dG(J_Di9S=;GT4j@W9byHF`l_hCWs@z+$x`D5a!8&tSLQTPp=3^{a%Q2(RQL-_@FOX(CBH z0P?8fp(a{UnyNGb?eGll0C`%n!Vze)kVw7e9ueckSMVF z(iNR5cEIb%pFozZ!`lk)@F;bWSei5n9wZSgSLR`*`E|P4A+2_kcpO^RtsqWmg{bmu zuu%!2GsL8Umat!+{o3mzH{l)GgFU3l5Pqqv@Oi=` zytXn;sYT3_4nTh+T`7+)mp&rhkjC;IG)JC_j)Jx~5jRV;dIRaCG*eIF+59k~qZ$wm zVqvV~jEd%mYTni&mH0)T^K_2mr5f+xdXO9Zlaz;GnL=s0&=7{TWy9@{3^n!J`%rDIouG$ zgEkOPW2}rLXOuc^qd?6eG{9q4c#UkL)~IcbSUVcUQ?bDqgc1b%`~LK`GIH?^f%X3 zJDi(?@01#$S|uJ#M4wB?pm(+k_Rlro7@En=)n+oYh>z-aPh6H_)f3piq6oGqCR7s65GBP;$PWaS z_mJ!O0h&v~GgJWerB);^{y@7cUeFk1N-0=1c`0B)D|rTbN2-SQlA9yf(;_=ecV5Tjj9Y7+C?|`GTYL6x;Cf9M-uu^Jm@Q>UA zpC+GGSTsr=jD^cGN`~1Kwdv#9bJn>_NJL^-@_lew#ooa9H($%dkh9fKGxQ5*tZ4!6Uf@K24m6 z&z1XuEVQK56Qe;F$w6$DSA&X33iS1!DIewc*kt(vxFf|9+r*im1Yzf2VgDkl#TqCL zAX+1@!@ElX=;5Y<3?&-;26L6ys33GCYw&N-TG&x`igqi%0+_Jo$_Ql_dR?6)0kVzQ z1g(u^NtuL8yo5Q_QsA*-LGQ^H~e!7Q_-qq6eUTFe8sZKjk7?8zjl|F~77H_QJbrta=wB zUAx7VSb}T?l_8bENGXUUHibCkYG@@i5W9+3&w*d+DezjU4JN4b9D*}UMGK%9T4sPsdiG2tIbrO(hZ`^(V!MY=5_6>f^8$d<4W_@*$Rw6zYMhv zJn+x-mG!ms-gFo99Q40phsZ6kMw&1B+NKxQ&S5Ucb7z-`K-BlxO$m;KtFe2cx;oBU zqfFIxt*J2(890S+#?~U2m7wSs)(f(5SX?D7l52pT=w#?Gy;DWSCa;iMN`)@xSz+4`89zbYW;lV7*s$jVXMe=gMiAGc~7E?!vsX`NMM8 zW-rgR`l_lw^_^^^BgP~gF7dWR{bDy_qoYy|i7mtm_B8I>@r ztTg256%gLkVWN{H26^!+?D!T3}=i3oXVR>PboUH7TPV@Vj z+M&V0k)el79X=%Vks3k7{DLroJHqgEDl>w6C_I*`%RQvGLKW_5Xo`P_2X$X?Ep{z( zHFp&+x>|53|3mJ8tR;UAX3Y)RbxoZYqqE~bl*lP@JSNgIg<6U)C7x+}nr~WLSZV75 z`ygwP_8)W(U&6N$tw5VduaO}P1#PR7cp<)yX!J$5fUfxjnpQfH~< z<$e^OUZz%@p<|-JiBH)nKo9BX)%CSXmk;B=GyVcU(FC z9KT(7C?-oc1TB|CFAfz3{Jtb_Z_h9HY1f*fafScpZO<8>vn;#)AMmSY;RGT2B+nSRpozdSm)*3s^livtz5Rzy2K-L_Shg%{T1dBOb)(6t@;{7I!T? z+4Pi*gE5P0#CYYuj->(FW83!^_o=f+y&_e2$#Ul5ZaYOFO7 z_9x-}!Zuqiwkp=LdIn3vl63>@4AgeP6?P!$sW< z@&!W4{rF6_AD=GmQ+vaxZoaxxzQFAa=K9+E5dY`keYUX>CGHnDirIXSAwwO5&{{AZ z*f{=|&{eD}776e8yWCE$h{W&t=~!ZMJh83~kj znY9r$qN8Gx;)^E^j~(kcWMrr(#9JyxZ#O0xUl`6CM(Nh#pX4&aMV=Fq<*MphFaY7e zY^A4=La+Ci@Xrq%2&T~WxqpQc;!eIFn*_(@O7!56j$X+8VB`5nVYYBq_%2Aod*KY9*Gj39NSxbx| zMw_WQs*f~vHy5|eGY>PQP@ljmaUNe;7$*9p%Af;`d>Vj8>de}MPJaXc{QyP}XZ?IB z>A1L_>p|xQiO{cLrC_7bVwMu_3t7TxffN?<=eP&#A+|T`XD}|ElbD=fufQsQA8#$! z^@3;l&GKKuzWzQZFYCph>E921>y&pyk&Fe_uVL$=`^C+P*l+r#8KCQ8{%NP2A0mc2 z2ZRl@zqMY|9mAdx?@Y78D@PTNam4M2YZR>w;|vqEJ+-Iwt;|=fd#%ST2TcxLEWS~> zEL0bMOIq+7`HWInMHgVg3b@NCZ=(Q6yrB@mhDp5S^H%3K-~c{87rpdiRZ-2$`G^^j7p71hp2Wbox4oG z4!#Mtpc!_m&{xQ2&(ohmTyQ}k-CrpC0%@WOfI8m`mlId_#r` zj`T0}rF&|+1FmK+tnf*$IlEnEzx0*ADyMDwKIhw*-{1VlG`y{$-QyS*H8HAA*b~DO z{Q%qe$jsO~@rPqL2W54e5)A{WAhFW0J-l^{5aW#b8D))VW*=;>W;$rvU^yAqJ7T-D zxb35UAXOV1sPq&2NlgHce?)9n+t4aVyx*w1a*dS{Nz{wPzNxL)qC(Z@NeDpl^$3rn|B0TA`s}aBkh~*O}27 zXMZ_jK{a$QsiIYiR;wwcO9EEUWd~wWo~D!zxA`a9SJ>Z6mCbWxHjZ z`IYIGWk*;QC*x49?M$fQkJd$g#7NYOEJjD6M?pP#D-YO>(BF#-mJXZ@#DhF!mU`i?za7?h3khs7vrrU}zvRu*~1icfd2xRk|=I z?^@2Cta6!MGmpg`h-=orrG*O&WPWdm=nJ|=38V$ z@H2J-`B17U%d0aH*6J_(JYqnUi> z5bI^{u>;vWW-RlK9!Q7NMmm9h9qJw2>bu~6RHPKnE<9AQKmTN2&Ac&rQr^$}bA`*@ zhkbP5NeHmrg;Gip(w=Cb;dNHiZYvu$*O?GGH|kFGm6)-y*0_Cf-{YRe-HVgrBI2jU z8DslJ<5BL2Ea!{xFOH@TcbFrrf$gZ-YnZ3&3?t1`@y}>yWWTyqE-5bLFy>A0Ed*El zcz(Jz6|F41QXu5F%>SB~m)9#_&i`8QqVRFiOIM~_@;H4uKjZ%%*c^1i-f$~)8Maob z(5|3AkPt}muk)FFlf9Hz@ci)-f&5T+*2 zzb}vidqX~>=ltAa{wlwdufgx(zOw{tVsb+pg7&}}-xzNrPgi%EtByP9$@Hht2q*Hl zMG;~m=deR$s`ixOhWWa!v!j>uNkmrUlc?^|tD+}H8>1ISeTXcGc;{Rf-pw&4Y_olZ zt*JG^(!qSs)Yf#*NE?3W7w9PMd@6&8#%H75kV&AgD#|;h=Hg>MhkZgH310Na`V8J4 zo+s{RZlCM9>xB!r7r7gFV!RQ)GX5%oF`b*0oB0L>mL{-s!HS93Yv?V*5IuAu0i`*17 zF#1OHfasl3Lgci_9}%qcUidUe@2~;(_BOrsmieKnuW7cat7(Xl*3Z(#Xirlv;w_c| z5zkLB@5!btlXT(*n5g-Y9vMvXPxVgmNbU#j@$O954A)54HrEGNM|V@ta_>u@*Utr* zpcE>kA2Rb;J2w-0c^tQaJH*a_9(tt^6S(Ey?Ca>Q3T-Lw-t1oFJrb}og`8F7rFiu) zas)q1wborXj5K$&eGfYo{@aO0f~Z|lN|Zf1H7Xd{G;(jmL+6I@7LMX!#q9~Ucx$w! zjrp5tqlq`ZF*MS@*Y4GHqizuCFqw4{n5JG)uF3tSA_2z8xp*cmc*Re9pL)uBKDdXu zS=R%X&(+Gk+pUEeL0`PQ&k@iDJ&^lkrZbz&9f1C}o6FoYCx1G zYFA{b$SDyC&SQ=pVQ=gjyUF&=^3q(z{M$6jG{X2pKTg+I+gOuLRv@}!kC3A<>Uvoj zCEKMu;T#{%*%?0Q4D9nQ@m}y)J*nXPL(c*$V6WZdz2V{AC*21<1N~P*?cj{Ot~f*vs2foc??!Fc zRxorkSF%+L+vRu~{=<1Sf{gS;T!QQ`}_Jvc{h3Vo^04#cYB=P zBi{bLX8uZnqQKT*$51IcfvL)-aFuy4KS5X~ScN3mXBIFv`fzY~U?Ak)>HF&4Dim5692&bIytpts?TAot$~$1H%V6 zWcx{*-}=gW+d9em!g9rY#AGl{H}*6X>Qc35G+(KA%m_qz+-OFh-SUhh@kQh%>NouDD~CbXAc$}DAPbN%>e=u?jn zP@yqjjJwOE&>w^A0^R);eGR<#J(#z&Z@1qN+!lJmEae+Xhm`Mt!s-&Ys9w6GhHa)n zmU6bI_K#ts97Onw@bS+3&P}kl9&_vs+h<>In_)d->1&y9IcAvyb?2HX)p*HpLEl}M zrnyZKR0?S)X2EQ^?jTVO$=f7a=)kk=b7mTihIYYxsJFhRzB68%cfV)5hw?u2p7HJT z9}fhBt?3R7%}i$_xS!kwe!Ku@5-J1!kop?+10I04$lC@H}P=Q;-0D5V#RKV_^ z;H%?n=&R>T@ZmlK+|TxX^{w=83A_k~(=C~a>`v|)e?^!k7Kz2B>XKfXBQ6s<@)qte z;||3KQ~X=KQ$1hZM`6qT@`ZxGm|}dIuw7cCtN=66jd(GtrEZ?F$lTr9#n#T=IV{mJ z%Q4r{!(noa3^UtZmer=4hLAp_Pclp}2!@Bo(Wb?wFUISJhx+3B!#YkIs~xDxqvlYL z$g{*?oP?<5Q*|@MCU$Urm|mf&f#<#x-kRRF-u+(M+tv5UH_cxr@Fp+@W?`n#Q5jh1)IOXQOBJZYBb6lQbRn0ukkfhInk$Ksk&c&s3y@E=!OZ!oZuN#&bL z?UX`wD3X8;B%q1W|1q|(P_{bu*s!dysg7fgQI51QH0+G+j-{_jZ)mO?r=6~i(M9Vk z8KR75jjxSMjfiowfij%dSJ6+=E!VEn?4_=g_ld)JYiujBTCF4hD^%is(w~Es0&XAh zP4d?FF7%f7;r>tl)qz&Qgpd--r+rLC?kHbgTrQ=`tCYHGD$GW%0&`)T%WjyDIFeh& z>)va&Q+vpLL@)dpdJyzhUW*D>jM)@C?w{j(19KpIdv|$j`eOYh10{l`LT%|4%w_f> zx1YZY5xqY0WTgenN^b`9lwZPpUZ9+pfJpK#=69$dkn4NwxeO6KU%n~ta!!8Eko?=O zLcfkhq^+O=9!p*)i&KL%eRW+7_l@UGhs^(4###4UQ>_Ko0k(d&2G$t!dP5mqQw>gS zB`Z=-G+Dauh7?l?^F_1WQrY4+4>9jDZ8LT>{LppNqMC2yzrvV|Xv3A(gM)U|4E8Zf5{Wey;9~rUQ8#H7c$6#`KavOaEg3!$9}YH2N96j4n^> z>3BF>`9<$ywy=-5ibA@0Q|_#mgsUD(s_*4V(onHLFo}o6w$e4}lXPBcC2bPN3wfNC zMd+-6%h%BR$vxZktk9IdJ-aAla^}^%^8NwRUeac|YaMCTo3gYWsJ_$y?IQhJLvurf zeiih1eo+fF6JZYRa4HeEBZcZ5WH&KEf7$vYe0}7^$QsV>VJ6!#%PZI(wM}E-3?WsY zsT-}!){fQGBf5Z8VGbSeuktnUuMCc2&T$j@i(CWNPgkUM^a$F)_?ZkC|5+-`lvXP? zFavCXF}t1OGd{%i=bs3ZrM>buxs$w5dL~vAPYFiB$j^c^yaB;y{?on{-m~r;h*o$q zCj4IhXHs4?T@7zzDFuD%#*Tc`L(MWmB!+2*>2B)2XrF0nX?kd6Sd&Sbazu6T6v}0* zd-jU%S$VTG8e0E`>^7zj6UJ0vHbBk#$l+oi`HhmQ7DASoQg@%l8B1$J<#kVF##a?obuoqZH znjafy=(}nsYO*yM+GV=WFcbJWk}95Ij)x?A7M~3BO(0>Xv3{$YqMvWJQyk3 zNPlEVp28gQy5n+lH=u4(hgXgE7Ec~Q(h?( z63)L0<#??{Qtr~M`suZQRZYuF+nw3Qe-l69_#D?I{z0VQI$w8^sE20~0`&pT1sdYt z@X_RPawgswj1p5>8*7%1lk@CM?26*;l6EDEF*(kG_HSmbv6*fqohUC1KOu# zRWwm<#Fyk62+b86YR5JB8Z-%1kShzX+3HLynh#B(uQAcwH|{>yhbzxD=H_!hxbFNd z{yM*$&*7_yzoaPTj-pj-sC86KJ*>1;^5mxSVHk~gA%&y^aBWCeA;|6xt@gEWwamYm z&8543rhY&7y=TUbfLVJX8cq6G>}PbUEnN2mpNbX7tKd7)ctnS4i6PWw@;ka+&gM=s zwfQ)7y?Jm(-9f4?W>%|1O>d^$L*J6~ zH0!8Dq9S@(St?kVCV>Lzn_Tr@qx1Nx(pzyhe}=gq+7PPBNL-HSkxwg=VdQwWlrN0q z-*AI@i#T7d3yz{iaOJ~TE}HR&NHW=r`hx=74rQwFv724#2F%I9w(RH08A0s9`v?R$_6+ z?$39%DO%)y9azhamCne2BuVga3)v_(mHo`E5oSpiHUod8_7FS6 zSlMdE%U%~ID4&obnC~@`YvQ?|wK>iCjsG6|XF%4%oFhe6xubJ<`EQk!q`7tjv0R!A zd%z}Kp{9~o&`01Ias?d%hKcvs@%&UIWc(+(bcy4oUnZqT&#`&*wKY?SLUbbX5P6Ng zA)VS!nlN%VS{}I6zUXtZg&r}b8^7vq6C2cgZb4v*dspG$!t<`XzN7R4!348KO>#@& z0aqVt*i=~qz9S1^en_G)hQ8ppLwqDJlr3yWUXvHJQz6cBh)6_wNw2u#+*iJVl%^Pw z4`85thpp!?R#YqZd}jCbZRx`@yJdb&Z<6~#C>^;pc~a$z#edr(sHWjG8Z4Bn`N?@&4%}60VpA^qSyMCL3Clg*Be;RY~ovppJ4oGtB}es;};5brP|6h zxXy%9&q&$a5*Sy#=wDBt7ym(1sEOKdnk(cmd;=k(U3n7X36Nkg%62+3ElZHgswWjKRQccOecrx}c){(eDmZ44)tC1YB4d>w(fnS=7 z=99Lo7QgNtR#%SZ^z_T%sNkkx-_Q~I6~v|HFkk4dOl^qTSd>aIL**J=<8@l;CAHyi zFni&OvKL%GsSenQ(a=A9M!lluQU&A}A_s#j!k|sBqXgxV(n(=9f1hnmzYkpSMS6$2 z&laxCjZA;}t&&S7V89UP6n7j9-??G8MKD$1v@Gkd|JpHYA3uU z4%%MDR8DLk_sTKAaGR`0j?moFcGu2^V^}$24Dw#?DVl|c!X0HGQOwZWvdXf~u#_mH z#PgTg7JPRZqDQ#Ygu3W4EpE>MkW#*dI{sl}1DNS+#|oRvJ{YY~&O#o6LZ7>PZt z)_Mvf3o*v9!{U}Yww?u!Gn(K zl^m5v#MUKuL$sin@=c!^Hq4re|6%L+a@`xerGm%UFu5Ln)-c|-#x~ScMLUk@Lv}W9 zb2uaJ+rDU4DL0s*FpBgfG?jlR4+oRrisH@cF{z1|B)3C8K=1qt`4G7y&Sy#oj|cXK zRnvIiMPTqBPV<*{;bTuGUt#>)es58j!M_Rh*%oY^V8OnS47>FHbkOe$Ajpcgr=`b>>FoW44;saz1bzi%e%0{M%ui01ZIzC+(E)Epu3V*of zY&~Wavz0q16(ZxwXpM)ACE)r(d^?Q5_tFj0KiBWk4}yMUKZucs=_(o4o6ehqCbMCp z<_PJac2V~T2YAGhp%Q_rUb^5%7Wz9Wt?AFsKj;5m<2`FSQL=X_)p?q#3f`cP^_uY2 zQG@MWu{EJVuK0r01(G`__?Zpy1ErnHRwYLs3SN_pX?gh5s1^|wP4|#P{6TgVe@)yc zuI8=>2YIb-hdbRXS;#?_s8NC-g8fPH8DLXPrzAvw~kFwSrlB zJJld)fTa_QG?f0H;jnRn>80raj5*m%rOi_zZq&xw)_l-tG>q0GFe$GrAut1}<%}uWI;ovaBX97H za2L6fU8~$p{C7h#Be5UopT42qL(DjwvdxMt6S2YE1#c;J7iX#nb{XBFJYhAy%>`w0 zTju5!y!MW#XK;hL7`A{}#GMh(sR+KDnyxuR?uOn*b7iw~9jrx1qNQN|Y!{eGhari? zUQMFDl`-7B$Gp*e){<=h5w4FiM!$?ioZ0p*)_lt{D`AVY8Er=_y#5}#gibGLns>`} z$iKniYm z@)ma`=SSsjDa`P97Fr?4h?>*{G7lHA62wc*azhPMmSK!$Ih^6TaDE_x^hwmUWO1!}~cLy@c ze}D7k@XxDxM}zI9OKPg36FYHN_|Bl3_LKR5wXU%X%XJ_rUTbEH$jidfX)Ix9O)CkD0q#uA2h-W!ghjoF+xT z$ZQScoX;W%=SWAn@UD?e3>AMkJ}16?qB&`Pi371^s8K~9e=o_q$h}k!`96Fq_x%3z z6R#`hVnQW#wRnjiA*3p;$my1@F*Qp*D^rxDiI_xHmgAHK#7XTR+{-q~3w*!vnfZGC zS9Z2gP{Q@CsCdDuyzhmnfxGfoO)JwZLvOMX7zNit9R?)!3aKo)f+6L*QeF&v-R0Lw zmlC)V?Bn0=ZSCL0jF9JOjycvR94nq5O`9&0W68<}%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 17/87] .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 18/87] .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 19/87] .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 20/87] .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 21/87] .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 43d7ecbda1082ffa9c9640db86d41d06cde1d29d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 3 Jul 2024 11:30:23 -0700 Subject: [PATCH 22/87] 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 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 23/87] .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 24/87] .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 25/87] .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 26/87] .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 27/87] .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 28/87] .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 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 29/87] .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 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 30/87] .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 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 31/87] .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 32/87] .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 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 33/87] .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 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 34/87] .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 35/87] .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 zcmb5UWmFtb@GiW#`vSpjVR0uo3GU9~?(XjHZh^%e7I&B65+Jw-C%8ibvCHrOzW1Da z?zdY#Q+>`8|Q}>fidmJpitPjJym04h{f-dnKubFlx{ z2^=aaDh4_R5hf-PJ0&S4`~Nfj8wTK^!V@A;Bf!xD;PK!P@ZkQ90?6O=M1uQI-TzGp z062I=BxDrSHz_F`0O3vee@Nei2yfDVn*dA%H~>5@0`A*;w>)Sst=-=BbMTt1p&&H8PdA#cWsx?+!DpAlX(+N7+SspRDI)tu@I%`9-SP zZQ6Pv1!`2+0;mb{^?Mv^H7gcv1Ws+gc9)Ab>&tmLwPRVA%y9Fm(M45IP%CyS(0*lN zIng*P0*ygexM?9?HG)%BmHb%*?`oF1Fwh;UzciiLul7y&T5B=kQL~G)lw-izGU!-c z4R;?J;p+B$FYK&9w#<-Avd~?gw~Kfg+8S}OJ(*Vuv50cun=V0 zTr=khC)HNFy*~MEw$p32F=1MyS<-298MkQ_b{)@ox*eXGQL1oUkA7fzIo>QEdoe|W z*urC-cwOq>V3Y^pDE0R%b?dYq$bghJ=PBlQ<}ocOF`JvGPDh0qxCpc~5KUk_dCluf zc4R`4l;JYwHaftBH&{xgImlUxs9FWw^5eQNJ0kk;p=It8Yf!!?uJ;xiON+5JXQnoW zPb=LjaYUq7ZdhcrD2LNJ~MT0IQPuWWqwJH)aX-vq%Aox=(d545OAt4rwg}NG!hKMRM&&&)A9XJToZ%M=isI7CO#{ zdc9LTG}|b%NUl`)PB8P=kKJl!^?~TNGJBhuY?=z*FkvJ5WUW^Hl{hP*%#L;LB44EBvqKrpuSQ|oqR09d(oJIzH_wMyKxxDmilsKTgB=QhOMNob4OjO!K7xD5rb z$36h$a81M2y@uulP_@nayc<4>UWcHZgNFD0X}xz^UcYrV)C+b7K?qH=CrmQz<(X_? zTesR}_)`0kbJ+nq0AknQ)U8b-YN`F4B z_)$RL{uwvLFmpW~1I{`rm8r1^1}}-h$5>k;bHUz6C0u6S%i@=alDJ)T;9 zfBaq65TUI;A~5?{|4yRDnQfW8%5}BG+;OA4UXu@Q(GLknqV40?5f;?c^(0+#S{CVQ zKyQ&3hOjEbDG4?cBW7FenlSc21&gU{-gvG)2{E&({P*I6v44@C1LppIew2vN>%|-oPQiC_4g0zp{WqDK=xg^ur zzrx%c&)0Ocg)l|FTt~y7yssP8vu=^rqdiqxV`;9SIaoTgoO3o}wxKr8obwI)(SCYV zwyvc)tyZ>%ryIuKYSSpHO9u zAQd_if5SC6>S^TgQLOu^Ack&9<@5L{ul4VL0PvdK%!7tEN#vELxlIL2Zl%IZ_)JAn zNe?UY=8GNtSz8R$VeUepLKE_xuX7_Z;tw>9+3_# z^)+F0?Bl||?&>F(rBC$i^#qd$T}kBfy?ABWILZbEhl++IE4YaZWyxD7*R~e(xH&z9 zJ*>@f&XFux_ZC>OcC-50dw1p4{T>GUuRZ=}P-PN_iX_%vk#2(m>3myJM*3o*YI5ni#CN*8#ckjLyRgesQwk+lfNk5D2HMX2OUn&)#-ps%2Iu05%nXLazB-@&a35Y zU~Spsa>egmms9PG?-Q>`;t!@`Q1Sb%AcfEO%VHJvd=H{2A!T`YLGuzwCiEp_IZoTVU*B76#uL6Kb=1!~1*HjG zA@-n`F6H;AI^W_*&Qqxpxd=30ZpF2pIW12V@))Ef$~+aZot@RKxhUwhSSPmBcJ{K8 z;CTCDQy{ijCz)F)DEBPO6UTi2JEZjZ!f2xBn*x^2H<^zimPw|Jwu)k-Ti#jx@|f(k zcy6}aS;4$2#LLCBdYU&pADrJfBPQ3zh)X>tzZO@#;{W5|OPW);huJwvgTAO1-V{a? zIeEepeD0A|lWL*v!dARq$-3)wkQPaTGJVdgJCVaV&52lAOd6Xt3%7oUbCY1JYk$rc z4#@?Ht?5UyrR<5#E!yl)kmgSuXg2NM&Cgh3wU;e;ee)d9OJ9}R-OE*1`56E98j~cZ zQ-dr)b!P}@CQRy+{Iwv2-Z7Cf3B&KphCG8$>XH8fi_%86`&!(=pBcK@-Rauhs)<#N z@R8h6iPZ!GHi+8k2qga%e+6|hJA(&ZndCEwaQG}O_cL!g?RBUdGn}B>Xk{AWso;;P z#sjj4?_gO;%3z^^RA25^r`9naPY)L64=78%RI-*5xcrC zCv&@W3rmgl#(lt77)4G`@J$BTgx^gydI{_Ktfq5PX+hY*9g1248eUWU(>C@~4rWa+Te}?kqaAhEBZ!zr%`BJ%BKX*~ z=}<*#&8!6B)zvaSuo5(ky$>W?{q2%gz}4-3-k*_i$@TESwt$9+$}}$2yRD*-Y~Icn zoW1YLfz;sBzGag#zXq-x@c8OAp=W*NDxwnjE!#e*VQKB|ZX$;QHt zTE(Ta6Hk4wb!=aL-9u`jy!|YzW-BrxckN?bD&CmP8DE>8ru46kIXm+?vDzlbDNnEV zv`%?C6ALH}{2l+LYx($%3F{DSNvn;k8;e9xT>K+9_r?z3 zC}qw{+GF{pNW9?og)-M*;_+N&>(_v^jS#MSIcF^%Al*%MHgIGkuqZ-bU(YOG^m;Ak zHJH+=Q_NV`xOM7jcK3cz{BG;?3BAyMrLit%`U<6dB>#erP3VW0(u8UV{Wp~CUmkXy zalcaXpGZ7j&-}=M&yUScNQuA3DZwn~{1+yC&1WwX$M27PXZ6@Cu5y$K1GIB9dX}H? z3;hRGm_|r{wt~gdUND}5;4Ae{n?m!NndgN0tQa1egqfJ`lRuos;G}qnEk2YFxGiop z3gngTpDkSXh~cc*8Qlh81l)XPGfc`)D=Q-)MKy;D-#%?JOp-!2<96h2t&KqMqK#P0 zj~1+<$DIlG!*bBzoYmWa;2(^2#gFq>u01!aFyIRRevdx_{_h z!*yVL&sH5jsgsb<_;9bmDW6|2Eg5PZgS?ep+wt_~&TO)Q^s!@%)C>C!bgRaV1i`O1Im|*c*?}%;J%?P4bx>}H{kpbe()XxMB z(Gy9PfbhN3?THZ4&C&%_$(~aPk2p-QpPX`n^YSX@Y_&MpiCZogb9i0#RmX`9gcorlpFEVy+U zH`?Vd7`SWy>fiiu1lwdM=sDZU&s*|qwj@28q|vXKWHWwB_WiN&W&hQsz~?E9zMb2f zpd|rR{F%tdF5RqGK8+7G%zO#^!)X6Mz_6c#_f7eWMgL_&H|Ccw8za-KSb2<1cm@E_ z-rQVd?&l>Md0q)`d%5?Fs_Lvcj^CBk8fve;URG4Ltj&>szx+cL;Xj!B5cENG!OZOY z$zzu&x9wH>>X~nEfaeo!>zKo!Cb@+Tsrm=(rc3>>*6HcdWrFlkL)>DPUF{#zv|EWka;ryFrp|^In!InTu$J6ok7LQ->;7P&H z7oF4gdbj>(9j;1|n6Jhd7sj$#I|>y=svQa(gObzOk-6b5Hr*M*mBkNm-4x+Iu^hsW zVpJ@#qIt)4yibA-Zm+U}Q;z}y8$H1s9+b`pKe%dRh$Wxc5bW4+#Q83&?k2|G(3qAtWU1^w*6f?c*7 zzi(!z?_LTx7*&M7KJ_@yPdvT`RG&ORZ)#Uwg!o!dtEQfEep$`Ul)O71o&~gr6t)%i z42Sc-{sVA)ZU3Qc660tU;`|WI^u15CaOO~eW#O;`^@<+nUk$s6zkOtd26w zLYK`~_Fxm_XZF_kkxw=jv;B0-j|JsBqL>A#v!E~ERW(L|q3L9a$qc~5_wQbu8`I-| z{6(Z>$_kkO^!g!g^ZR|-@fY< zo5T;`g6;MF`s1k$j8F2;R`JJY8F?FuMqPkc<|S4BqB1tV2Uu|RtKBizldtiB_6Myf zog5BXER(_y#U`yYtN}<_Vp6Kg^RXw-XU7uPPkk2D#y3d^xBdMbdB7ugWO22sLA-!Mb;EjSDJlUw6Yjg}+Z9`)3n* zjie|F*}kIy^QT$kB^y4Z=KBkg5WjRiqRDM}AIR;Vxhu^0&TCWo;z;V0i~MB3 zKp4P1n(ElWYp@=u-g6o^%i#I4zQVV9rOt$PzV_gwVm~>{!A=SYh{v z>cM6K^B}NsxyyHc?Qurz5nec%$3}^|BT8I+I}HoWkO+uSBPIJRpv@+w_TVBrKi79s z<&{S`({XBbIFL}f8ZA(7AHxp6N@RfCdRV+psi86%9mdn*t}d_Ze@(ipADHyS290_k zu@jvr@b0#xG38b_+Mi2QVS?`Yx8d+_oaN6{~ zk3Xi?t3qDp)i-lZd@N$Wv3Gtj`Ul8w zJ-b#It2N=Vjn>nSRw;D$$+?74aHC}6I=<}@ksZE&oI3A3>i!P6q+Q;-1|@yE?>-Rg zs=1$9{`KsWNES2ujNtnxuE>7#7K1`G_yLE2VavaQ>b>Fbzeyj2*H_zL@b0TG=tkBC z3b#I_6PieEr*@p3*LpZrZauYZDYLG{uQ{0%3tS&o(!W!L@bX*cN>RF*PJ(u51XHn& z_EDi|@hGtu@!RmJH={fy#ni|~Duc^|4ssn6hUgzU~-G@-BcPo;$ORc!~B=LD5Sw$J?qDl*Q zBI*FjKu9EQUw<|l zGg6AdOiC7xOHIu(j!ry=o`jYwXM7<8(Tq2&Sq<(al&%v{gFy>uPQ4gk)`T@ki#$1% z&n7o5%@XBg%n{{V9cZ7{mgV9=3%; zDVij0!6yJ=NHvCD+hqi#;nXyX8Ds9F?EEo7Ym9jRysEFS|--Na& zCx*R807dNe4zqGuJi9H2r%1vqr!DnCJD~#FW=?YcD+;L$x3sNNqyiSf_C&6yvFbWq z7^nG#p*#ZABaL}>{)LC_R3Y{0e6zhZg!DS2PM-*-$d!sl-A~&kHzjaH-2-9G2}7o% zWdjNHw%})AV}Vh7o5ri~z?x*VoMU7NsM6~ybTryg)kwPf;_Ath=q#8PP4^dkT>uno zw6(Au;F!JLSL{snbkWb?B#QDxqMgv&v4yq)b5T-kyH~YnOn+nU6nY{tlYdHEg$0;`0T@xp1^Rri=xxx>qUj#Q7dLF8AbMwa$O_14RRj!LxZ zqoRXd>*>UC<8ZO$mw=8*@Pt+9PTn*MZ*o~@THG)DfQ@)(+FT(A@ieHYD-u!z^^ATo zy8R({u@^ES;WDuBvqYYL9*>DQdScxdrnylG+)iAQcbD~asmQ_vwlbP1tOTe96C`a> z$lT~>`+hyvO3^i(BH?>7c9mz`bd1VLXk9$zOQ8FMX*zW|62{1KLChv^lpHl8*1ZK< zK&ce+l0-rkB0a2Ei{GqG6qm=Tp|b3CZij(I}fyn6?{7BtFnaH!6YrAr?{GWLje z@V+3zD4H7SuiDnm>Y0e=7G$Hz)XlnPKAz%>5*o2b=N9AzpW}e=4qSlJ;z&$RpRiCK zKUz)@vLI-jFt$QlItN)w5-}bMF9aAL8ggMEXvq;@_^5OQmnHsQ!!?W~tW*|2D<1)} z#NtXNRSHLU#FfQm;)$0p7my1_lTQ`0Ljq3pGj0`3DU5cwk4$`6VW))DCJ_S7}9N%T}VQgzaqPv z%2#5Zp(!v5_KmjI8)>!A#%AQo)AWL-q^a#ddED|fplHi*b@`g|D6o8*JlCSeGQM(U z>Utc4kVJm^yYdXBGg^7-)J2Q8I2XMdRKm9$dkO)IwtNb|G6V4paW7RGluMgVd&aFU zUjc;XYp7_*b4yc)E)rzXjN2iM(R@4un5W7wk_kz?;a4DB030Gb{QqEB{~w6;hFKxt zA>!i_AOWd4KN70_hh(9?0bFqK@Ei`C@(vwW8`FYv?oEVIwA2he+tAySPo(W1@wK-~ zMj$)s$7Z-4hemK4LxGNXB1A-XH9!JjTxcmYWlRpe`sY_L3*yMyVoh^@mNXyY>M5E% zw)m>%wlS5}82r%kG2UMPB=b=`xe2xhAz`h$i|)rB<<(zKAqWwl#eszF_;}RCur%2O z7tG)*cE~E}$wr2&VW9;S+qQ%qyJG{*j6zdkXk`_3e3N7c`(E$CiMSClatC|2 zK?@^%4N~*|D6TKeADca~M!=qtrR1|XC8>*fBHMIqM$p&^-aVq7%ap~P;PG#nWQJj(uPN)j%Wh7vd^y{re_S92`3lj6OI;as5ikV zOQw!9poQ`Q1X~2RxMrP{PPo2!NM=xKQXUDWgff}1?+GV)Rd$Fs0(~)>=eE3T<4Y!g zk^@)3m;paarU;c);3(;^gk`R(Tb5(FW3jfH5EaQ*9IM>Te@m_IUB%o2TjjrJ1>wock^?{4qmX}*zyc>9hn5XV-AdxbpeicJLz z3!o0%Z)m^uOIjjjboWJt4bV&TN8cVOG?4p>^dsSmP>S!-rr;=H@7F}qo@BFOT3x1A zRd%1s&;`8Hk0|`JRI%Qjpt}j(Tus~SY;08aTjq=$tNG{(Crsds5EBkSs{TYUByK_@ z977#3z*wB0lfu4NybP!6ph>ObvA@>%@#}P_5R{ZYvY5|7Q%#(&o*cn8MmZ3;-yb^0 zj6i~ajSdq*@Ufek=3VU;6b^;={dh~z<);+`2V53#Vc$Q`qAon<+D&pIa%u|xZLod% zxWdieH2k&UCSe0tuU^or<^Am=wWXJBUJvU9VynXDp%OMbIZiM0H>O=c7O8_VgvAA0 z$xgj`1z#m)BAxRfM?G1$2hlmH4s#+!XVh(uhqm66ZOw>xO#!yrzE)dSKC!T}w6R+sqtgT)K4|I;NRNyH)o7A zZMYDv1W&HB2I%R#;~G0BUX?#~No#7uEJkWZzNeg z1S0U`#{?az_PhUy7 zJqwK3tb+GOI}0L^%kPK+aYma190i?QK{K7(Q(cgH;PC}Fp>kZR{|>xZ8IG!sk?~d> z3BUEj{LY~DpS(E!w7mA6yk}`TLWo-87|0(8YKK6i+?SCwYCCC&pzTf_Tfu}7Edrf} zsfkQVNcUBG+d1CZ*v=be>Ry`2{yscRN%;p5Qw83TpAv2HKBJ&0HC%rlRfbdu=T^| zm%1ERzo2QlhJRc*U+Qz6?mC^TNBkz}IJJ46@2Al&)^G7{^K>!hm=y>_hwyc06;^B9 zG_?68t0p&in`r?2+wQlq3cEbr~HKQTw>u25{|+SkUD z8iYc zFc|DNb8X(%-95KQMDo$FTmbk!rvx+G2D2=E`}F(rsD*^$P(a2K&VESJ?(8ipjpgCT z@0EY~-_+)}`fYse1#zew{p1#gF^!F{1{J(7ob2b2Ect3nzP;+F`@3+Fq5py<^wygv zKy2$F*{1XQAD|@kF5x0psOR-nGDLDOXK;RV9KdSy=ojRUS4_P7(nX|x`+a-K=Ekx2pRVzv26erdBlZf2s=UJd*A+4_WPA=AL z0yoH%751~Y7oxw~^Bdx!7d8AGt@}NzxrZ1H^N)*pGaXOvF&T6_`OJ8Qo$hxqCdDDO z0)@GFDmQ=dhXw+ppSb2+Th`dUKL^9P1GQy1>V8I$mfXL~#TgF-+(8=JA%;e4hlLd) zhm(e9B?U`XGXZglQC?n43^y4Jn91{I-AHXYr$+-)vw1-g5rg;?cn)u!{YR~H4P$Ss zliX7(H6Psi(3tQttC2;{fKS56ZuhGfXRCQy5HaeD?{q*a{>jg71<*anG0z!<|LpWr zh$0x+cUP+$ruhQca?oMNFP0jQKe+Z4^G2Q|6WLD&^m9RV`1a9x!*0Yb# zFUtAW%U_feJ`UMC1$j3epZC`WZETA=J6l_w8cpx3A|4y-9@Q{aEeN8wd7kX{1iE@I zg~%tl3A^Vq<%Y4>eC+=LFn*z;zrJaXy)p8$bxBKkQdN^_0*U9z!k_R~m6$x|#ca+N ziH|>iP2p|2dk1?hp5Lne&fV4yrHm7P?S?Cp;~Y}z(R&4r74VSiAE`3>@9ut!Wr5hp znzgpQDB)~{gdl$$jHo{)5xT4yzOJ!7uN`6k$(WP z(97TBIAm=90F;VcJXB;YU={VzFUY3yQS2lx=e`s6_NJ4uU`!CF^<87ecDU|N7A1qV z#`q`js*GDIA_Mo`6n2UriDsR|jQP*8m0ZQ; zl6%w=Z$AwV3FTV*rH}WsbNks-zlrMd6Im=xYpO@VkmMLBM&K_M z2Xcf*)%xZ{9%bz1Ea@*OGdL`FpTn7Td15uO_XE@~W!rKZK~lwc{{V;c2bf&GRX=YYcH=JITAfuw?JA)T`e&8lcyZ^i zfArN^ocb(Hk|05uezdhld~9pV7`7?Xd>AcMsevT_04whnq(5Af+yyQLmhmvm+NAyi z$o@h8{()`7=udy5B-vLn!F91uC4bF^xNa`VV}u<*Z^LMrND4DqsqehJ))`>KE#DP%5N8v}pxO!z}m*?d3sSo|RO%^AF9iT?>}kb`|~o^`j3SN+NV zq~DzS1vGfmI0viIu1Jo$-T4OyVt)HTMqZVbm8rq1PdbDzYh!}Z2uT-@k58UYUT<^X z(DUE)H}qWShyBMc^j9EokZQ@WKTmgtI;XxcIwMckC1u{X>&^dG=4HEBGU8h^;&Z@2 z@PdDH8p=N$t<6H1AzC$rZ>CEBt)tbqa#l}tzR-~M_N1W)`9d-@X6WbOOBMv8kTR=7 zSpU`>q7_^33!{RAFjLk1&A2m(`_;g5@}CF7{C2-JWQLs#pqzdn{VVjV_qToRY2XS( z=g`DU^6zYrM&mLmow4j4w(L{NbT{m!fr*mkelRsL=+?> z6nG@0x6IZ52?x;N;&X|sBU00v5pZ%#Xt<@(ebh7$Ly}ZWF9CvhwA{m+rA#eKTW;^@ zc|9T){{Mgh!UuS*bY=-@O+xB~JQmbysrY#2Z!1K{e1XrJA6q;R>QGl zn8WrjnyA^R$XUM23%}I5R8c*gIm-7KnE7z zMR-!9@tKL0_02)3h`!yWna*rlXMam-Ro2#&d6&PuJe;1hBb>PnrGe;p!1Z~xWJdy1 z)pq0=I_jz+>XZ@}3Hn5G#{9jYLv-WJTQbO=Si7P;`bpE3W{3f5t{x|ChAi9 z)r}a_4|uJ7T4?&Z^blcs%jNKF^jL%G&rxA$1|Vz&Sm_4Rh-EM1bZ-U((jxAEEwAIj z%hQT9M$hj&t1^WlRhOeUoMxh7W+!UwX>VzCbdInDF}?V&?#l)BcC(hFDNZkm-(se zD2H+pK(p;O8c2|!eam{W#!F}hew~&T@n4uqwOXYJN0QWN7vjrTE+M-t@*UYKvuaRS z!(VUZR#azsC8jl3bQ(T-{tYNBXNN4SGY;R8+PtO*J{vuMc|N@!zEIATtzU{$V|e@| zT9Grd6S<}fNI%@JC6NWG(Wg}!?i~$-;TW1BC#t8zGVY|9^$eI?urkHf@n2~vPHLGu8+bUCFBN7w($V)kl~=obT-FZ4N|jYf?cZSa87(l0Z)go<^x^yBz>8XXFEnNA zaw6NgnWVl(CPKck&GalZs98how+eVS{>m=IVaqCB#W;w#$IfT?yi$Uu8&wR@anN+{ zN-N)I$K@kfB?x%hrG}(EM-NRa=-v=AklNYYC@oOy&{o+n@6rm@tzji_E?%G)Aw1-4 zZrbpJox3vjw`fRX;t@gx4EX3kyc&!&rP-I$Y-sbQ7EK+s(##D^uw-u{k|>7mF{MV^ zN{A0uEB;QZUL6F@kebc0AXB{Wy=HKHUL;J955hLRL2m#im{Oi3X4rwlquwD>HIGTp zyk%ThN4wImA%!g{?X^i)S`o(q3Q9e1Wf8?SwIHypW94gl&ZsEhmF7`?7wiHvEq1F_ zLpnJ}t0h}|NUiBTiPPpA$=%zPXc|7Jg5L(v;H&bK-|-iSuZoBLFqqRMTV8J+?p>1z88B@6^iDOW9Nwgr$_vX}M*k zJY^-4<3>PN{OC-Zbfs?BbR{Y%h?>VD85zq`NA4?4!{Wx%I7DBQiC9j&fFiX;qw4#Zm>RCg`KW@;Y9RSVrXMYaGG?4T(rr2>N?4gJd}I0vTu|=J z)9yTSJHBCtF#kL&KF*qWbcwj7{IqP;Sv-0eSPN6Hi`tARDt~#HXN;ehLy7f41xeXB z$W_7?KAc+`sAiZZO);g82HThgG&0C@!9xku7#WVjiPhSeXQezWm2nFq_caD$naK2^ z@%fow!XTK7-xkptri4Lv>FM0*)FrfD67Gfz#b87yyXz_LKm}M!R``w+R*l5W^qr2J zW~D>41?7Nk;txR=q`Bb=v!>H{rpeO$&>w&}10yxZhb|69Wr*`B@?w=>X?m%R+jYF3pyk?rkGRY$j)f~J#jCu) zFU88NU_~*d#jcoDoB*ooomSeLr1z(SLS@TET~PD_0$PK)H* zD!W@;Ay+9{(t3Zj)aV!bjjd)<9t~7Kha4No4uzDa+)|dMbo(dE-WHv9q=fzhNVf-~ zys}m84c=G(1EBql=NJwt^&KmBJ`cCYEFL4a=`Bbas@v+5=mYoi(espgx4z}hVJJ_4 z!L|jZzEhMASrPu`a&N}hImlET1KuS{5o9iO^!}T><%d73i{tJXm$7=^Ol&YeoEo6r zgiLpUgD!w5&x+{Qaf8o5k$uiTKpcc1AlJ4_sdFrisW>98IO0;QT0sJ7aDufrC$z&P zy2IH_a_WeU_D!`>m(oqNg1wH8KijKc=az|IX}J&$pa&99tn&e3m|x%TG%*xI)iewj zRM1{sB5{8dFK{O$Mv)ed8-{N9G2ZaUJ~6#?mz@tDw5|w)J6B{Dq+>F|6Hq$+4rkx< z3SYJ6_kX1hcZMPSQZh3jS0eM7k|GTKwH+S_Hi}&wnYDAlK1z!{L!@arN7cn$T})A* z!opoZI*DgGJ?rw%=$;;S-C;ha*D|!%)Z2Vi$H5Sg2{ST9yt6SAcyD*K54wxme^MW} ze2JVa8jW#1CzkcgXt`SRsE-l^I(=VxIHwsamr}yst)ukzGk`JRdPxC6&7C7^PTyO3PuX#x&yQ0+5eaaqPjkR6FevHg{ zl{5{_g;~0Vw}s!LyVqHoG|8WY0clyRT2X>;AhVg%d@g8!Z>_SHI)|m)y@8Uc-Y=2N zeMJR5Zk)jz9r9<-x|0-V+2%$)@c#MNl7JR1_=yx-yV3`#d`sXbz60e=(_I^(}Qy6RLD!}YT4^(d| z>r$5nI%PFXR{e|*s8;&XBjD`bchHrTobXOgb=FY8w0BHS*&#mn?OauDj&5-%GJeKWE-NG5#3z@15PLiA*JN3&w zBzf7*3Y4@ly#MX2|8p(HDpS3*otL_+qqsRJx{zI#O`MlX#rjJ!`3OC%;9J6bFhMDf zaGP9ik?moX@=ZfA+ye1AdD-YLbdQ?$tO%g9In5Jw3%43s&OD14YZ?su>nrdBN1N>I zAJ~WBw>^#DyF>eL(O^r{Jm_d}sgY0U5Yjlp~ast1U01^90Yt<@X5a(s#6eb!I>AZ(Ha8{zf{WPH@&-9O*%+Qa$p34pL6*ThLfwymffNZ z*aZYgBWYV6e`YjG@e*4$cJ)sh2K;7P!1@t?JcW`_F;zhK0H37Rk(BTG$;BQhKoHQ- zADuPjsLq*gCc=>(;x)~~{pcRQeGnWaIMFoW*P&?hVbn5v?t11Z_b%hv_Y=hq z5Q-b3$cIupW!qFU*4OmsHLCCZ1K{J?GaaJfmLnPDJjd&rLU06X;&#si|s8j(2JPoQSdSwp8Cf`UIhMJqRY=^JA3HSf3N&#m}7#qzQphAbQwvl*Jm^H*lx~BMfxr^nzk#iAXrTJ z3G;3*8I`C}C31;M6XdNY-hfx6C@NK_5QRe3q`-XkVIy_e__Ssp<8%$N{{VfFFZTTi z+x`LAZ=|jt!ThJ8f~CUKA|%Rx=scOR*XQu)a+C!$r=*?9jpfY3zW1@6#4|qi1#}wn zD&-!$(oP5fFqhxQ2@ql(A?IQ#xO_yAdk@^pxiqV0U#5YbG#r$6B*h++@z2kawXjU^ zBVlWjGu7A3qI9-(RUS%Bsq@2eL%3Ufodu|!D0Eq*P8#uxbjd;LJhf0WQDn&)|beu1sBl9JMjq__RGhw=))7N4YnWtHg)zZ$e+hb61Z zuMyv!Gx68J*@9X#idq~YHXj}fS9$n5N>UMObYx_v-l7Z8DL52{3* z0LiKugz1bXGYTpJwGkjg{0fYvDP1%5*$fKg4Buj3kXk!i{iZ>Zfg?SrYtm2IQ2^A`hcX#)W8=+XmH0KS{ zp5sa+`oaS~E%~8)^dm01`Z`fBkgr7Am%sFH^|#urt8nzg^!`>!k)GrrqZ2$>rkY@F z8YqBWBSZlnC;dZ5T0JrL72FT*d*~4K0tA_mdsfbfrFiqzTU>PZf;t1Ff$I* z^=mIJe2Sf~;%g7IVQW07ub=A;d9x|XOFj!uq+GJMT(VshZdnGhGp8L@JvRy+ajE9L zD*^`x4L#cO_nN*E?^3<>IGp@&=^2W6PZz9pWZ{9L#H7z4+n}W%W3(kwF4Ap0zcSzB zn)V4+h_P%CQP9U{yd8nAT?2|_M&+fLIy&SUNPzRWuf>;?==)llZiX1ezvR>0-OprI zMo*woJUrotXWu{z;e%~LH|c$61o5mDw{AmKSn6yUNg^4>7i&ZMqBXz!H4EUv4VBeJ zkUk7+ZQ;dmIToTnFH!9CQC?j~-QTNw#~uI^z`5vP@rB1(1C_bk4bO~Qq;}4j92|f> zai?d-yl3gbHmB0N(MPo#Q+Y!T%yde-l&w+l-tjLs7v-prp?}lG5CDUAiQlb0rPnRL zN{+m`REontTr!dAiy|VFT=wP2oDMeQ z?Qxb!=VksCY!;q8DOnM7`BG_|N4~~C`w{#3OAcCc%)&6feT?_-uV*eIu9H)ogAp1H zU8!2@Qft;r3kO99%p7r21)61ex(G;*>expvC*Z zGkV+~bq5%x;U}J~IQ;h44$rZBJ3a7NcChEqU$2loc z(vygrh7*wkXST9WcRdO>AJ;DN_D^_+b?BN<<&^a&_nT)IOp7*Hi2byOZ}gANh2{XO`A0gavArD!+q=OCQyy#i(GeiSfa56~BdYXOR3s z_d|dU4YgtKJVOsdbCXGgv=xnZZ=FVy6>PzV70@_|C=08Mm>@{+d28F9?y3{@KcKudikjQk0zc8xbly3v(d5Qoh$RXlcUcJT6mO6DWEXgo2aUN7z znAp2|Y1L1Dwei{x2;Tiso+Nj3V?sGLDDFQy+qD7LpR1v7v@@yQw`iCp|K#}?ro$b5 zo>`6enZFCu;CzB@pTJo!Z};%q{mIYXc$tXkaS-{S=3AS4IbiF~wVNB{c>(_o7fWRM z&$3_io$fh7i08!em6+w*cx=YTMPoE?`61y(YE3k|5dHFB-c-5trSa{#hk5<<6iYP) z?5+>!<$pZlcfGg!k0L_qBe-AW?pw9OsB2NdwHw7#4c!^h$(ydf#=n&(;d4J~Vj^6m zf>(ms`DJx8KZtgAsortuxYKjmk1l^6I2(eo(xEct%1qmzQ0S>0Ig)D@Av%1@ef$Yh z8@=|m_G~I^OMHh*u#eRDc6EF3JN=*5&WJM?{H`m>6Hlc)!7{Jf$S5%Q0d# z8Pe!9)g1)pbeyych_6=yzNEihD@&dq)5Mt{KGRhHFYeto=hv5X9cmu7(#^C&bI(Yt zj5~L^%x1r93LH@WvE@1~VduvJw%XQh4Wdf!#ekbNQ`_H`G?YALBiD0RyQaAhjKI!aMaxs(nIBBEE@v4$P6sJpKRnY+FHdf6fnl=wB8-txXYN} z5OH&s@6wYK+4u5lykuBp4$$Fzk_k~^i3-sx)&!7ns*v9)$Gx$!y1JksGSwqM8{GLXjXFC&s{KUR30R=IM?z-UVh8F7r^yUsOrXR&*zISAsfzs^rmeEG zCS}{&le<;1O~-W6X~kY`KP`nIUY~-+G=Ao)3wu9L$+s4;{F>xX-;U zD?ILI`4R8cDc!Q{H=IYz6RTPNa~H9By)Sx(d&h2PWnSo<=eL#b&|P%=td9y~st)!? z##RH-(n&b_*v*q!HL-TFL2hO9oCSSZ*@HKcn!+vhRbDeT_~xBg%SRp$CqU@Pqg4CC zF1cH26VuAiJukJK{!tjO2+DGY3pKBVvN9Ge|L*(4xumlx`o&61=68eU-{I}vv>B16 z|Mb;0k>5f6u&XEAya1?6KZ35}nvSminZ=3u%(ZQ@mgzqV@mGrHHaa1;Zz*hxc36xe zeqVp?41OO5af*r-XDEB}Q130UbWQ~n9you>H1#BR%5w>s>^p(7-j9k$T(#lxwzOsX zA3(a9&(wS-_5ZP#op=bq}vH3-p1VJDYOZ3(l2-|U>cmAiWXNEwBP zC~Z&N-2{*wx^AmHTYLE0Py%#Yr~PA7$}8It9;<$1Sr`cUgZ|v|u+h&(z3D#TEzt8v zQ-U(fI7rLS*rBX+3s41J;)0UDTYc`C#gVEWWBkf%gOkF4sRti_y->@>Ziu%({QZ<} zICGm{*TJ3QX~90JTH0=qH>|rlg%FquR$RtT3ys!4Q+ycKG^C%y_bBzpck#pE46J&C z!u6?J5%fo;qdy2%UA384O|YvmCN|AYLCpCcy^MmHx7tUBfz`{osfA$%(+YOgZmVe5 znv*ZThK{XNvR+(D11Xo)Bxfrd!d;7D#BhCq_(8Z|`NK9ia&|!P6M9|=9H#N~H{8u< zQHfc+I&E^+*tQSKvROe`9H+}pWUJovAeU~KwcYjp^T=%dk#9E@p1vsOBG`J?WvST7 zee|u3og{6~7f(V_H{*Vh%G;=-#mCP!f46dlHml@ZY`W4`uvQ6H3QEfV zk+1)M`t|?Zg|4xIj41F9G`oSkFzh5 zq^%Ut`YnXl@;fVWE^XM$7)A~vXpMgKDjuNTaQqlCfwJxb$LtVA)Gt*2|;Sei6+rvcZ z&ZH|*VeXR~;Vr`JmuW=*^~Ff#vAII4K;Kyv{&P0vNpzI}P|p^z{e8&k^cTNOR+vTV zLw(s^(Fy<>FQ-jbALedTt2c51A&quSXvUEX3jzQy@lR$&gP8?Wf=I=3Hl-G8l{Na_ zDANit9wcxLR7EaYx(!3$lWyA^mQhB8nT{&C^JVri*20NAUljOga!0rj`r6!+#MY_L z;T+%vJRTU-&Ly2&(AJ|^JMOM)wFHY5hvOdyT3DJt)~_PlHDIokAPnK#2E;R4$b+&b z(Gkw|c8Jre13J}B&Ls2H>ofl{wFP?xEsR<>qp1^m$BS%9eK$20&50%DN@v@9eNg$Q4f5-CSGOB@$f*-0q#j9{Ee6$26|t@=S^8wf(i9}1Ru#bJTeG(VC<(wn zSl&rH;3*sHSZ^)(k;KjNC6GC-JEdv5rcvOmXetTmYy~aH2Vw!LD#(7LBxPJGd>cSe z?gc*lQm+AMzQTk0Otr_i?-#;4fd1m69tIzhUdR;&drEk{ogo#V24hHzFecqwI0gv- z%oc6=pLQSrBo%!<)VU#5c;R21B6 z2Q}Vw70H~7Lil91EQk>FZbNt%yHxRIzW*pFBVn{m$?p#M0QYspr}(@qqqHTFN4DTt zx$QGv;dk@}DGGFVYru@%G-=#FIFKJ87EELO!Qz%8kYhHOJFjuR_x=Rz97;a09cJEW zObHsSMYoCG!Lv0Ob$r#k1zb0iys!8keLPs6Ci4i%tdH}e4u2XSLF3!!W>mF)eN5sk@|m$o{NvRXAUqR>xiZZ^9Pa4oYqW$kt{e9}%t(?59^Fr7&T_+2^FBg1s85cy?SB(YOM$2wn8A zE@O3EV^p*eeaSv)IN3{^1O_aMiba}rg>NH*xGblTP4$ty>w@ zqznUY@)zb7dS4o`DgGMKN*e#z+@oRdkg*hLs5BdWUfj0 zfz$H`2egRB6#ATIi2{3@c9<5^y=Ac;5G|0`JCg`Qcx=-I%Lt6SSi-n`u8)G=dNO!@ zdx)d<$uLz>yac3}&%-(tj4T)v=G%RYcfw0zB=&&eQh@n{VHDCo)1&C!+-@U=lgN1j zoXK{qbuk!s3Ewlj*>)__>(!uRVd+X2AmY(5*ix`+ru-t7tLP7wNPcF7RHpv;r4Xgy zQ=eKkO=Kz#BR3U|yrAL?QGhvuMBxo%OtRLlP?Hlov7k3TIR{=XD!g)SDvOkkde@^3 z)9uQG($v`T)Y8W)oq+~U9V-)%?04C=!WJUhDS?9>vFVQ6)Jp_4?$nS@%Jk4c5rOPG z4_~Ghy2)+5W&^5Io0SYAaxMWiOOUHrW@ z$g{G>`$3N8K8j3mVxX^$8jgmpy$G~#SRV-_7I?szo6HRi#`FTHSNDVa{TlPtdje~7 zGKq&WhGiyN_}V4aVAhI#NC%M}M_o1zHRlJtz-#uQ+xW3EQikj#XEQb^0sO8@zD*8B zSW^h2m8ZGc-z|DS%B|2T5T=(I1yj=l`XV5^>dol)n_pEmKSJW@wd6}RcpCX^oxDNM z4S+b58Adjq)WPn$9&V+CSerMGKyC*rgRqM#avs9FcS50m`RU&4%TNBYK!cz986g6A zWG;}kRu*_0zbd4XXj0jPx1XPfAuGqQDnSNL&c{hkl3sESfkJ3Whr%xSj?H_#Rfeom zERm4hWeMPFx)nDV^2$&geFy$1N5-%x^I(fg`K1f^nYn?rcMQ9BdUz=iLl|O^H^JcQ z`#_Zp{orn28fs8dWzOUaGC0DJAfe!wM0$UK(y!$?F}jtmA2@k3x?Tp{fFn1xvEi5~ zk4EIuELj)F6$5-aS*QZYNX0y(Ms@y#LLBQMOMkJcP~*p?zR9AAlQQ-bvMNG^#=l8Z&w!?i3QW;hgpZb0~q%92dED+yvXtDmTCaAqc+CDd8p zAl*i|pLGdC+hy>jo7=l}-BNuu*1<=;oru6BM>g9SV|ut~=(s1nwSYL|B5?Xx;E^e^ zVLj|GmZ;XrP$1|n@&uG_2l6S$@GKf zwi?&Xy9LDF8_ZRfYANs~J0|x*R1VdI7lRTQZPxOQ-n@p1$U>grgA6Hw`C|;kS80Ad z>``VsQjrMV+!&r>PD%au`k2RBcy4oM!4qiBYusuctfR$kNyW=iNl8;wKUm>5c;QZ} zsLYEb>t}ya)Wd==5knP~EqukHGDYIWCBa-?O`rE?-tb6$xRz`-kCj(v8ur=K)s11bom;}GhniNg zu3dn}!I>&J$0fo7sS#tUV$$S&Bbi4JA%J<~?+A3;hjxLdYG9O(5W<1fM3_WLS+dX8 z9kaa-;_LPSgS~4?n}Bz{5^b6LhN%)Sb;iEw$k=p#x*K%q2o5u~db zuy4W?s1hlA^HJb~JW373_w0%tJ%8wDWHZ-Xn@qWaO`EULAZUWR%5O?QzVC6A^|elB ztU{Kca!PH~T+wjBxC-uhz8T?W>NHi{oBTjeqCNuUVKL6w25%*i3`}4l;c{{FW|cY5 zpzY-~$We|Vx@e$JC_(`->{UuBL#|>(!;Px1wB5=4xuek3^6<%hqmI$kDBv>b85%#Y zi7f%WsgoHB7f)66;rZx$kFl8-Tria7H$2xfo{h3=6z@_no?(lYCf>-&9At&f49)t? zVq_J_8KGIg2ugr?*XMbb&lQ;GT?oOAO4-kV4j;^_x9K zVydjz0$_0?+a=J{{PDbK@MMKcG##l3b1Kz+w1W2x?T#b#3Lkg0xR%~$idj0;!b&gV zcD2t0%U55B$lsh!EUB&O6I3xw<`(IBP!(Wlq?O88+j`Z< z@bugEqM%v*N!UTKl?rgqDw%3zLm%9U9IXxq6{EYgmmj2loUF7_=tAKqp7XGVL0rK) zXS)P~PnqztxDP;U2qIB}Gi9$V#FJZ@v})do>^fU}IX(_Njn$-v7m-hOv221CBF~v{ zcr{p!kHsULf^!t`~!_RbLMSc6H&^8cS*$Cz6uNr9`#@khFIzn)f;ed9t_q8233?4_*pSooVx!An_XZ$T3gDFN2c$aaUh#~sPzMc*4#>Djkm=8ZnFK*7I#l>b|z+aDk18GjA7{9j9MamKn~ z3W6H^bgqNb5B>S?2y@y~=y?XjZ78N`Xt$8NBVib(_j~k%=4bh?u3vL}@)2_LsR=xqq7}@} z36k`ViOBbxZp)qQUxGVs5E6EJ4=g&zs-zBYXK6WYP99F4%52<=XCQ3rorGo)D@TL$ zf91cWnum$}q)&MDai%ljh97ryP zvSvVIM2Ldsw1=P?l(8VX7ZeU1U1MnG;yCBBOyg7xKw|ZcDm|E16&9H#v9B;Z)?B9p zBC07EP4LI@Q@=)X@nkY!qEyhpn%qG|pIFghfmFZ^GPD`l z9jQ`Oa>w!o$soP^@X;6Mhx5>ax*8}R1rdE-P@hYCv&U$yIgtUXOHDM>(~ElesNdR0 zIa%uA4>+{_7RDPSIH}tQ84gSz(->S)Y`bsZ9|ecQi9i@psyeOe`Rg=)fix1988vQH zV&xen(z_Zdl2Wr`U~Q2kv1})oHm#i-!F@PES@MtK##~r85*ZT5K+j`>OMN-vYm~Rk zQ5#auY5Rq`JmoFj#Ao)}%7iMV?|~hE`a^^)X#OJmw18c(7#Y;n*WO@bheMO8Ypukd znm8xZ`(`ox&bu#~z)dcIns}q)_%^->qazjgStwlrJy0W5{-l>w2OXMKH<3f`xL+x40uAV*9N_(x;Tn;%t(2`L+u!o@I?h|!4?6}rSkt9$=F6`=h3x-2XlmtpUbDN1<)o;7r_n=g4C zbm|y1&!C56LRUg@3P>!dPKp(y&(Q)Hpw@QOUL?<`BqTyvPp0uMG^0sK2qz})78|c{ zUTR`;_lkNQ^?l~aVwO|oo*h4zT~VK#dp=9dnrOp}JftJgg!R1{6<%Rrp>q`~24r(AVi zgq&KjGhzXE(MP_ZD=59pFzvfPWM-1-k7ifq-5> zxPtm3)WQR+7;%!}#l|~o7-#zPNu-12?W**Is=wUte|G<)sQoSakHRiWobF5B7aska zY1W#!y~1NLj>rr3A9|~o7Jr5kd-zm?lmqjMG^4pYeP1x&jRHQr(7b@2*+5NAEs&#Q zEpw$J5WuNpQMY=eh2xfP>Z0YLM6i15pG2g>iz*}cLA$PbUT`g4;x!2JT$D(yV^9sL zG9f4P2vi~ISTkX42T_@h?#b#p8N~Ry1(3cLEv5>R_zO5Sw4j@)bSG4!GXySzg73z& z0!*U7&OI}pQ^NW-D>Z%#H)<8);j@Nwl`%XbffBd$%#AC{I6w3b3EC=!QnyXcWID(` zf=fE$*OFZ%x(o0BqrfrbR1eWPgGE9sI;Pn%1K_Oo7lXARKz=McwmlrgyJhL6_b7bj zPj0(^_e_jACu!kX;SnB%9sq!9;_y%t7=Esn@z^C|GGy{|0Yk*R0Kkd5{AQ7JoEA3R zwZ|QItnkT{1tF}=F_0`0iCBDo*duq=tj8Psh8d|HdQ++gY`JYBdyUz~)j+B@*lt6_ zYO1&-b@59^m%yJ``PQGePb{taF7=hw1Q4#@F60(I>{S$Tze@gt34eGM`SEWTNQQH< zcTyqKiGv%zO$G6_n*~CT?{F^Y8sFvE_aB~K)5*U7vr*mWeo{ojsOqHvWNSV$xBE$A zR3}9>$tZ^Z2IiCuw?C;ju22Zx4Ir7NXO&m}PA{52`tz&4pfx}nG_TF+JG`+WG1mvY*Pt=a=4~>#$-Pfs<14 zx{@U!cRNfYo~K<+3g&AYwu)k*FV}8B;;XX^=31kFTr%i54E@FYjH}o@ED3yjwx3f? ze7fWLc(Me;wk}Sr+D$;h%>AWDq?+wL-k|tsUgz5Vw)vCu`s7cgNEurFH|^n^oY1d( zkBGleDk7fd@1FmrXSTH;;Z=dvdVOLHIhCP?Tp<=1H;icl_h+A2!^3>Hj1x|NK`MOH zdzXI{x1~dg36rDKS8~|}^p(=H^x7q-8%JlQU#sYln_$L(hf*Sw|86L4OCjWziwYI+ zNf1qVel?|&PgNe0Ghnh}1Ahp~1rw1|pt9_fn^~nplS*n^YB*p&89^F ziKA!-$`zJokpP{+Zk7z1Zu8a5)#)yUmuFKoHBsXqUyFwf7EGS{Knp7bmN_sX9lc-eAERgf{gF5b_R2I~hpl8UAc=#%`f5ytfO>M( zuv->P27kQ1Kh~}Oond=sB!h(8_>IOJbGfP-AC8k=YiOp0|Ap)1?yV=M6;>*qI}T@5 zn(XoUC-MLF7R!lwp95`WxbnzrkLbayLI}EZVe)ioLv%(3VwUVs48kpc+trp89SW8C z1K~=@l{1cRsV-cw`~+_y9sob!r-ReYn^||d%`(MbdgE#Ia}%<2^5V0(GdETsB{HO< zja|L6@Rig8(d+l%kLD-8w}Sd?-_H{npoN`YjlwF)DD$+T5>5`%gc^YH;XSb!*}>BO z*Bk15+!Z~atGFLKU)3A)QnY5K{Cg?;`Ht6!74v#LN&^JN8|p6;mI@@0S<{glxD|i> zzs4wbAwQy30F#Mtu~juR%M$1o!#I}kovk8 zJ!g@_h!R)*f94EPn}@_mumZ$=-|4tXe{G5uwneI_tPhXslUq72$$X65H@oZe7@D)2 zBX7&=s$e=jFP{~3mh`n6U3_YaDVjAs4=pmcbs2{C6n)3()(qhIK%G1(c#_ShRIIev zeUS3NBiwpsEx)*Xii`h#)rU<0ixECBTJZhZHG@9V zo6@l=cx`PpaUjp=K=-{TXCY8zcE#-#$FmL=su`SYxCN!tt|@;0UIL)nv~KPsOW1qU zUx$y|=iQkXX-!UfuAtzfiT0gR85B0E$4id@X%CIe?{1y2wO$5Kxk4ip4S9?CEOrg` zNcBoSfyamP@PoAsO09$+l&YHbj@&m#c>0Y&eQvK$&9*Gw0M5-4EZd5I{IC2&L*KmO zUIlw60Ih5Nl0TC5`*S(7yw_SJH zc1(CndAqs-FCl-oX8y>ZY2QP-J?n&h*ss2kGHGgVfm0R1{%4H#3`sXc$4MG5Xo_?I zAevroIMO1ylhjtgDNCK1^#h-1;<=Nq*%08X-c*^>t`+MXn3$t+Q~VgrN<=N!{NNNm zGI>cvSg;1QN?3uz<|Rqxzu?xxXSI=6g&T@DflWh;8#wCEFL`&qMoN??IALS;sZWhc zN+Aro)NK2({lFU63mwiILK~*|$P65s^KJ6%VHPfdo<FAL0EN8xK4o=ARUV!22T z*;NNOR;4fJC6}@kcx9Y;l=$Ohvhv#}do+RAP>ZmEF-S<14nlziyKb`eo8o^_!bi%# zTsVeNA`OT=SD(R9F`cbPX*mU&~PObf;=dXu6e z+1l$O>Yvb1Yef^aZT?`X<{ABb&9oD-Dix37nHw$MV1UA9G&EK{O|b0G!>E3d@au!g z-wY;y?Zz=01#^S|N+z56uM54PtY1;pCyhQ?aVhAe{n!y{vU9;);Y*O2EOZcenG$8u zNAkJosa9=$3XiQu+8ipU{-dz+kUWhQBzG~73rAM(|#O^*;)Yxf4tsMLRCJY;e={AI0G*{DYtAhX)IPsWLs$zC=)a z&1w`@xU~LbZ0e(t`ZMoSSzoGd`*()Cs;H-oa{v{oeK>Qz>bYT&Iogz5ytd; zC7xmnFaCgK_`crgMvX)MQCLF4LdZ8TH0taFcmtY!^hA-*JHVb+1p`Zmz{e=W1fqI? zdesfg*TLejEc+))`T59I*x+4*<-9fac}OeBNJQ;p=wt3g{oZW73ppIRW8kW(vsFKd z>`Ry-K&y;TI%Q=4Q6!xhyMnFp(?rVh4W>Vk@AK5dR+GJS*6(_&OTuN-&Dm8wK6~`K z|I9URyfgW2cT--~kDNh+u*1iOLo$gCf5$fB^A1`XehK&p&J;|ahRwKvg#E8S+vUy; z-`M?QBeEiFjF}jz%=t$FTaVvTq+B|hd0uS2y`i^f+AHsFxkN}acWmG0@ttfU{8I5h zS4?k-5If}lyHjZ*l}XT+y@nD^IhbpTEY(2W0DKNdyZclbM57hd7tVo_OZzGUoS()z zrDdsdd8{Q^mHLVnZZp6G^XuN6v1Ks!%B;N76MjAUiduCL7`zcaYWE)t2>sVAQ*nBm z$FU&;nB>uGyO?r-23wO}&|8OHa{z#%p&{N*UM4(d_9S+^6oV@zql;B|C-To&h%kkK$Mcz>0%sgt6A4H;-SbL5wYr3L1*}bs6%)|fL(ExudJzl|dO7QV}>(xl9i!@Zm$v=v= z@6RgaIjfs+Yy$FtOc8~qn)R;^q7O>;JMXjEoByD^&JViT`qsEE|Z*776uOZ^;@LpCa7Y#jN!+&)*t4(7kjca}7X18{!^ov06*aj&hzsW7g`n{Iy z@{;0?vIWMv<)u9M_jZhp>%b?5%K&fE{VQ|tw95S12YBH`%azG**-9%_``G>xHL0L^ zp&`7#Qb#Yx6Y!JuEoSR(wX1F}yH3tcY1S(7{c!V#Vk-Azemru*@wE9X|fwF-o{vlibLHRLzRc^&zPs%AHzEO&Nt1RQja5Ex%llxvA&#f|zxy#lnk?fdzkzSs?Gl$k!+E zKO+BO4UnXi%=6fU0aXex!kPOq1>`}E!x<}e*cYBZVCY-2J5j}95x9SmXc+!S!7LB% za&B|u`@^Tv)DlI368O(JENkKj^auH|<2hXj{4o3ew;;yH+=VoAW*lns z&|KQEPf-FHVM|~lv(X)t$nyWG=qL?U0Ar>=k0~GsvS~xIyH~H3o`fHrxXg6YGka1# zxl?#Dy5`2!;Qy5vhU)@Bjp}oLeo6sXMy}sR-MB8P$XljM7v)_BCNd~oo3a-zdZuEE z$j~#>bZ&glqnhl~`#X1R-SDMjMB~;QYk{|=*v)T2Jm6N#Q28uYym4-!4sF%kKZ;4P za#GyN9RVtDU36jxcvA1uHiaLps$JR|e z5eBX2+>8AeY+^lWuK+BzkWbNv!Mrs$j0UF?t_(dOgGIpi&Y<05qEwY}5`!%v`b>jl z3L%DWXDAD?*B6GTeAnyw`%;v1)^dRA_Eko(T+-w}O}~p)u1>Zy1Acy&ry!a{_@VFz z)7ol%xtgpL5OF1!jn(gDoL5X3Csq4Ka}5RfvVPW5(BU&X6@_5;_*c3Uy(1>7?~~b? zH=5ipu2RQ>+}n3-H9D;liSP1$Zzpba{@u2p(t1{p2*#Sd4b{(U+s!lNGZ|*Od^JAP zsm3;vC*p4hSW&3Sc{&>UtZsQ*8bFBEE-%%_-)nCB{Ryv zni5}+B7v`8!V|G%Pu~6XiX5gVB>$=31r9enUrqtV|50>3IWx@rE}-xIy4I?6?Lu?H z&-Ha(6<3i+f*&sLOOntAp7F`B=N02q&;ehZ7gRhCt){;|_ra6}kEvzj*M?!Ss%ck$ zJOqgm9)3gkC5bB}lJ#&6Y$6Em#c)}@`Z1&T-m=NHpher9g*Dk?P(b^?XMXwgUB*z1Hg=60CG30O%{X=rMUvgF32~KgW1+|7-Ox+^gF| zTqjH_7l1%VO*YN(LIBSFw9=u^?twDU7vJv+$XlYKx~J# z@$%b8(a>u3QXKC~Vz0s_+wHn2mAL-$*JBWSfmk{BXgo?&s0Q><`aiBe?Ge-nAUH08 z>cdBvy2+`rO$W#^Zsdn(TQFZr@P2W9L2XC+)}gMil^8)?iWDbcv5)#k;bq#?4S$&}+X2J6hWKJL^QSOJCddhWv9nljDCB z^ZCEoivLk$_#Em%BmK3>p5b%iMrPzPG%K3TRvA%&y4@jWX~v1f5w95-gM*b@Aav56 zI3&1K7ApTpfVJwq0nv;p1DbACKmEyQoF|DR4)a9_Gg@94m3wPdl$@rT79mo0*{~O! zOXp>h9Ji;uAIVA#y(=JUI7*LJ;9*Q=cs~iob1L|P_C)O$osfrGfXdNVk*004$I7r5 zZGFNoAPtoF6z;W^lZs4(;iBp=_Gsa{`TOqBj=Mpf%v~KPX-CPbkjWJ!(0VtXx)}X% z@UAO{|4=?qr~Z!PiY5?$zqX8xyYFz^w&tZlE0R`(17Zaj8Ci|}B%4MBmxBRD&vIQX z06HS;MnS74cz3B{qAcDOPrk|-SEmRC#n$LRQ^zngBJmvu3?+LLv&faftUqGa^HOG@ z6UR~nw05W6gukG^V{$)Zx0X)h9C3pi{;BGb6l&0OP zPmCB-x^+4cNVwn^4Jh91^`+0`%FYI3x{>Jw40q027%jH2RH&hi-Omjqnf|8BoU8=) z!M<=@UHfuo-F|y-Y#Da3L74_v5$bid(2Q>;lZe18`AzXI@nuQ0AQqu`N2K4JsueY2U9P0z3_zEzFKRvgUD1&qmL%cC?M z^D|MCn`sXe709Z?huugj&-79n!8r@;i%*;0eTDq##d&PybLJHkY#bviZjU#wQtAu} z+0x%@H>_gL4>0P8`4axXCWRw#X$|`r-u|JOi2jJ|`t>K%5v!#BKF&|(*-mW<)J|5+|64rUL=_x=<}h-YAn?e zzjUp-BS-L=ngpvE&hFUc`ngDIMMdImz0qoB5xEdtN89Ig-O=$Ulhz-{8>}K|udDpW zx|)(}upHkNrLHeXxobFSTNAue3-b4tri0&c?5tIFhP}eu#=ttgoYu`(fcGWjbb0HB zgnSi(a%?h^`%(Kwd9bPsvJI;zkA37#9Bs<*aY!v@$SPS4DS$tijLSdgjbqK59Dj{u zw%9KpXDc!&sCnazVBkDJhoMibVM3)LMtl6$EuR z)GoZA@RAhgnD;K!$4WH?N^1Ks6##b*kv$x4`FDE(C(z+S7Xad2K1yj|hZxKNGA=DxdQ5UEi&FugPQg2!pONXgk9_ois|C799OVX=q5w zov;aSF$#*!YG4nYp%+`t`2_^9F03aNCh3tZDA#jRF=V587!MG?SXZo}zb9uRy6bDd zukY&dI=i!*?3YpB1t%I9NOM7mN?#{rPT92Dw~%ZlXeRyOt{fPwp-Z{;Qt&=IU!KMM zwOvDMfSxqp`mR~`JaPtvFO$B5{B^syJ;E0ErGX~_GYtGBnf#eLe<1(GOt~SqW@god z_X~NXQx_}77?Vt^*9q1{40gO3eF5A}ohvj^*-un|f6qxX20BtVAdGw7VN8~HEo(}v#QfB9&S z!7V$O23NEBz)dpVz9HUzt78c9mlmV=UkmJ(_@M5wuoIyYj2HHEWq6KaXyh#4Pyf-@UIkJu|?Hwa8WwkS85x;#YINxaEOg=9&Lv8j5 zKix3JjKgKPnlQ@kmRW;Kw)!vJ(0*>$TK0Ly6^IHv4jaCo$y>?}zM`WQcb~DZ;|z+M zbC$Q(^Yp*BRQkWlR9Z1oU4+JQ($q^P?M_zUBJ~+MrgvRqh;jZLP@ZTo>lpNI>jqEM zTT&|9#4@?5GdP2dR%>s=iJ?QCU8erOo@$SV9#V$1iYu=W{d{3I08k0h0L9>ImfMXI z{7k}q4xkR<>R>An*}paQiD**77_-YP29MMR0*jLwmYL$qZ>Cy|N1C9Oezx;Mlp>lJ zSPxF;L9>xz?V`*meM6rjSAP~Gpn*pdCP+2OYQLwo%g>FmoREGWOdlngGWY%)rQ5PZ zLyRP;o?ybZpveOc{+rd}V2Ecwu_@7Ch8oB4I$4 z@K~jt9^5OIB6im$-5iUz>P&V;uz9`tM%l_WBvEuikj6NzM~j$9t?19~Xyv;dik+&T zIu<*F*;J_}ayY$l=H^}g;!Yww=*F_&xL(q)^;F$!V0uJ$GX*CAkZ^ldJdzhfL&0P z|IpT-C6y^y&h|*GqDIq~dsz|pQUf|g^#6wJa#ny~Qy_fLDaG`nLy_AT979Pj3($cm zJGmQlxPA18D*=T#>;D!n|4%D8N@nbyxZc9P9t|6T(;e3Avm9Pd~vL za^NLxUB(1;4D6Px(a)SP zJ!)&Jaj>83A9C&94nO&pLny#d-a(agrcg^3h#j;INqN8i2rCz7N4X|S(FqZD`7&B^ zGAsEx`!KE`uipY4{c`R>Zg7tD);J_3k2_3MKRklt`%yX}^#?HjRrqJg3MfN%59R}K z?%OO{SCvPgzHIElfBZ`A4!6`LCt7sKBhl7yKtm`q2k@wp8C~`K+6*Xrskpfm!=W~M zv^(@wrskGtc4`~9@apQF*bn(y$6gXi5=b;80x~+SH1^yZO?lWw6ta zX;k5TL@i=gtc*_O$50igW*TGjjfbWqKeGF5qeq$bQ*HTzo!MTQ=~Brg6n)7DWQY5O z4ZJk1sX<@dp?W0x;9`2axxKCu_&iXb9OtTfQ)}jF20u^OJZn#cY{XDLZvDY7bt-<# z%%U=3sBNmkc-UuSLW4$D>X+sZ?yvd+-mS36`@^T|xUDVD{O1nA%Dhx5y{Tl*noq7@ zsNVb3NG-Wi3c_7+aeG_D_fcXj1`rd6ez;yKEidOl+1y;zkuM}XAmAUGF=)z z<|-J!j)@k1l3_mQ=GwzHE7j*f%-esUWFtf~u5b8C?m{nHf%4Pg@j?${3q{?mqww+N z8%OnM1#6e5gDj`7I>a^Zrq*vs$^L*n6X^!TSX@^ui!F_MZyaz){hzu0ulmRKG(Wg+ zqN@A_ON-PW$@cGX!rh!4A1r3y{N|4Tl|Csc=|HOiZP?rHBPNBF>37Zsv5NA9-ZLAf zxMiQJ7I|Ir#7kGm_h#MUmd13}H*3LWVuX^$UYJp$@R#==QPT=XhrefHl$aSF)*rgX zjy#Gmhx60XjD5UMRiCWxtOm&BZ{8ngrF?Pkz5KD0yLt*fLNTVp z$m}uru@ta()G_L}#|F!@nCLxiU&t1rlpZ4XDPl9AJo}@kNACk$iT=F>$xjrX@Z0FZ ziPVM74N$~`{tl%ajb1c=ZsSH$_4{JA4>XiF;PnsOWj6aA`l*u?AV$4zRBs~cqqtfa z1{*3oOrQB%|1Rnd(*G` z<6fGYZ_lj4MCQ?shAmZCN8QZSqT~?xvEC=8D_go|!N$|3Ej`w!)<1I*zg>KJ`KHt_ zc7m(e1TKtev*#gX+z-+2f$6w5&0T{YhKSxik*$H&aJNesA}Q31s_q_bkwy z)kd^hW88V{gcZCpSA`wp+BP+)9JKOcT4|c@Odh&BQ4(G(V&$(b&%(Y0ByB&WyO@TY zemTNOhcb|lk|m2%3{YJRQ&nTDb{nr=wi-Fm_m&`FN8u>qo4U8LaCR#>8PM%vrOtF>z zh-*wf%(B<=)kE%$Z)1eRcI3hXa=0&;CS|lnOdDTOF{|r14u=!?6)CxY71kyt90YPb zK+jgo=;2{wvyf1=F%3%{!cR-}!9%u(p{Xw&Y`?^9y`DA+2pO}$wo zdGKkTE3?*utv+CO?)d8RS%Cf}m7G2cBUk9i7A_`qPKY`tob0{vtEBlHMbI?xv=?Cx zaSn+89OHx@$9^@n0hkzUP#Ct}+YmG7(vF1PP_KK`_4Au*+06~Qrjv`MMC$QOEApU? z@DB0ehe^-puMX28Tm21N(K`bkb|0oS zx3jbZxNBmLy@|BdF%Nm(`3BMLYK$83b4A{INu>Xkgb+z%vEWD7=N0cLQO`i>H&KF- zBsMNN!}k2EB{4%<*TlE15v#2n)~!@qbYKe}!>3Q)##m8vxyQ8=&Hk-YUAk0QPN0_kxD3y zvcV_;0VPExq+9qTHV_7*TVf#HDJ_VogmjE<0TraSF%Shtj`ZXHeV==u``+h$e{p{2 zoB*e1_W63loXhqQ(MUzHx}FXWEJCPlTX=HWDbiX*PH2@Ya&khV-OSK7y6vdAeF5{# z9*35Kg$1JWD&BB-Yi~#A<2GWM${y5y!)V!D$qP~WA_rT@bs$l1%HBT}Zn|46Pt&;l z^VP|o>1cjB8@tYvAd#ifqAM^l7&VJyQ7-sprOVt60zE$u%OCAKfJ`zQ~x zW?;CIWqG?WX%ht+UyH+e_Wo(=eWn72HZ}UHZJX*{SP7?+Qp~Yj1AXJd$5=xi869DH zoM|G>G%zh;^O;n-0$tFObfG}6Dcp~B7N#Ndc66@1 z?R=W5-bqx#r?TI3gL#!vPDI>#PHBfHZOFZwvo=re#51?V`g{fr_7Qx3I=UA&M$6Lk zt+|;N@?UG^5Svk^yTPM~^rO5U1FV!RL#mI;hOrE1z(Q0|Yo8FO*drMA7j5IV5sD^n z_8M*(2|RD4dR}{GJ9SktONdNZSiNzq5bhHYqb1{Cny}zWMn)*R(~BH~#k4@VYdQ;n z?uN3&_~{Ivo`Wb|o&4X}2`xxitJ9pD1Hd?$B3-$lzTzfAs%SXKyOLvfzUwn=Y5N;~ z-R)o=U=1T(I4F)Lqp)!1V`9ZK8(ZeWyN)X`$_VvBy{ld9sz2P@RE9Z@X>a>o?qQ z!hH)H8Q9ZjhF}%Rjum*#uO_|@DjloLCW8~YA{%$6!=ATI=r%VaaTv?`%>B!+#z&qTHaL7vNbzz61f~LY@ zW*q87chW=>iux8HKgeH7*T)w5oDFkUysN7WUWzhOQO=xM^12b==y(?wFyWrnwn1LI zYzLl=`fe6I-Wax_*T0VKHq;p1 zsXatD?%<|lAeuS?JgV?6?O_`R<`41|LwT)cgGwErE5Y}kW)33sz)croG;KFPnXj>7 zl{%DF=a_c0{ImAM*50>Gi5=ar25L)s+$3-7qeXN$F-1)MAX5Nw?|lejBi3BO9Bug| zHpy@k+fH4tbnAxv@ut5x@iz8>D5Mk=HPe0v{$#J9bI+k~wO@KTLA+y33&;tG_5Ip5 zTin*dP;V!G%P|F;=>0K+Z^*mbRnPX0xF`xZ?;!9R|C$4W07T?eN!~rG(@I?UT&3cm zORm4ZPW00_-5ZPqK9j`5Hp1ri2(9#>s?RWohCfnq-nL3L4P;5tQ^49V$w&3Y8_^R_ zkutIrRc`TY!*ej8t0LKagoF2>Rwn&)-BKNW>J#cGh7$+ruKKVNbFO40^4_{|M$j0Y zf!TC|mR~23lKFEtW?C|<$-C5WT(gfc5}X*E1WSG^{8;v5iQ|5({`X_~2pHMsk_79E z`LcZv_&!-yzEB6HQd`mpXycPiF*dX)ITEBPZIs640V;#5XUq~9mXInzy1&?9Ja}Hml%fE&v%T`qBI0^zb+)3$qO;2$Io<*3-lwz=|N6 z7ZIVO+C(<@J=-U^YeHH%LvW{~P11aJMD`^iqPuL|_0tZfeeK2h9@E|{8oEnt(t|2^(&drv7rSA9CAapyS zoVwCJvL7wo2}Bwp59?!X z@!~pz@_^+%O#ALLlV`^=AXMz`f3ZMKCpIE3*ZvWiA`%6QXaf(JyEAu?!4K0tiCyOE2%DFD+lavJvt(>%XHNsZS8(nIK!ALC3apOmCbPc;w1GQH6Tc+h>+2yK z>nTSA_Wrgoy({~Sv&-T~HzTYGDoTMi@BIuV&1LIj=M|@35EJ*vG?%$q>BkvnPR?b5 z|7Z?P5Yr@z78z|sk{PLQTYZYb$1`7j#KRA_Tj~4Wi=DNy2q@0)zQwQN32e-~n%rWh znQkK@`k#!c%hH#)jqm+OCX>xi&a^D#Drl7doiW%;H?q(#CL(C^=LwdQWqtDz zoiC})j$Jr28+{u>i*j$Z|ADaJ>;vKW2DP_?Fdms1vyXu&b{QM!EaCQ1P}n{oGTX9S3q1v?{4o;=@wb+eXTd1k4P{ ztoGbdkBn)HH=B486p-_vv2I_ruex7Gu4*+Q`nLs2U+|%H{`x}LaSOX65Y4D$&Kh!1#vlF zN}la`QooUgkfkPTmJ1%w>^?d;?n}a{f<+t^I zm~A>G;wbl=2y*lU>k-O5Bjtv=QR?ZoUaC^Aa8QIIQ)TAM%}BsrUmOo9BgG=Jn`Hx* zGgO;|f8cSl#FTdXLxxL;`Nh$2f%ZdF1bHDEUTRfNDV6Zr7@ipLdfPh{j4I-GFeZb_ zmS${tcQ#SnJ*`bnZ#l&GMzWEq)~9kDG-1iVh zLf}WQtuq<8*;yzi{?c-;`8drX?=hbUj-D<&T3bD8sPpC<{bZyyrt$P_!plnLEhwjl z$~qYE{BS!Rm^$yLE?>YEg#cI7v4Ud!(v{W}dhW9=`n&W5%Y9Ad?A%ocnS1X_gN|%` zB*|Q>v24v^>%^F^X!f1W6lyb}_#tr!%ipD& zDp_=2Q0;i#0lS(2Bn9SSnyOUF8*V`{M*Z|$Xl9{_eAP09M4I`Gt@X?whsm=TgGG@`%!aP9%gO0JhI^ea`e-A- zZ_13!-!FajOJcW=44ar9ZqeY*GY^32`=TD?kO*JH=>Z~`0tyqwhW^-C5gB!hxryRI zwCLTJd&kbsoRzosbLRKKCIhudS%2=E^25YRc57w3;UXIEwF`ey859-#p@c#=pjSRPIiB5&7R&mu zw*0jWxt&j16#ql@VlFYQ)}FuvIXM6D=o<>_$Bd`bVj-RzZZSqQUN-)$b-#8qB|U;q zC++*@En{---wlS^4^S`&sWyEQT|(S%%b#A|;FLCeTJSYK6$Y~sNZD}u9K8|= z+vjZG2r#j3Xy+=0@Rh`nt)l7cr77l4GQO3Xwi5HQ>8)QY?}9w~8Jh^gblpqqM$}{$ zu4O`T(n9<8>P9I6aDdRFLC;TSH7*f{ySH*F>GXSJxt`rzPmN7zDPNYuyDC`KP%Ko9 z_W1c4ndMu44*)`6m!3_~sO4}&e^s@N;3l6J|Di{2G!jE7B7e88Vlq-b{Cezp$&a0lQj0ohlc-W*LB zj-)Z#z0)I{rvrR3Sj|-DpX#shQG80py<=;;r@RW7C@rX{Bs_JH8a`OZj0%*=W}A13 zHT>_+j-$HbXhye&=PQ3$h^NABX$b8c4pSgPD2wft9gzuh{+Lt5pptB7uj=6YG<5?{6X}*^73v0#-mCrj(;!|^e$RKYkgPXF1Rpb0_Ne%7 zBEg+;r$|&D%umq47~U&nq|x9o@Gs}VC>g#tGaxNxb4KEJ$CU8bg-nT)u z@oMC?f7!E@%6mN-#t`<1VE)UH&WPGApbY$fkPJV|l5kRT387NmxyDhYgO(HWHc`x5EHf_~H{&S;V zhU~TtoQv~wRW4$Yq32E=}5 z!tPoc;v%H+?-`+Ba)nx1f@B@o<@XP8^IEQ~Fj)2B?6CtS4yC2APxVXmr9XiS=}0!VdJ6CTOFu2 z8z0RzR#Y`cJg>eGePh>x_Hi;+!qFe=|2U9IGCwD_{1@B%@dqQ*8;A#s6=xJsq$J8! zX^B~}YJ^0Q+J{!%x0#TU(pZXptSE30Q<*WS%?+g=EYVG!RzDKR=#F^)NR&XS@ zLXlj|8##o?*=q}baMp_?q_6s{4gDS4WHDTP^QE{d?i)ez^KaRPMIX%r*PH0yYySWZ zJR`_$gtl;TD^}`N&T{Q>2Q-vpZ%a2peCX^{Rem2@3U`If_cUf_nZ)qL zMaCNh|Wm25hOuP8AueUrIGYXBQH5wn)E3)wyw^ANiO!4NH(Gfewzwu-$Ym?(v zb!i+4HnW`0VbamsX|qoJESO)7a|!`Uua1)Btk!}P%Z(`{wt>~o3U;{$Q} z(m)kFk~<<(mF*TQEO5kJNiaxdSEFrBJMyx1PnmXlkDGj`WbtGhs5wPx+f2u#{r;%V z&d5s%0t3qwXW#`wKA+rZ z8cquL79t%2d}+9=l=c! zP}}2F%aqwzsP46U;5JJmqB~ad|M*XmCE{>sPc=?=M2Xc~2ccd~_8YcVtFI^}pokXZ z6Sx4YM`cgyr+@)(>Kg)up2qJkmXZsP97a43DO68|TcfsFPG;Sf_}mWA{Ud~*xTSiH zA?JyQujS0W3#BY{TbdngJfh6Qu#y3~NX!HMNUeSvvIzR>G0qNS^;x#dmV`IM?dn6JtCOq|7`l#GB zH*f0>Uz|WPh(y-%u&l5oO`DB&D%fEQw5h&+&4oAbJbECsO_OtvdF%stkUTxZ+`wCS zFmK&~01N%GR8`0nO(HqbWGl&P#hT(yiWVapb&oqKte3dFW)o_JY#c=Y3ZC~}TT{ZF z5(DUX?u!>C)zX}EQ@WwaH)xA0Kfscp!kQak)&juZ#xsd_yc79)EK#=ufJHe*3R zR7C^z^FsR8=>d3Cb!EJQew49SnpHFjVkS6gwNrZU?-09YZIo5RZ8;=;{u8?Ep36004v=|XTN$lIfK2s??!0e#D%(mm3)Hp7H$2$aJifsnwEQw-cCwOXZ*=w#You^Z1w8(l&t}CP4DJYG(zi=stP%y`v|?-u5t{59AcM+P7}~_U@x+uBJ^#gv(Vo7JyAkyk ziBYz9QySYBBeWZ$hpWPlJiCNuiyQYN!xI>Y%|qHt_ai5vR-Dm>In1a!X)ur7Me9LH zDYJ@u95MPTS)NdR`Nqqd@?b?Bw6s_gO}@YJ>*S+62TUC2SLn&4jU(~5PebqiO?yJ$ zgYh*+ji6e8v5o%iRN2hmh-BMNx<9U~T7#%7>!~9CKKR-;=d7lJV{j`?@e5w>m!O1T z>kDP^>gH`==3{E(gB)uT74@@FztYo#70^b%pyj2G@B2#5G%yC}RK&F;Hbur;~@}@3+1S5kB|_ zsO`96KxCeSO%`A#%F9tAxdP@LJP`0gy8$m$%YZEiRhiOY8IlnPwLwvw0n;Q+^JY2< znVJyaaqvYe0I_)4hB$DSoRKME9z|+u)Fw>(OgZ?;g?wCov423gtm`W~SP-LV%wKyN zm?Ch?i;9r=InIuvQR=g7wP^bsF$iT=l}S1GSLT$CvB-Q7Pui(5V`9V{-bt zC>EC@OOd=W9`6bCdUFn_U6Ir=A?`)GkCay>i=0Obv`O=YoQ$~y_zW#CWd;`w$mb+L zi@1KNlht0^wD#S7 zohXOwp=Vhd2QM^FNE!gDnwI)0R-MDh)4@d$|BO$FvOIbmWj}qd zCvh~xgA>(0;*B)`>9h;-N2#-PQCB1U7T^||)nF^pToT8bPmq5q+nsVK{(^}c+qg~| zh9r=Cw>N*>bP%fj+pRcyL+Ers^ZoHP3Tz11xytsN8x<9$PlFf7+8soGfB^TNXUyh( z^X4MD2cBex1M8r)?HR+$Q(p{5SoCA|*e9+(yVn4J^KMp_%jqm`~xtug3hn+T0{I!%!)tljZ4^*?sIKsEyMUvx>0AG;qTQ^i_d8x?DPbV_%)x zp?0M4GCpzYvI_H4Gu&gUEdTF1!saCI|Zr)=d@(b$PQ0bnU;!Pi+DrPp0o6QR+D{@g@q2kI%hujtk z7u8{98zF zdnfCsUT$Rwz_~6N{S0pl zu~$i8A7)PJFab={k!n!S9Mm-lFLhsDoz>GOlI~`*(IXTzU#DSAZUxvFH?2i{lqCL#@(}!iEs1hfpr{M>6y^Ne3^9azI0>9Z|9W^$jC?4NlH;cC+`fengm;wraWs0-(gfoQ!54m-xA>6ja zuJ=P%+zM7GkFr&LeucsI^Nc6R;dLH7e}y>e`0y649Sk zsm5mlY2W&es}9=WdX+)Ne-*%{wDai3CAk|Et(k#@Wynw#bsR&6*){YVX(RQ!116Oh zRt{>8Bw}c8+GLKyb}{ep^T&ORRs1}G)tta;&Tc;a2oIpX;MONw1r0znuy{~SWr z$V21sV?rWEX!8#UFJG796Uv*W;3dR=TYde^(#=k7w#B&XN6QR3c42pvRA*zi zuyW7?cB9JJJ$zHhI8$Fsiy_J|=j~mMb{vJR7LV<<&*QxcL=->Z$uZeYX8tdDJ<<**8In~}6zlUyo~;OW^w4z{982VJ3k zo3iB_n-5ixSbx3OA}~>lh|1?Ms-C$KRw5lmcbiZjF4-8(lYW_3bQ+`cWBHwL2&W|F zfhhx{v9B7V5T2duYENL| z`=Mi!cBIf~1HUUBM_#gpRev?w8D~Ic$(7GqS(1WMXap12&P~V_D-8RH@0i_pIC9QY zXE9axZ=4p@(e-5su-ft;<7td92m(oI93cuEw`H`<{-}NP{$AwI!sjbRVXGCU3g!zD zZ{1Jw!NlgX=??rgllsz+q>Yk!0kcyt6k@_1pzk9pb&DS_sC6)q!&&6oUQro$1-u+6 z1{Jk?(SBe#9~?+0BW>VX$a%hzbZ!?E+!@PHCmlpf65Ax%)^6O8`v>5hnN{*$<2Ot( zdpDTERN4L?6H=^r@_qa*keVH*xq!}Q*_$GGTDU~VqC4OrmM^C<{FIlSSm!yDje#t3H# zav;N~a1h)IioD#Y>|>A5eOp6J`7(3$lz$&bZS}Su~%5 z!r|KT`5{daZTZL#Sq%sOz$Fp#o7EGy*Ke(TjVE_$Oi(XPL+7-4xrcf+TMuij13w>? zSN~)rf6+&Ht^arJ9dc+N(aLj5LOg>of)QJ6R5?8`%|Gyv6}x|cf+^awe*i9UXfs$7 z8Hntvhh0#D{=?su~7fHCDMC1Yc)aZ01W@4vjSO{;IEAy z*D3sbTofPfMqE)UJ6!(&uj@^u=mIwD1>XD zLaJzO4{69W^_ye+je$mxJIA$3)xKA{f(4>Onf;TkBmocU8#w#tjF zR|^el{UoLdf?dB$jj7W4`_w=BtK zE00cW&O2!>n0+`Sjun{iyK69SEk4#sRIqJ=oY9`&W0`}-us?LpCJr3tB=kKnNXPA zDe`Zp&R65YbI}g&xa{P|4_RMVS$LI>foB=GT$lVaMg8%dzK}mx{x|%l9S(NcSR2f_ zm^>9Ag|c0;6(e3nN-$yGpa7*JXhBv!Ar>&&=tsa#x)kaNgokk?Me%yz? zbJ`-%i!`dd{Re20S;$GtxXy7lq?t_5M!kKB_pZ~DDq-P-lxh=7LvQ3`l|v3tV6t(? zrPI8BxVYEn_x|1XM^9%PHk*vIPC);eTECS1L+mKQ<=*%uamC2(k(;Iqw9B=;(jgLZmU)V*iWL_{1_cBaJ^LL{jw8{yk#+4u-!~=kOAp-X_>D(JK;4x^p3knUc3l}&2etOC zFM0<(OGC~JZ%nrV1q?CYTj2st@@uv+F9pz9`SQIhh%3jQ@CEq*7PZXZugc$-6+US9g+kA_gO-9$?wDei0~qD<-`CPW zAI#|=PMj+#J2aB?6@B5g9}1LfAWq0A`+i`$CAk!Z*pB6XKnI7?6mJ87WWDYFsRZf7^=pKMwT^8L()|nTsso?REwKz1x+beb;@BqN ztPi(%qQ2w5K>*o9W`KP`oxK>);19bn#}S$ReB}%7g+-oDV#tqY-R?W(my(yHL)u%=h)8UK02et_WG=`Zy5awV_hX4NfM=s!Tl=!96-45LI0 zqmt(Ny`@qGniIzy!u)HE8n}G?1o+K0R6UPGT)$~BAy1xrXZgJ^qER9WNNZ(zmjNYx@C3PwE9PVvhT>IuE zT}jHETreuyUqCsYb1B_2%NQazdunx&sBE2c$kBIH5O%_u=C?*@l4diPolA+TR=K+? z!G00W)YBR+YpBX;Qm6wq2BLtU(@qiaZ;t_ zp8x8?K$d7EAxeA5JO3py`7Pt*$-Pe+nzvq@Bm#972ZI{Vs3nar7|6@jllZ=euY2=tk%B-Swdf(S*-D%;CFxhJo^YYVSH zp!eJ%GTR|{l75#W2xY`&(`%OaACkW{*>dx5Ifkht|^+h!@7aNve=uu|~L?>h`s2U|)8 z12zhzUr*X}@&~N84RcC_XBit!VWT5rABFdoqQ>i?Uw*{tiJ38743CRTHB_$6*gUOe zcyeT%PRdg27l~LAnf$rMqW^=hIZsN~Dysb4s?(!0mF@51ydMhf&mNBPV>CIftwAeU z&k8@(1`4%=s0BrpGO7DaPkjf9MR>UlN-}9p2JE&EcSHlEEIX54>J$JpZ```js7R%) z>`9(hlYhJ0(PS$Ny^<&rB5M)wezW=NBo;r+)sO3RCyYkbxab+uj}XB}tTd%Yh1zX{ z2494`b5-Q(wb?7sB-lk}mTJ(Txdym_LAHo&h~oxHft@YCUdfm4DPt^?1G=3xWC_Ir z+t%nI5&(sw3F(%^Kww~ICf|lY@t3*j?$QeNtOjdm*e7CXWr1t!E~CiFrAWcSwMOa7 z$q-3(w8jt3<%hOeqLZfQn7n5S{pl#$ju$xWg{$=7ov)q;wcY>Ke0u-=-km+%d3G3Q zcC(E3Y<}G4j^6@_yZssfYWMsH=FrQZyGRqDkN{?R z_`#N*H#D}4B_KAY#GkS6d@h)N$tN>E-rBp`XDtD}V^i5Ita>DPj_li&c4ErQ`r9hO z1aE#4*zKM6BQr06O(+!WU17t&x*R4O1#^AlcR9}RI$mcOv<7yj5KCX*SNletF7xm# zdnKCbDf*AqgqL$tfKD>+`-nonL8?}@3sD$K^0ktliNNaTJe`LH?NY4((xG~ zRQ~`1OS@`jDp^16>tmi;z*>K=p3V`Px5;W(se$N_lcyn_5TnnQMpn*oqDx-Kr$c6g z#Zru2T@WPJP{jwe{Fc+_VV=zamB1KT?i#n9p51T!l!B{E$-K%9GD>|2$;AiR6DXCTfiX$)JT%*YB6Bj zzD_x29^175C2%3`ipx-JpE?t@u}<>ep@iBC@shV;LGqPX*P<^<)~qpa{hxsTZb$Q;edoj)TsJu_#$URHQYbtt ztY-)@#oQ=-*=pYDdbt6&`;Cn^8q;JQU>e>tF$4$+S$ zn?hhr*VHc}oJ8HL@qM3Eca?4HWmoc3Ei$Ioal?H5N1+~X3f`+Wl-ihfg6c)$ER;4y*2yvEUOwgBnoM^BM|FsbPj+2%bv=<*L(gttE&2%?Lr8Go#M2FUitd4#12KyPH=v| zNH&@mze>G2C{Elfzp~++6v~_HO14IwR43a~=<|h5z{+p~z{8i3m~a8-4lq5jKrZ0F z`u8qnB{v^FeDs{e=bWjp#r?z!T&>!dJUBPYMt)u|>o+PnaFHt$955cW)AsY%(se0? zOkRe6E|6RiRh_{8&G_q#wbilFVJoOp+P?VZGr`VtS-#-<^=F%3yQ=(Y#l&~Py~K&j z*{h$3@Poe8jUrSIpfN}vLJeV|pVKQ!#W0df z+biAZPygIi&zZq1;LQ9ha%CztSN`!q4I`u5N@hRN7lTO`+?UxB(|=xGh6?5WZGi~x zgN@z~+4YSp6xi|C;YtERJ738f%j=B&Ve6Z%Rpq3(rD5~90OnfrN=%CU5ODc%f3h5s ze`6$Z4TBCuO)G+&}qvbXeSFE288%(KHn%N zuLQYdV#ooS0B%Nh!UFS9^Wam2TLT{+;-Q4WpB)ZUAN>WY7JXn2<{o|O?MaCnU93|J z%PpULLk7M&6evc*9in|k-vi=j|4hE*oE{&gR1E-}cS~RBUbT+*S}lauAHtc+i+WP3 zRL59>`-p41NM{oNHti)V;c!ql1OO!&nLDU{*m!I*+nM!85&gr(p=s!)nZfTwcR6W5 z=l-p06du z?2THVe1|=|q8tv-*G;^0>6*OYWf*$#9@kaAG=NRA{Ltt>Yc|ho(V44a?^v&>)FS#7 z+7tL5rX3>1!=PFr`g4bxbB}#zE1JELFC02cHuo|U{Ww;Rjsgyrfn?DVqC-q0egi}{ zXNN<4|Fe-RWB~)@RYRP;^)-~fVY{*>Oz9Xu#1-J8pM&?c^G}yRpu!`qAZ!!H^&=kC z9fER~&Y#(56WM|e=t+p#3&VmlHj*YqvoD=xZu8@I%%;W9;19#k z)G7$hf3mF(z8E>ZeL*KnZX>{ji1ldGpVkad6- z3l81NiaS^h9Ya!VDis3PLfheN4PP&JFRx+fuXCq^mjfE7oR`a4r$JL{5Ru98YxU^g z*IClua?w~Q2GBM94HM(yy$M z78jwl{l?vxwTvY1ALW~rI zoX<0fjJPia2GLo&k8g8(hBDabkVe$kTp9IJ^e3?*oAPYKc^Pxp*L9zOLoIbd*9K99 zK+QWpV?Lt`uRh&JDt@ZIGv?U-^v649l1;!!!&k9ULO{UFmNX#wZ{dcux z$S>@XiB-nTSS@y_FYAw-Pq_WH-@-j_ zstb@#9!x(?FdKd>Mf>tRm#bZeiM#k(EG!g~ZtaIb4wx0*5Gsi-t17|3e@aWVX0jCm z8-ENJa6Q-Q`8zORcwBU#D=G#4X$O0)cGfNH>>PSwSYactt0YBrlxlkq4Sa8{u5vvS zP*(7h7_{8t|M;^-jl=%(N@Lx{3#F9SPcFw7WM2FJl#Ku^-dV zetI6z^B;tLXbtI!wT>v**cN^dSQ>rfzHO(DsJiWQ zxsK6i2yO_FGC?77K2Ut!w&DAV+?Cn$=Dx5*${C1zhJT99NspMLaS}@%ugr(btM0;E zL$rhS6(@JMWqU~N$5%WH9j2c~rf^6x=LbT^QbvVnGq&PR7b6<4mtA$L5&aL)Lhfyr2LB?RKUZSJ{W&<`*Df^zh8;B04-#vhfi9bA3*=x%`_%(S;` z`vm+0Omq!o>pbqNzWZC0(3bg&@V)mn45^lETH{~ZA z9L;y_l>PYgh8o6hb}Num#L-Py?FSvmKS4yv-Ot{m4t_0nY)Ez zCz}y}uU>AOk`hX0HA#I1${TAJ&^tBF8V_oZSIGspA=KiGAyzC6wSyhK<`<`vN)ddV z@>W{d@|5HhvzmxGkfdFwot?{+T=n|#7D6f`VMsmzaj1VNRbBZEpRr&2YbGGc{QlEfGwT_wRp!Ubl#r#sp|~_gR;V5Ki<&< zW9#J6h}r13^4Hrt_5YsEx~-M-8cM0wiw79!NDMgFBTML9had&r$yBBM5cQ4*x zp`k#51TXR&-simM+}C`7`=8mfX3yHcmDDd-NYFJ-$M)i> z{_Xh$D`SGzCHd?cW?@$>Iea`voU@uw5D1-LZ50{cQ@ zq!Z)v@=hTNgnmU5gFKZ(lq2%3EB;HCfsEvbOudk2Dxm9j!#^Jxp9=fx1$z8Yz~hH`O7KQ0`3t2JW;9ffawe>GeG!KX07 z-S@|<3FvX;Tv}RsL2iw%HZCn+ERPq643%vd4cJ@EI$CL9N_S;a66^l@qXF!F6Ln}I zsm0BnWJ-9oGA%W$=y7*`@NV+93Zs1wKeSmEpY+C@KUr_-baW`$_mHo&+Bh+)+%We2BMgD2O#SkI|+ge!R*>6I-=kQV*2*^nQP;D^z-?toCDdVapMFUY+TVNmP{`6d=6B zc(?%3bBuCQ?je=WlO7OiP0tL#F8KXz>Io<^v+!z)5#uiVLbSA^O-Dz|uHba?vuHrG z8B4FnEil*)#0?dtmJ&tUseXX>g&LU;fnImZEG8V`$h>vqH$)^bxhjOop!4i(r3VjH zWw~vKCr3o=Ei_KyTtP2PbSQN#i|ERF9U{>vn#F0E*JawJm;wF5q3B?+!#{x6JhpBi zmG#F>Io)LQR%g2PST-+0_5{& z($b$rHR8P&loUwLE;8E$q?St;ug83QHNRc-Ne5|fDz-NTAd|92*~LVQkAypebC@e| z+AO|^g@1ve_IqNX-Me;WRAg22w{>qUXjHR3z1WvF6#O{oMSQP=01|oz$RD|eV+FUX zQLPadGgnvad`{a1NJGhP+;anQ{N^@LK;kQ@Q)|U2VXse3rY$7dhnCd10Zk~Yj^lBLJkP69H=P_uyrg=6)b$h|#6t=-UQ)rA-aaBnEAEbywYS}OW zeC0xTo(nzsQ5I$~(UYBFW|2+!mDrGl-KdoBLI4lPthb5YS5G#hOizAz9UNFVtZ^lA zl?~_gcn_O8ZTN?^vov_X_;URo*{#T0jut*c0O3NpxuYpToh>awp5GTko%#(!u^r0X zQ!uoRF9l$yPsNGj;c}jq1dd?RWzci-?PAzW(p{w^)A}ul>HQ9du-9hU^ebtm=_uUk zilM%TmQR){OYe=%tY*WInE@8v-Jghe+K#hI7aF`LDhQ&w-&;Otl5sO>sos9^QJvR??#Ct{Y2i!!yQM%5!N+URyYBEHU z+MMT!4g>u2AA|MAQtTD;gRt=z_~g*V#TU}BomEScP@71ae6Bt|URCfoX2R=;&|nQ+ z#q24@?@$*)wCJ544t=w@=hlRa-#-kU>T|CcB1zBTLLP&)LcOk{gaw|VuB7Ky?CiXl zfii8F4E2@olRsREOI%Ra*gE!R>fy>6Bwj^b&CC}g=^C(xYP84*Db>C9dOt(t42X5a(f-rdA$)<9U5f#xj78Kx9;YA=d&&* z`}^m8-vd5QLOiF8V5;oy+i!@6X~MHu2tJajrVFc{G;MI+W>;8Cs5E!%Xf`%(qMZU zgIx${3gCVypRR#KB3H4F-Ycw@zonex<+FU#ogA;f+2B+t?PBp#1W;C-bZEfnJ$*M! z1)UM01YI69?7(OEJ#hpJ^MK@-vPBBfJ7dnZX?O<~nmI%K^%tz-go_qjQ2h9V@Z{iJ zqFu&$^n{3>={?o(0(!w z%~MxG1~oVz3p?nc&xW{C{C@JL@HS`ybLtCCQ5H!IL=Z>s3*cJr>j{E-FjUf@x!D*f zPm8DxygQ^L0bR7O84xCraz-f3w}1QcJcVU;srvdp8$e9^)U9ZtJ_v*82Y6YEDhX7Z z8<}!=FCXb!X@(V^LLd4Cvy(mrJ$Y1M4v2|HDioY7m%jM2SPEY)6ghi!f29rhc>%vm zQ!}{THHoOJWydf^ge?HT)|WM#Lx(dp-BKjUB*GQ=v+NSp$sWfH)hY1Yp~JeH(4*g? zhcYPg0T;;ePkt~;YzvV3p+qe~i3A{A#?g}Uy(jVPec6+EKvJ?L5$<5$@i(Kvn;(L+ zC{5DE{10SjcWKC!kVG_YMxo&vnW55*tUv17x1|ThT9yznLwB0KVZR>I7bp)(^_VPZLf1@MYv;(uQK? zP@*Jf1kvRrp3hm-#OU#lD5%Td{0EhQInr7g?)dp#J1j9u!twsZ~x;)Z#FKDjl3-z6yuO^F@XCkDDqOR! zSIm3HB$xFN-TM%t+_P9rFJYkTT7J>YW?uJ8Of1wFv4-;KknJHKmZp3am-XSH=3&kN zSq#6Wm7R8^%gl-!a<~moZSjl$kg2;=k%81!J9@MK43e|FR%PtCI#pLAcJ9!y|}(5OBa?&C7@ z$kIjOJ!QXCNk|t{3od?@+ckZ|fHSWk?96g}?4;__L9hPnPeZPglW_*i z0XuhmR8q+@?BhIydg#rKBo--H1!?NL7D=08mTXwGU2{9uB&7>KBJy?%@z79iu2KzU zk!!>m2jOXC)ptdK9B?uFbIC)-nwdl-bcR+x+=aI;^KKFX$f}1ZK6bdt{|2Q3tO~Vc z@*?3`iDN@N2ofdGrFSl) z<*ddh0%DeK~p#tAhCD_RkrPtUW=kE(VjUdMb0j87 zaX>QcAFkk_-k96aF`J#d@vUXma;bu zxGPCtkib>UJY0JpmoWeL>QLFBymRl$-&qXq$Ke}n6=_N9!u1LI8x|D^-piB@T>5?y z(=em_aRH}5h9bqcMiH=`>qAZ@RQspA5j_0nmH7#GCT6#G9&Ltppq*DGhG3LpAK zQs_Qqe|Uox=`Wf00FOr)`5qQ5i(^Cvk)^c~n#638#$*Sp1kq!Vi>N zG_F;09+!_00JY9?ES4|ki?@Uu|SyIV3Xt_0}<5gmy?@rh|gM< z7=IVi=RATs|LV8Zp*yAo*gw!5ONv}4u?bY~$jj{UGI`d?xXHT=TwHu^4QHw|AYaFOTCFFhkpw?(>2e#K#w-q^>wR zcwDgQ%c)$RMDppoibbC2ff2Y&El~J^eD`)%X??Xwig#lYb)5PGFkvWubaIP(Sf-N8 z!tX9W=>|@dT7s`?WORbhH3Ddv=U(T4OnySWUPGpQu-^}Sp>*@XPeKkyn_5FwhT6i5 zO*p5d4i+Z(a109S0|Ry>D5aV_aWJ{V)UIaI$>`qwleXjP&Pbxjx)@56R{y>u_VL~9 z_k7xz{lO{Nb_Ue)F1%DHh0krRvn^)!)yB*`S%QDRWW)&aWgovHBBx}p5A z1F!&r&rQV;H<55HjB0%TgxOC5<#X1x^t~w}Djsh%v_ic-l$Bncj$-jJ*yofIjJE`I z&4tz!DJoy(cS}HqNUmALq{s~aL3)VRLk&I5IWb$z;Z!n`V9kq{~AI3ufC#VT{6_z+!`%a)9wI zPxW|^uP%rJxct51aByWI;M7Lgyc!k(^T-;dw3OD}!Ka>ieEB~+X$Q7WUXiA};)gIk z6dBKU7W{w|P9HHb7S;Dkv?lBGm8g+@-l@E4us&9t_$s0usfcmr!iKOzbRwtdPtdjF zd8U>7+_;RP;ko%=X2&OlJ7IUpbdLA5_-sdQk5D8(Rt$2Q(W_O<@Ahb)@{Jq1+|Wk zkm-NQ0T}XiWTDUHaM&2+6+Ql#=fM}iBa~_BwBmusQAr=`*-g9|?!qnWy{dYbLMcu* zMBlsr!FNFE`(^dmpyVq;oq_1T{B+b;j{5(8uLyM%RAMwj5>f_q{{NEGQIL^QkOYWq z_&iCPc@p z3GLosS))(Gll!JxIAf%hQ_IwYMaI8l*Z3zmmQh75mXgVPXoJn@C{}{$ZB(5v-7H)5 zAjwBLln$~6)6^_^CS00?0+bCt{B^}Yay-iV;V8g`=!38ym$}X!!2FMKp zv3TCexq@Ycp~P#V>l3z$=Fw}@tTUhZ@QZkTDO=lYh~KC)VS!kbu|wt5TLTb4>`@Li z!BU8APIV%ZesU+phWU;il~lnQpC=S?4um2CwWH-pr07@^uQ<<=^3%kq!a9b;fF$!fAWbC6?6K11a~r-QccWZ*+d| zC|8OmvFj@NXd^i-E8kv|Zz0(!lk}9$DS>WEhRq8S?_%2hk1F9{!_y&q^;B) z;IUBnI=a}cbT~-eTrbTdMYEz(3B`WGrAh9oJ-ypC6?zi}Yo(N+F@IHh;{*KotmMsEsia78t+rjR|w*ve-(q z@w^-e*{-(cU_0pmj>k+8G02Zcd6{Sv$G{_iwd3BV-T0dEE?-)9rYNwZDNo!zrX=6U zp9mnpj2Jm+5)M`RK+M%)i8iB5*}3iZPY)+yoDr9yNAlREIr@G=< zy)5#R<%Jth8h>eQe}H^L8#sP>I2*{Gqw~=>G34uhGQ(l~LG`dY-?HX=7lo6G_X)vb zyNmR~72c}Ej{<}?>RcNegP7{TJ$&f(F-@>cLCW8nG^{8Z_s(ti&PSBC3ORYt`=;eM zmhA6;s`6c%Uf1{2rtk45p(rdr)ct;__b*#wyQE^Fak)0tGLRAlH;rn3`{B)!=^mQ? zXY}h|4jBq(zem|6mYivIzZ}o~R+3}Bxp@?P*GmBNhl!T5U{W=BC^4evY4Aj4Oeqz;Lh{g`CkrW+Gh zJ-Es7qZhlh!zj&*Q?O;DvpX9xc40SOjAH`r3j4`enjQJ0m1--I3OK}KdO7imo}-9l zGZ-Q@!t*2-&gT>sXqwj1WFsN*x7mE;MM&0DAH`jsWo6BfY^L<(j_26|Rs|BZz;5SD zwoEM#Jgy@Lf5}f>!RPtnl+SOjwRxDpOH4f^!?vzyWje07SgVQMgRO4*Gj`HxsukQ- zDfu1gO64<5J!u3s$F_>h^$ArH(C35#;<LJZp|I$ivkcn3t+|jvox?XW!~FV8(d;<91SX|8VkW|<_VljE zch^b6&R$Cu_wFRoAM}n`$xjx1n0NB3w;ggionPjFo0X5kuQY^`PUlBEklSS;Fony? zj#561NJNw+73^02a!G_b=9#){M#Q#Q^UKKV$SrPoNJ?@?5bz{~yb(vOxUd#})}cH0 zOt@$QRk#l6>U?(@OxA8LxaPE+!%39hZJlgNps8>FS!_;oMYujqhI03g{t*BTnfvfp z$f$Ep7vWI;^4sN0hKTG`#_gQ)h(kr7dfJy0mBezbNUv_Dd;3e-N(FW}3lsuw(iUhP zodTzMY;M^~mj)3oc`QcqO?G_X%QG((z|y*&V*Jdz7%4KsnPD!dnI@8{eEF2wqeQO= ze{tyS&O}7s6Q(Fd3;#)CjQhI=iSGF&i#DDT@)@YGr>Ky#{&d!t@9v({2AS126G-xH z(_{jZwX>e3j$3k%j|&T=I7Er)mefCN{G=+EG`Lb0>S}ZH`rJ*yX8g4=#VjV>wcYT$ z&gKJdWM0E}k(^sW*UTvpePiwlulpYoFq?~Ehf=Ybf+x=l-cN7Hg5%LfCAGbD?ep8) z#*Emey!4;gF5uwuXc1t>KEX@mmA$)@;Sj|#;~%1PwxZQ{Zu%!(+7yu->y=Y33)$&k znkn|fV5s$59|fJ9v<4)24Ym<1FQ&xm{~Ui;!=EDaZxf7^zCkN`lpJ3wE#~O$94U0u zvp>eS?v3Z$uKH7bd);)t6UXzi2RA^s+@aSCYD7kvDVVqvxDsP(q-X| z7c;2PZsMz-hO8!y4>o&=-(gju_%xCLXfg&koaa{UzuF5qFU6qvZRghUff-;5P1Tc|X=+PRjvOM%?2N zAjrm6JET7o`1Z-f{K4`%D_WM6F&L|{*$-IO{|FCi%s#!aeJR4S{Co4eKk9nz=gVZL z*?`33`ZeAchmf1S{~&F{Zw~)~AFKlpJztL~#{Vz9|DX2$ukRG zGoUlwzPd`!-(m$sRwm1L&fkMU68lRJc=zUWmUAK5e7wLK0 zc{AooaaKMw>kM?D*wG&Gpn?S2o0%Ou%q^h;`{H5moM4&3R*%6oGx4Kj#}lfHOh@r& z2)xh-lLPj)NxoqFS2`b$4bq4*Rac80ZN0-S2wx_ib@eAVz45U3KX>^o)>cvqDh!RI zDv$BBRz(G~q(7gIdA2ocP$J0YIBnjT{ps9ZlTt$=Z{&6KmSgPm4M{K(jTy_Sbi<_(4^9l_j#>J14ZimKeckp8Oh3gOY#b8$oHN}lA zPQQkE>roLbq}S#Ckg;g50((My_NBmxV3yxPz9R&x3Vr;!}Sl1S}jP49m37{PqMmSt!Te2 zJWS9a4i4}X!fTO1>*_4&MJ^%%SS_+B*j}f^%p4<$xT`DV$YW2t1y_lH{Dae!T6#%UPq72)^dAF)1($>6QLhX`>>)Ds96ya9kXhy`-YYQZTt*jRpP`Knd|-@|#uT zVw8=PN7wvj&|6#9I3bix0k`o^SJ!RUlpxiPcf5a-AZv+iGatr6# zw$F%IET|))-pdG7RMg1}Kl}V{tFtZfZ&Le8f{;ySi>=+yScUHjt}L#!vHJD)T#6Z; zEG#YRUgKA`^=?_#86U^9HbpbXMW@JYgtGb0;>g<&%GQEpQv@uo=^}P&RjQHGfp)Ix zya<&xprnC1RsZldDOaoV7KvdHOFGl!1BC-0kI@v_PJ-V?J)U`|vW(LGmTOeF_F_hr zC5m|*yp7~2zC|@U7B^B9YVQ*nn_Dr(q-6_Ek~GtDw@V)@)=&sg7SiX|TGeQ9OqbEx zPYjaIXR+j2X=qos7WnKK9XEiMLnCAc3(@@{bN1MNF1iz< z3jLp@B12W6RP5x6j-6DmmHy#{Y%hUuS5ojcNKn_Vh|S-gDa`{iUwbJwz2IkXY76|_ zslMxp#xgM0YGDGHEPdGeM{jz@!nLQ_l7sEG@=eVXYZc7MY3Gwnhl^`>W%F@n>-u!? zUwu=;^azlEpcZ$(|67^C5Qjt{GrN;>T)Wxke~^ehZ>2Q?2cSQb&Flwe3ZC=4rN1Z3 zXnYY;D0F=tD``sXYx`V+=k9pqJALTTRgTWIErpknuI7yA9bn&#P%kbqxRIxpNV{|T z{u&t0`{$%Cee8jiPFaF!mo4VA6W@4>oim?)#l1ofmS(2n4~Ba?C8fAq+92%x%=$C| z+P@spjSCb)8YK zmpGr;Rz@a*RtV493XCIRWQ1h;myBA!KG_iKNW1XTev*%A+rRw%^p9gJ9-RhB0`dbs z-&9PLZ1O3>yX~HmlK-R|g;g*`*Da>58L;$hdQHkume35Fm+ARqKE09123)hZktkVm z5^1Cp<}SYQ^fd&>b9S_mPn8@hzd4AfIoF{AQxSpR5VVSnh)4<<`ip4m9QzzGHW5A% z3@+KS6;)}cgIgac*I67VMpTT8av>!{TIaWkS%RjzJKZ)vc;h)jssQ)ez}6GlHTB|F zKK)as%M587n09fcFNRB#&HAOxpAwrLr01`dLEy)>&DNrLMp(&-%loMB?V1IEdMyP{hm@KX5_nOzck>v z)?e|up?2<{eUb>tThPf|w^PwSh&z5ADUMJcQ)mYX;i1W=|FCbih$4Gg~B?D8vW*7mRa#%^|(juJwwqlQ@6rL z^5Y7t{z%>}`y;Y`Y{qQ%6}Qq{e#cLoc52$f)3$l^EZ~I6-q)2m%82XMF7vaE{M5b= zyk^Nt3CZx|?yz$?>s5Uu@l)VxNJ+y|_{9gM;HBp7sp#%x`@{5ZpZG`cKg!!6>Gd=$ zOX9K|4Mz4)TGh_QjVIzGADTPpW53nwTRka{w}deL+Pz*F-5)E7m4APIc7y$8`Rt}E ziA!2r7EJ9}_w$D~HRAW&hd?_co|TW0-6|mCaoI3n_-jNT*|k3c5)wW7&vN`l<01D2 u=Z_`vzx?BYvS5Pa-52tZ+F+r7=vTm~>w#yfi Date: Thu, 11 Jul 2024 13:35:41 +0100 Subject: [PATCH 36/87] .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 37/87] .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 38/87] .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 39/87] .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 40/87] .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 41/87] .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 42/87] .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 43/87] .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 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 44/87] .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 45/87] .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 46/87] .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 47/87] .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 48/87] .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 49/87] .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 8797fc93baca0a946f359f357f18923b9ef9de27 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 17 Jul 2024 11:06:06 -0700 Subject: [PATCH 50/87] 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 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 51/87] .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 52/87] .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 53/87] .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 54/87] .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 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 55/87] .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 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 56/87] .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 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 57/87] .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 21a905f37d59ac32f1d818dde3e8f30acf43836b Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 25 Jul 2024 09:20:58 -0700 Subject: [PATCH 58/87] 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 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 59/87] .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 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 60/87] .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 61/87] .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 62/87] .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 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 63/87] .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 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 64/87] 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 65/87] 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 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 66/87] .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 67/87] .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 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 68/87] .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 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 69/87] .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 z?9A-JHhwKZrU0-QNI(E62XZko1sepgu>B#aZV7S+IVn1U%s@^cI}`8@ENpDQ_7D(2 zMgZBF{&wY;=-+($?F&%U-qp?pzyg%9GaE11N>F*m!UuPFMiEGV`FX(9v%*6R&EmmaXTYp8;~i@FUP@g{y$X* z2Wnw&N~7XpWM^vRWcoj_pknC)k}{>?Vm0I8<~BBC4fA6OSQaj~?w1Ka0h1(`9kv4I%bxH&i&*-ebO7>(FjO&E5la_m8fLJ*uq*gHoM=QXoGhII%9bY9wno3;BkBY)asgj|eu0pK zu!JyyaDuRha0dUFLAXFvKmZ^#z-uQ6Q}D{{_X_ZD3<`k=wxr|Y6AM-a_q05{Dsp$LHWDDet{WW zU10cw>*p7Ufyy9fdsioLF@Wnw0(_9a9#{aJzwb$~0$7;;Sg`?EIR03%16a6zt-uu~ z?&0zdj5-$(Sm~V#H-P)M#5)xp0M9=pz_FRAfLwHd;L-=Gf;?P+Qnp|hME-aX{o_IE zS0#bV)zs2R#NI;(tjY}FWMKtx@v!QF)AmPpodJLB|IW$Y)d9TU?{I!U0jdC1os8_9 z9e#yn;t3R00g8d#EKNYl??k|MRgH|D!6JV&k3T|p1~C8ilztih^OPhkZCpT3KnWXg zRAL|#ds7fl7G!7cVgX=d;o$nEq89T@P>c z8u+Dj2ne+KdHHzU1S;r`%F1PT@C!It^iv5i8k>p8A+lW<7b?62xe5B!O;)}*a(>Y+ zG+l-7Pji%z!c134AiHpdaym8A+*Xq&^#I+KjX&>cFi$y%?5rxLw{)4>2Q2 zd}tXor&_6Oq`oz^YIi(l2wjkWPnrzs235?KL9d-Foyt1f&1lUpsfo+D2!}3%BGfZ0 zt){W3qMGn;J-knXI^WV(o=Ww-qL$`G$Tj=Y@0!RA`<1sKpTgy^M*M(gO~Vvdh`rR# z*!SAKG#K@1+fKbHyi>G!qLK0eogi4}$)^{kNlRJVisb@Pnx52Lps0>4@wi5uzC0)s zByRa!17BP|GwnZ4X>!xrnq;35e78<>9AHlAIMwq=gNJdmG;H7bvN62V`6AHXqwpNW z-J6cxmTDMMy(YNRlT6};@<2OEg2Fa`%-!gwKUnKXVQ(Jc3pIFYbLaM4Q*k{@wwU)B zo>C}hBq%tCNGC`rmZ~lYEi4W92&c#-v_TNMT7AC=%aibnB=RH2wQW^pI{M6*4IwhaU8ABmbrqUcz4NnkX&$BhXFCvweRE5^PLkzo5v`T!PJ*ncnDFEY zOHnV0UZl;7F44HvZ7oeiz6TKJZafq5Q|unPY}mRQ*o3$)TR`GY=0cwL+z1B&uWu%w zvIeR{R(Jz!@MF*L(V8%wsFDSXM?3!K}`M; zSn316Cpq^NdwWM%<>GzGr_u8EvwE%8*qG}kDOBS}+s~vkLEjiYJ9y*8{ooHhUhiM~ z=p(#NB`gSBPR%BB??k!C&8FLf-b!1x3Z6trkTI}|UyEOmUPh0e41;s^YY6GO2>VH{ zmE(*$FA2N33!CcuU4qqEB`Zf)K=t`G^ibB%9266Sv!>3j4)|+|R0pCtE@44>_2{tR zIwk}?*MRyst)H^>Dv00A+uvyV*%YCk()Ql2vLtqKle>ayKhD08DcQdB<|exlcqv9e=m9u$knO8G{xXn z@s?doJi6C1Ow9b3N$ zkoq=+1HP0k+O{3VUG9MX20&)(K~+eT|BN=WF_S^#IH@x%M&2!ezct4~T6RjPHL_mV zZyN77Z(-7J34Xl;CJ!#HDUZg1e1}Ume3+A(Ovw##N`%oX2Xkvb;2e)6^p!lGC`%54 zA^2nofnOnHIN}!W_6baNYkCmNI%mp*Bl>Ho`KFO|r>OYXhH!SV&VjIEkp8!tLJspp z2(Q(64_K$o{73%l)5@;nzU2BmA^B3q~1EcBA7UE^iR=` z#xyU)dYdW7(0AtJ#VDU9i6n+Qki>0CMjC$!sfIu*qHXQ`Gmt-;NDeu+N9)bupc%yv ze(Mhv(}#qXV^0@b0%Mq>!X&t3eGWuX>x^DGcA!>4Jfi-;Pnmh}DDh>5_j$3TsVH^GCKTi9-fmGt_*mD-e^| zr>fxntwV+xzpJ10YU2g`5H%wEwk3I3^dK_EWnEm5!aQao{nv2)riA|L#$sX6S_ghE z?lA`++|Pq4h=CSKAi=|lRY6+LOsTsssu$5-`I9t%6!nKMw@s?<_+8=f0gO~5OH9gD zJ21Elb>kH?S-k{&McLUAE^2H50X2zdJpyszs6^YY)84ON(`2APt z!5gO}%1za9CwV<_`;@_aMZ^sX4nH{-#N(0_YmhOabASjyM)#>}^tFvd9ts~5nPu@c z@=TeYbnG3t<}ZBT`qxUd?9H(XUCRDd$Wn&q{_$ zRx@_?x!*6W;P$Z71w?=rt^0inWLTPy^wEdXnu88^_2Cj~59-Y;`rKMusyUMs*2!3# zeBc2NlLA73pROlakdB#|tB+nWmkyZ3gaY=~gr^Om=8XI%_w^EV$d*g9itcLGn-^LK zBGph|F>RoZ8x;F6cwx!Q2TbDhJ{~QnkiGJ38VaYbh$eP#-m^k~vWiUP^TiKCSGJ*% zh|;swFbSd&?0-##(*E`b1b*PU{3CZcbBw*7mQFTx)+>Vu=>?G46Pp713JOfwa}z&j z@mr=!tw9VYDkm=-;$(X_tC-<~0#>A&?Tz!6Vx(m3O2t7mae|{w$3q`0#lQ!%paSpF zG4zwZ4El;%FSM)1Kb<94*O#~-<0AliPi&Ab^1i|n5FBD9`UZd0~AIjwEGZLpjC zIyjE<-5l3O-lnGds@$#&`M!3oEWnt5q`nPa zAU|9`PJpvuspZQIv`>BVL-Ni@!(F?w^2x3U{(_tO0XWd;UxB9o3bX+nsGz^PABW+Y z)}3UR0~0ZVrpcSdsQ@EL#n+ns>6UFBS4->-9R}0-zp2wzWdj>+}+=5}f*SrFesYc@GZ8zHnFpPeSi>0@Z*{{MGghH+Rx4J4CIdA;b2m?b+KaGKki$QEJ|cx5tfr6ZXYd9+KtLGw(kV&5l>l z%p-8Q^>LKdcN@fV_scJy^?~MBWF{U;$RQ;bTM`Lr+smJhG(Jd);I=+F#UvdZ1)tkD zd)=K0%60~qx__oKahEeOSOMKJYtatuaB3Oyyvja5xz7WV?5V3y7NZsiF%s zx1rv1+^r=a2T`9_lf+D1s6w&-XcI?Kq65!`=n^jWX0Qf9X+>I-CH@OhA~lW1Yn+Ia z9g`wA#u1xjiqJ;(X$r$-E~%Q6d9f`^jjC6^sn8)utm{8tLnSf=`7;7K)vbbXMUPMl z*0EGtiTj)mG#{a0@2-^de>UTtekkT{u$t+u(s)QFP8%Dz3&Zzc>?BFlq4ns=Sdc z2t2z1MU0$5za>OO#l*$M>4hC!?45z)cHm*g(#{;HVQD99=WO}U=f6b7K+YykmS8z2 z!0*`(C}HX3>>_Gm$gT6V`vsfd3*+eqH!Ky!@Bp-^1l^7ysrK zc+fI;{R^@MG9texPDT!9X27ov z04yxvU*K)QgXb?3;BgQHzR3D}_WbM2_-oYtTeSb2g@Gb+KzVy7TO%8w37C8OtJ1Hj z_`g*W75=-@?+N^GTECO~&-8;a!2GwIOZ=I01`&H3(|;vi$`pL0mM)%*lK(hUQzLMI z;2^>2{Kdp616A$S>@0uHyx@p`&+>nD{<|fPeZ66w?6H5a1+&Z>o4YgR}RG?gMjozkU9t z_AitHSbkG;K(GeeZ|34}5&V^-Q*bf`If3he=Fe@wT#Yiw+!9=To;1Q>F7Ou#;0KeZvYzd`cde}LqF(~DUCK|}p_&4}$ceE*i} zf7>(vLxTQ`cmKal(7)}#|4PvR(I_PU%*y}09`Oge_tzfryVm}8?Eh?w;2!ZCL4WHJ zf9-#N?GgWSOO%{Cj&~{iD%=+XL%wZ2i^w-x_}w_5Z8(@Ozg(E&a1p{sa2{ z75x7a@cuu4EoA*`UVn$HU~Hx0VhfZ9_idIx{+0l6voM2K|A|~>;b8eM z|II-lVO(})S6LsdEFt3gk+*hW6kZ})cI;c_8u$z9ujbMy7dvUOnbk0c@E0@08)ydT zL250_#=0@|PJP+611uZFf_R@d3A<|8#y;SmuJXTkVrJ5e6!jUQt}b5J&Z7;b zmZ)+_W;{mEW|8WC{05_fX5autr6-Pzux@>XEaHpm@-I_H!HYJiFfr0PghvHWnv9NSLHN+Vk;j7YW$g0jItO5aRh}8;YMVh(aN_;7o zndNqgULPsV5!1uQbbArVz|>t*#!+=yqB?q%4PVnVz3Z8d=ay7Y!Ne9_ij@d0%B1Z6 zoJAFV3%OtZjX#zBRf$L+8N*&hECNFn9RwqMsWK5`7(9vh(xu@L*`P(Pq_x!KD%ENr z)Y4~)7;g)NcmxV-$+E)rb?I+C&O;Ez{o$y%jxaGaAPV0$@0m2#5tD8H!rjri6v}cmZ%;gjzljTnT2Q!lQ@pPo>NREU~XKLYR4->|QKdTn4tCTV6 ztu>!HG{1zYag?1RFmP)j4vz(z8^=F5u};cD?of|ScYbCmtM~@Lg!O=dq1r75Psza0 zK%0_-K((itEml?l=1x)`3I2FIM<|=clKl?DmXwfchQTdp*|1wTQi^9hwf}DAD?$lJ zn423!f@QcI^AvBnMUaePGrYPg^8|cyR~n@bT9>L4Rl4OW^q0AKH5J?_j;j*Lc9yAQdSkLnNB%m?Apl~j;1lv7}C)ujIuktH{RdB-V&=E z5mytkJT?&LAf)!%dL_GA%#K`G6tW@at^;XJAU_e)o7c)1SRccMh%lR+IA}(|EzIKT z3gw)q@u`eHr#VpVm*u2ie$KOJln_K?E`j~DWlYXSRNYleuBB0XfvhMbX;($W&h zGeK{yjIEgdMpF&BKSJLV0@E|ELj)>{0FfYL*FM5QZalXl7?zrMkxcg?Q#!d@Rm04u zz<5ka-e752g6sC25V{kQscqvUO{268vK4Y48AUQ1+IwghM#ne6-%$Dhv9 zu5`XbCA$~+7MRGZ=m)k`drpWJlNZqSa5i%8w^@4RMG>!Y;90kzUtNI}9&pZd7h*E@ z-wD@sg4)cUK4GyW{X^HD?TJHQFd|~LQPB@Om7SBw{cfc94GG^$*{x?L#=Z>KrSa8; zU2M6J;JekYFC?^>Om+}<&y7#)`fry|(=&biW}&>ENt~WhSMviRj+ir|HY)huoP?Z| z!m%MA$!qAy%5^GJr(_+*2(iHcJF}2F-B&`)O+5F3Db{SqA;&=i0`?WCT)24m&W}r( zs2Z)&^8vkX_wp@h?LQWc6uR7^{=2o8Y8aw@>H!7j< zFE`KJ@Z1tND;_#p=A+-;kl3Gjy1ZDnx4f~BCeBzXul$%_jfoPnWTSDjlzcThi6f(R zqPSbx@W8!{x&Lu=>L?VOzl?LFcX0A)gL2ks3w2vT{ib(g%f-9s$0afQRZ8>IaHiI= zmJ?qbpYLMOMl{Y%S`*^eC;Veq2i*4$P>LechH{wtX#^fo3zAOAc_!!ic;kwS0-08m4S2W7^@HjP+LJ zvvwD<(pX;WogRB-U0PbBh*s&=oHp|DD_jp{4cW~e>;N7u`E0$NO%?o0GB=h$go2k+ z>0#o+N>%bCx_Km{w?8u25ov9zj}|=z^>CTCv)8Y9t#B}%*(Xsl1!Ck%ZB3$iH6@N| zXPq{($i5e}@+F7Plg!`r#i#MPVo|oGgeB+TsRlwTWdthciM?md`>7(u;p_ISb}*eL z#)2=Z=gl$Z>9vwRlDnW7Wh0)Lm#!Otk>`Z0EJm+Dsic-qLs+q;fhcxL5$7Dm9hQ*} zE#~JLq$OF>#ZxkjwCTJpRt;G$w5zR-O5F=17lJ4&;g>Dz$A!n_AQunYJDDgc4$xF) zd2LJ2I`Bn<3Sg?lOk?Dyf=-JhCzUtSzlAFI+FePW$_I|@-ml%AN-Sz2D!*UKnnJI& z^Ztb4UAm)M+f%mh^W;u&Te*^V^|URTEo1pu8+-di<2T}uj?rl=3c8k<#{U5cf1N!3 zGZMfb=HO=j$Gf#G09I~J9@f7jsCyOPe& zc3%xngot(bX2vz8wV`dsUUcAuylb#@p-#BGdF2EqX#44PU$*6;)jrL_4&^%<0CMQD zjn#>*Y>dK6)b~e|*TLSLTqy^=@-JK5tMbB=98f}>TW6f#wIQFXwxf~XE(^|d>mptH zd|2Q+7!gG9LERkWi-}cTHagymcNtbOT7p^Qg}F=!1em&xJuG z&=n=vUtL#;v<80MmsfC2Cwtt?V44z7qV#2s`w43EDLY<#q)C0Q)3w_v?6GlYU+DmM z?J3m*DM#gy?fAG2`Y9Vy;noS1zaqYu%}HgkW=e(oj69Ee^fQwi`Z?9}$@to52sLU6 zS>ckGmm}du4`lsx?z_Ft;h=-FKQZStagyKX&yh{=VZ?`el$iZ_{62dpkL1>PBijwW z!HeB?Sher0=l-qA+yWk$Lj_wx8i1?bIcpw2$Mf(Xxt}@C8em zHt%#S%!n1-x%l_#w2{hjwfWhJ>I^O5`F&p1ar+&zt@$c>KXGk!qxyx*&h0YPb!#z0 zd0jdmxFJvZ?$6#RF6?}{Inn=V@csA&s}krXwS0Txqj9@`ub_uFc5}N0Ozm$qV0wC4 zhT(Z6;2+?^ITxQue!`K;f^5Ue3Ud&_&f&3O(hiMRq?3N9MFY2$Z>$p#A+0CEGOR^rsdTyY%$*zU?iw!`Cs*xpgu}s@oNcrS{yZr-(-WOy)hyg- z8}3f9zXyN~j(LVix?p zxgCVWSBnvz7PK!6ZMJat{g>k~0++3qBzx`w(r5^<_}0Rmz7|+ZLY-or5+pe>0hOrN z{c)NRxEq>nSjmP^6*1l3c)AjQnGlhMm^+qsGlHjSc3P`L7J$KUJzNTv{WiEsBVGr77pL(BVhP*<%J3 zCYV(;PV>M;3PGB6>jL6*zHU}9aurRMQ@No5zSVjydgNJ0`qwHIe}mNt!SQ0dPqkT@ z8f`J*Uf>xivMuVeD$f>k;qJN`k{aoFt#X4ej7e@%I>k!R_Z^}s0fS-^D_wa_Zri7K zUpA+xgYI4Z_x+)i(iDGsuGyaj5+*o?I-6PvbKTUCDlfl z`I`^HuvK`R^yLH+`J4omyjjRpvyreCN;IJOZtrRN{j;n*r(O;ovCAyuUU3AK;sUG< zQ@lw0isJ4)hD0(&ND3m3y&PSntY%3mO`k1}(0*Yz)h@_cKO&3n9twFp30DnT6sjFO z@v6wnD&>O;geLTMOdSYjc(^y4RXb<17-8Eqh5bn$p4i+W6l0TVfI6vU6a@`Y&4o*G*!R}TA|s+M zh2rnxc9q|xz*3rITwc|?eNho?W5?t0xgc zNb8HSWhvaK8`|w-D-6S+Y>ZStl~$1T6OnAh)$E7DWLHb$xu~pmnqyz2OZ`*<*Gwt| z`O*McuQaqFuZjB1ha_RWDqVD_%B?L^dB^5%x>ECf{HiK_dyWtD(yp0GmK@X7I3Mo- z1mV#b%VY9BPI&`qsFl`13u+uC94z&O0cB%+Msn_u1h1^AA_qwiXQ|DyrwOUriJ2wY zW)>_$xuaT^2ZE943@`c$$=2{CtxjoW7U>MO4~;tDh;FrPL_5eBGk zq=YGaM`k&+wRKaS!c1`tWjZFN}7da$8q5S5+3899DIPqbv>;W2xJ;k{0kVJKQlX@dG7JjJMK%XEv ztT;L1(}N$Z+prE~YlWO#RnBZey3@QtX{?zOc&~yH%x&RAKG-P9*tx2QO6XvhaT+!C z?AxkQYmr*33HM-&{Y*+T)KMIs!nc~9G7B{|4zl^d21$xC!I%N)@wvt|I`}TH9@c9w zI%`n)N%T{ds&I|Shw`p4GBf|vALaE*ufmum6Z?Z>1)#oH=vB+g_eB%9C~`?Fr};MgSueaN?kj17E)gXwAq_p0r~ z_eH`q>|_;)<-nvg5+iJuGa_LRs8Y;Eq7te5Ad&8o*n+lwj{*p%&tasa78Hk+4CJgI zC~dc1V>*WotS-rRJFoj4ZzhkHFk>=+{EgMkSo?}8GHgATy=Dl<0b@=C&txp3b%fg* zl%2-}i4A7?4O!`w>ugN~taPTwE%i3wcuE5&LP=u+@2g6FHjpvekd@i>W~ezg9SY@P zm^0Hr8Hf4{KRZO4)-X4dYHG`Vih5sh5Y;S6%3y>s*c&C4@wp1G=B<>36DmH1mvv@w z)58U&6juqO~ z!r|kx)J(83g@5J>11UlwCW@@$<$n6m{Hf+T5<)50UXM5I8#@2{Sb*Z1Ou4$d`7`w% z$(V!JSDfSli*`)IHxT{-m2Zy)$eJS22Y_&vC8KYXegaMHoycERp*`}Q z@@G5GIvIx;X`*R`v*kL69(@~2{9f!j<~m;VfP%C%Uk|lkP|0}V{ zwh*J5WQhs)D`qDSd5OVAqtLZ+{3^KDJ;;MY6yzE76}*0!Hk-=WFzNht#--Yf8eOq! zN62u<3uxB*evw8-RS^+n9t?TiXbkcwF_OUivSx8ZXeY7+D!MP5i97s;M3c|8MjWZA zT0`o+mlmP4HeY0cSct`ua^)34iLbjx4D+WD7K)Qdsf(4N+^I#m=~7f-KZmb5CUrYV zv@RvVBsj1cxbmaRM$0hS>0~T2bVapkW**wOx^GG3A!#^N!wRZDN)Z`zKfiySRb2l= z8)Zi{iZO_Jdn=&AyejSsL_8sRJfJ<_EKXFdeKxlwyt;_aL_!Ul1x4*n%kHk0l;{(IplgqH0ZDx!SF(=xW_Q^>qIFEp+zFE1q~s}XD{ei?HuQfB8;EoDbZ(!U6``f?v)6+EA- zT|ulyS!$v>EW%jwB^O04ekfO36}bdMgn>>kR1}-UNwl8gHiV98Ha8sEPpsY}PQ0Gk zs+o>=C{LTYQyUwlt>=AWVA6_ zsxZ0W=PmsvN8(I>ch@?3Rbl~aQxuGXSi#LBkKQx-`y0u?*bON2S`zn1VLYK{In2F$ zzd9n~ zpY#TTcmsdnTn{G)ei7>Mt?%8X$`R;NCc6#(ZXGz0DO3w>F%9+e{d69yDh%F|B5!P^ z>Nuh^T2xysYPbZ;eGbg4{TxZ76rueEGVFmZMA04}A7+p+1|kg!y+5fsIviJd4nBjK#Bi>G^CI!ai_ySZf4kH3$L{xoc zo`$S1BG>Crj5;Os42%_fyzRdIzA(k`&M-Y0I>{?x@Q4wlM?tGPDkQiK;_pEFpSIp0 zz+PWAV*dnAZ1*LcL|_zckiema#$nN35i19ZQHTIa1F&&SWy8Z%0um1#3C%2!kMeUV zZ|!H!NU{ksp0k%UPH+5}WSd=yZstGI^3)@DM~^CFy`>HZ7%)^zo?1bx{!FHIk91U^ z{lP?Pz-}8*i{IpxEA%x7t-X+fh6YVPusXw>P=?zZa|{8wuwAe@BE>x`h;RNTYxJv++dvuR^SxX5J!0x z1N;_1ry#RJ=*8zERQ=jcF5YlagWBh<$Q??poz#*^pt^d8$Nk~(`SAG$NjFoT_Z=w1 zg6*x~bd`$#<;Gcc3~IK3>v_G|s{G+9ikri`K(ACqGGWN7fCX8^s{04aTeE!nf+*FS zOJOUK6jn1JuhmR6Y1UZ3Ja3(9xR}s8C+bY@&^m4yDEEbgpRKq+`O;w8NubhMV+Ce> zkbKjIi8PXn>A*!zN{!(%S_DpVbwkKp862zoi~?Q|hs zdxqOZ4v2Mv$jO8H8*~+Mux5`K#<8V0ac4r5|Lw>of_D$qLmqZdVzI$QU}FVh2j>+$ zlgP9QloJ^9`2OiOEQg-WuO!acq~0Huk~_YUd=e3uJvALclzD8s;RBK z9c@|IGE0AI1$NrJWZ7O081(nO@bcczoK<%clMI#I%b;+>hZUV(>yuIi4IPH3<83N zbF-?A3NsxUKT#v#mWr z<7X9kmV!@xM@lhWRSorwJ8{f@t49O?*xBi0-!x?w7-Yd57KB4RdF~> zxE@tl0M2DY_Add8$dNP5?@Xb0-@VuMHz$WsFL_O_%}fr#{nBqjMmT*dt}K|v^aZW} zKaChYDDvcK^T;u_a_a2W6Y~=ag7`(OMCI#vZ3?7~VABrc2s*rGOzA@_DPs=MQk59) zCkH;{4~Sj62vo`x?^h*5anatbVt#uFA6qZNeXu4dD@I_mq^6RLm??pP0rG zS4V7sEK%FXqR^tsy~>CHoi^l4{wjG-GIj{2a#;ZLk+opMz|mK+MbXT3l2Xeiua%0o z3->xGz|h4Fy+qq!?3XnHm77+~9Xsu7zl$^K>^-`wlTnrrzB)uNGMxk3aM!P505JfH z-&2Ac+m_79-qFI&+slX#H*JEqYjTx$b{y1ljIMGwWnUt_QHaFI675I| z4A3^o6|f@n6j_HvU}&UB;&D3*D5?24I0P7c?#_47{9kAqcP+4hH6EnE9i}H~1}%ds z6RK*i`0whDHB=N1Jrca`GMt%rRZ^6s0V^XIX1A{DlIW!p`!4W)&f>TEa2-lM9=h#Y zy&8Tut%wiC4YvGgpK7P_3rvjsJe3~99ZWh3%=79F*E#~7gT;5?YvHIx&TIIhC3;0}J+y`=4bqnt)IeoAT8%boQQw}ra->rR)t%4j zm0#g#Y4*%(c+^}*Dj)q6b41d@kxBkhDfAw(Aqru5<|-kV{*PJU6t6-m&Ch^rQ$OUdiZy zk0Y>Bz<#{v#BZe{IOqqgYDxxKYv`Zfe~dte=f|Mgrg-vrZH55 zg*Mlr>sX}VSejHn#4_S&ymohsSc%>_eN%Y%opRJL%tKVvisje0 za3mHP_HmEji{ZrQou3NdjeXEalc;;oLF%bBN{$GXEDss$2eQ!`zCv8h8gc#pSaz>NFRLK0lqN*Swx=ot@KuUX6cCNAIJRD>T(qOsO-1LKpnnQ%gT6=i`$EBM24s%yz za}_JpC(Hp6KQDJKYG>N^PKx!=Si`5RxTni=epJ&mz)yEZN|4y&~Cc>>Z( zlmO0%*|c|5=V`e6qE=Zv#zmfs=2QH!(K*{|bY2asTEf+>jJ2lf-B4lI>T4D=Ln?6E z)(tMljTr+nMGKjYW=*trE}hkF^lmD?{S%8IM!2@#^M)jylA4HGR`#Kh=sY!7+O(>v zx8Fv;+}BBFy2&f5HyXK`Z|jnMA-F6@<40im{L?#}|LxL}_P1lQ2rd%wYVCp2G0L8= z{onfU0{0)4@3socSbgiX>O%tP zU5_jmqx;JF+z_{J*dKd8_K$tU*{S3s@O@5X-bSx1B+0ukJdv@~*N&6b%LpgZnU|2) zW^m0VK7U8={i6kmGUZT8Fe*TfME4m`I0+{NiGHgrJ!F~>GuZb3u{t2C6R>CXoWQ_ zzxoB{AuiZ?#wBSkMkm>vAmH}FWkB_?pk^A+F6t@p8UKm%&B>aV0kBw{ssQImNCmU*48nC>_dXiQFAJ6$vEO(H(4j1`$C!da+!`x?*cb3_( z`OGNz1J<(F&@yKaGsdb5V2d-NzfP?#F#X5GRSMX!2eFe#$$CB+iI1| zT`083^ky))f=M7tFO2C9V;w7t{jCZwo!f;9&@z3y%(d;xh^g+0T~dcWK@QpBjkEGZdN|EBZp-PZu)z9MQ$K zg@zXh1SW_xhX-V=<)Xk(NVZWp?*b?;gjpO{qLBmA0H3}x8=&uEnE7g-W-I-u8AROq z>{592xa#6Mz27%63^$DOi4YCQgQilYcJX6lPW3XrV9M6^f<{kE+pNQNxaW#-(*H;^ zYdW*@93r#T%@Tncy*>P`ISl!@j1#;Sd~5&(TX;Ml_>bz74qF~`_N-c)7DY8zqe&rA zRok<^doQ~A$IY%)%7nWg4T037?Wor{DBJwAD-VE)C20I@ToK0ez>B~~=^|B&GjZZZ z=gUYj$YOF-;zKOXZ?wo_$ukvMbq%Xh~jL1|P4^oIKLxPAN(s@zFNLjc5)_99YrA#gtP= z-hYAYU^Q|6*pq&q9Dp78osuu5^Qi}d9u5*!l!gG8b_3DeePaaFDRQi z7fj8|G|hR;-&S$r6b48<=ZbLa zpB1P=3ngLZ8+P``Ms5xLMC$bqY8EAUHbKU|deXtXIiB;n?e?{vMme0Hn6I_YyE=H% z)8KQfyUsd%jJ`qD-{o86Y6A62`m2-+QA}D11Snej!wJi{DPxrGicoB)EA(9xy*t%t?C;wqs&XGNd;jj!(i3 zH`#ckz_MU>$?xRpWJ0S-0~k?6i%Ky-gj@g%Fr_%&)zTcV$gGJ`eVMv!Hfe!iG=5Y|HW@n zUBqUbsDnkEQtsnKYoP%y5_oD!&@T!&5bjJzGa=1cWBvTYTk0`CpPcMF1~8ZMvLR&C zIlC4sAYrhKy}J)nt4lYIGJ184`FSjb8dG+g$Ix_gn=n&e|k zeEk6it#o5WQD^pkuJJ+5!a+V=ZO#6AC{c+}HmY!T@K=N9AwUw^b1c8I$Gc6MjOX`H zfv&OZZUyA6;vx`-NsW`Fce@ll*A@MI?ZlsMxMBp=xpfVR2y_jqTLpAkD!mu$*1|#> zP7aqY`aV6ox6csTGFslhQK!)JDSwo;KY9Lo{!ItNN<;N+we4zi1?hv1|83M0+`F!8 zBEUh+?PHO+u-CZfMLi-kjjFV0o=a;j;S8I#xNyqu>F z83cKK1L-z4*JQ4Txy>W>Ij$~g+MXikC5=b?aBkZX9#-p{aq<)USJ`mRm?M-xoiWd`&*+AD5F1KaA@okU<1-N7fvaW?-aMBxHNJk?!G5GI zI!NcO=$8@bE$NZE$H9KI;Ey_BHB)RlVO$~~cD|`}_mO;wi|Y2@IV7k)_;Ph%Fkk=h z@P^CJztPiMddSUrOTFFipiFa7!?)neRqp=&M{S)k)dY`6MrOY^>u1;d?QF#M8p74i zR&JX+N4Chsg@kE|-1sDMIbrzhz7ZYp4JQw$OlOd2L|-HLo~U-`U)(Rh@iW|Gp?-kD z230xmhcd#3pZTsqp@KkI@+Y!8Y%UjF<0n0fjEu_jej)+3UaX>QinocY@cEp%Kc|F+q5IUy!2EdHl9=ClC$BC zW<1Na7Qb2F`_HVIOsyWE@j%8SOF7Dm<61N(-l|CKCI-NYwA831f`lWD)fp-X87ua( znObK8z0AJ)%>?Cf4UlJ8s5>YVqvk`1qDKTb@`+Io(Mt z61A*P`o*VZ^t;F8XT(U2n03H(QOGOZdxs0O9Gq90u|Akvk$5K~gnqF+r@hx#9xsaK zg}y}B1dWQvlm7dJ{bV+fM3_STr1R-d#D}@3mq-EYitA8x_f>Q)G=9>0G#x~Qv#KrL zql$BBhd+LdP|m$d?>V!+HtBc{rmwwsQa_2`)1GFKV;mSCSBG6iJTN#z)H5zFo6U&4 zeZ2_dmi3`$RSb~ua9gB$9B9lP8m=dD%F2zJfkgkFO?2h?1QIQQu{Yrkj;{5F1~w_v zx^%CqKrQoKU))@Mp}Yg5;n!sS`-=C;y?39po}y@jSV#dm{4Nm?wj-q@NZuR_@% zr_o67K+3ma+7=ioHj<8@6}*Po9p$TQmi^`Bu|2vX{&h`6*j{41u=UVKzje=x?J?wQ zF-F9B7BsZD&B=iD1*RQXLX>ZK@5-P?>n2Lw=;_6$t7(o~Nk3kG76AsHI+7~5B zQQPc#QepBM>U27bb)2os3yoH4Zq4dCS((a5Ch1m zS?ybxm9Mb!W;hu33TuwhXJ|wtH8ru`o|4R~8C_p8Mi?kwIfOW{P_rSDwz*Z;%Adx< zFk1(GYtH`owYs*1C)M*<9$w+uzG?LK*`GPxffkMdEB&L|WZvOn_TY)IWK`>=V? z(t*1mOD-}~|M@_j#|hnRNtU1F6L~&6*cMwfEYRy@&v^Lj z@v;r<*}VpnuKyh9V5a?Uw0+X#put9qi!(|KZad~jncpF+tkMtdaI-$0%Sew>b59Ym znH633v*|1s2J`*Qi+49|ZsU{`$0fGjFSk}@seDG?sP}y5RxoCQWoNuO8Z`zY-+V=4 z()dQ7(Z+W-J?MWhe4@RaZ6Nrz-OpzjXR9~PsayDrHvYqfLdZpHVSgW&J@Lop^*3BY zL_XBnV(+xh@%gbyKqPv`vj zC(4nCde_}F!d9)zyGpyOo#GfGAZtb0cRS@mtXbA}aUa~>T^o0I85rCLcXxMpcOTq!aCe7626uONxXgU}oV(8- z_e9)(2hkDPN!6QK)m;QD)_O9hx-14-ooED1*`wg9F!8jRIA%RXdfz46qJ2mkgX$(* z?*)&`^i`^w{XITKO`txB=krC77ZA$eP$m(|o4ya-a9%qp>G?zps0!L&zfH z>C0A_ zXmt^oA30X4SZC4yHNQqtDA5=4+s6X5Hq9iAmVX8rt~%737hSVj?}DO`6Y#1t%;Wo5 z2w4Vt5KaouB}_X=b$6OT%&ddp)OvQCd-l1PuubKIS4WqOjjYeEQJ-$%2hm&OEy@we z(ZMhNuOEN{Lw5j%$-OaOYzyy=+J4x0-1y9R?Ra&aIa?Ml#mmYPyRoW~czwcB>FM6m zNu5s?Z(;|-%lG@Z)5ZF)Mmb}h(=LJ=`<-GN!d@S(j(4-)k7T8NN)GPsp2^<;USc1- zYcu8U+Wfg2wi>Qo-VyJ1Z*p%n@4oL3&x^MdcL%o^w?2ypFKsV(?~Atxx7EkGD?V3a zzCZ5?Vr>t9Tz-f2e5=@0?SfWIe)oYmNjc*XrhLosZzeep8ZEdwK(y6q$=2B`))cKl z%^qMGa=YOst0W(61k2KYJ8mOgOTxS`#?f$8c?x$A`0?_)Y%VE&0p<10N;`b8_1m|i zc>U5j0*O|N9_A51DS!N}*yTr9GfKz;D9P(wjyY)NCE%)MH;kU<*? zSC9A5Bo?<+G!x?i*gP-^0M=;e(m1MQH+Zl@H7c?d3%jX)gUX}R0LOa%a)J8I#?pP?K92J@sJZqq7g3YVNej9>?@MF|*q*X`s78ESlGF90ESgC}}+R5DqKOBy~P`i(4=hT89X*W`3Fn zj#n8P8oDsi4P0-Ps&}Yf1??7mrrsE?7)3cgSfS~>SrFRO9hu2g6QB4yS{+>wbtpX~ zlFNT{o*0Za5@Zn>5ZrB;WYJWU=hl3xqE+hGi*tY^WI@D(=nRjAHsx$GP#|pm)F`y{ zMOrxc3}z-sBd|vvPD4(4+@HXWfl&xD2pF3Flw5aZ0V!sBJHl5CAoAm<&NX#bc4&Sn zk(1s?9%oZXY}}w%G+G!^58p}Bn2vU{v77P=8h#LpQm}E)%0(=VokqD1n6v?`$m^T) zx4pa?!`@qek{z^^lC(>5#N|FHHy0##TJUc_r^i;w`LBiay39oU%(&5*Q!N0E7>Z1n z=6v%}s#g=#sJl(v0~$+k&cw966>+PV=_8c|I|gTxdJ_+0^t31^R3@kQ=3)bup%rAw zSV9Ki(Nxy~T2DsxNRaCYTDYQ_#EBSEaz%ouc*p8$6b@$Jos@=kCC3My?EDeDHnPGfDUEyA)!xfIUy#P2og z$f-aA=o>`jL>+6pgH%?^RmZZ;T*Rg#h4;XE@es;5#_w zj4_~*SkYa{es1>eYt>8ZxrMYyG5gO|<_BNoWjq#&;frcUICJKIcWH4}T0SlSD10Ik ztdow0#QdiywMVp!Ykl2l`#)$S7}>z7zw_y#kkFSyClbL?0MrkoZv7PG>~{M|JGGQPRJ{i z6J|%Fr}yi<2t99QAT*Xqf+>^{y$(7V64g#-xI)ka-kwLLr-aJN_P6=4<#Mbpp@)-@ zbN2Qw@BvW|X&0DHwsud^5xS0QE!HM(^s*({fM@vj9FQtAw_VX<%@>NOfYb-{c&$91 zK)_1yxKvm)Cm>n0ER=dlzn*9`ontvVIhNX9vHv&Xz+%C;l3C~1S)o!vSB$u!`Dk2? z*%%ayAL<28vs)4)Y`npab0Ec3XDDc-V&<0oxNCF_69?UuoY)mILY(~|!5T7`spknWLHUq>O6^$l!bA5bxmk#DuGKbljuhXU0Q@F@ej(!29TXh zHrCMS&WwD+*dnoV4BF0>L1in2rtyX6=*5n&OqPxn5;@dt*%p#i(Vo`yC9@^-VLNXH zX)BYRdcz%+@g-f8aZAzO&d*#|M=i2#TBM8ip5DkOc;-jyhl1^ZV*q4fLVtx@aGk9Y z9l8fovCJIdqw2Q8oM+y=i(Df9ifvP^m9)(TdBg>8gj?+DBG*~E%$)Bf={*^-*M!4> z@BLix=?|yt)dDB)vSnuOveCSDS7h;Pxc*q~Jcft9$JLsp8+}f)Wvf@k1yfIS&7?Fl zX`8B8;~ca*#eCkWB)iEh+bWD4?Ld=xgzYSW$GQmXZtM%7Hi0F>d6naIA@fxi69HY& zNweeOt7pB+jTkQP@<-kZ(GS!oO7941x@IHI*(U8O^s7hU*O(wCI`3L@8Lj@)%XR>L zz~cUb_3)Rk*p2EohHUAKVh$Glzw}$SQhI4tdjpxWL1nO$!++f5-FOCP!F8{Eo#9@e2F9oN{ftfYOW%bhWqw zOsCKie7#7mJNo*i+oxm|$}{2GY_lz~O>aeSkMVGd<^j}4;feS%B~iTJ1ias(demUF z3&=6F(c`9HpykFy(3yxGP&1 zjHlnB+rSg_Qp}s_615B26=ws2r_TlGVzn`_?gSEv^^I3Inhx-N3xd`Gq;K`X zBIw5W2Ewc0J4QFbUJgiSgb&1{jyJ>Q!Q+AN#ld9+e|HzyD`SZ6E7SXzO`9tlQ2vn| zFzflm5Hdz-R5s&*HG`OxlEmOqztO9G_)G2mx%G zglvo~e=Lqq>rYKfLYB{AtgLLG7W=2g&dC00aeR(rX8tpkll{}j$@uC2GlGec_46(B zpJ`0YoS*a9{(4zH2eGjK3Bbh8qDjc{SC@(Nk1*4p0f0Z#K5LQwF`55(+5RyBpC*9y zulF;Ze|cFx6<7g(vH-AsdRaLBrpx|Ef%We+_D?V1PgN8`0OzNdh2!s7&d+oJY>b~? z=0DLmSegGdo|);7AnTu;nOXkq#NXh|f5I>`|Iub<`5Wd>qRgzHD`5sO|1sJAWXJj^ z3^Uv3cKpla_*7u|M~ve$;y)(GpABGTX8Hf~`qbm(_)HMM_$LY{=cnE$9`@&@P53_w z6#T7f^B3Iz*WkY&_+Q6A6;b{R>i^# zOATly7hY&;qc*cgug|YV1H|RsH>32t>RUUgCQbCtrA?L$Sk2SrEg%HJM|};(EWX+S z@ur{c{4qNix49;R0W3yzxK??^?w(a1--(Lo$`=OPp6VJn`NJQ^plF5AS!9?S7^9p( zixw!LnpU$Dt)x9>fgC+BFrW+Zq9;tOGPO=CGH*Df@I;XmWE#k&Vza|S5bL6tAjPQ~H`i^*YO!~$N5=M! z1pXLNKA%Tbb-^GZ)sCdD6^$pXG{}D*9!6bI%b;KjyVTn>LsBzZ}zlFH`Wpe}JFW zdzt?YBV=c0|33iJQx9lQrTO{C4x2RIgtQ97tTZ_0K$-gyAqgm9wz_!UaYKSSP(Ma! zy$D~z2oe!+L4ml5uc)ZxYG|f~&&nTnHidf>jSG0kvl?$J1BK|r<1XhOHsrvi^G!_~ z7Y`3>ZWn8x2;o7d$2sRg_6x`-N%*?eLnv)z_07Cx%aldPl}up8;$(lW18@ftB|mkM z%R4(`LojXGB^@SzY2sUOiitma+yu@aB7cg1=u_m8TkP|W_f8!sghkj|YjRa~ZV&;H zfs}47DsdxYlhX04=H`aP*W=51qz=%d*C_*phS_xX@(O>$bDf~qeqrw@ zZBdPh%1L>XKn80j79o1~x7&`|;TMxw_q_X^av{v$`mR3!2AR#7yND~&CxIr_D>7{V z5jEW#swbzwFGL$(W4zA;w=~pMdQ+0&4!UmNFTbnb5V{QV){x>XTB#BNPuz87(GC|+ zI9eh42y|4yP~&D;6k3!MLiPqSvn#f-HQ*Tx@1@QEayjvmwx-K}zG0^jwNVL|-!?;Z zAN29cGcxe{>=T0faUOv$>b8NJq8^&wC9TeZZ(y3};j*5IvGa^`*>fSscKmq}-Riy1 z$iN(8A@H-C*PR63@1yTkbEYVxXF$Q!5pa<)+Vs+<%S=#RE9w4t>fS;qv+}X-dc3Gx zeQ@!d&;K?w*wg-D#YK*>(t8rt5-Oy|-zj$?FmP+8^m>uF^K$ZEKOfZ*R6@$O7XfXU z=OE3+y;%q@dDH9&=RDW%DK|O+`!i;g(MO^!>IoUSWyjxJrYTLiMQx z1)`2H!}SnLoLGy5^~7&kNYYLjk?uEE8)!xr}Pg1JDf8aK1{a$}u? zHUA2HX77D5xx33rgd})Hx7U~nwqdXT;Kf*rRCy~ldVGI=Pj7y#*4`j`qv+Ip{%C>K z=Xg?)?lg?Q@E5%7U(FEQ%`hRJ6_O3pmq#BJ4k`h2Zl<1#dY%Tt_NTOki!dcR3;Aghjiei;Wb)H8}bYs%dZF7A^V-z!^OjgQ~^vhc!yBYgBi#} zmqV0WtmGX5Tz^>(@Z#;XW_(RM=JWg<3K}b_wYG!xHBB~~R;!(0&Bba>kFnwO`Er#i zOdUR!Lp8PxEkJdIWZuH+P|3H6M3r}C}oEqnqlWwR-$E2jg56}n9l>lZu z_z7dzcTd`Nfvzm%3Bto*!_!Ir0t)PfjYTanKqG%ML9~)#{$Y zE@3}4tDW+wi|ZZWW*~eui3b@~^=>X|dSoo!7eNwqihnu-pQnORIXay^$t${AsBvRZ zntv^y25IKrKly5qw_xgIw$f%b;2T955b-jfEq3&r(bbrk2sQtxJ+g-=N9^cP#ksM(*$ExH8*8dUJN^+j`X5 zad8HQz+X=|CN6o=8Q4G>p%FBv42EESBOfZnW!Hy-bnS9I!8U-=7{t}0hl!di$a|w! zfCFcN46Ao1>EAYkyljOs<*N7jecib;_$J+t75y#Bw*{XO+Oh-T59=rS$@=kMJ%?e< zv1<^h66Q|v0+YmOL0<(K%<&|6_g0iw*0={5xMr8)$z-sY3Fo1YFB_FMBCp;<)wtE1 z`vV|r&3u_9!+C3 zf`;R-LU@w_9h*)!wV^j;%6c=&tf9An}-)ht;VVILUa&SJx@CDZ>tKdaiW8N39(PGfN>@Q8zFInZ-&8a05Am8=m3@-Y(P@m-vLa{^)?V3+ zNm9#GKH13F+xXk~I`6mk35IEwMv`|?HcxYp!^qmXeqLQ?pPYX}=x=-=Dd5~jdLT^* zf6+6!>VY1{!NZ+h4gJ<=(m*9G$O>&&DzIQ$gTH5{;aB`s3h8F;rL7>P$)zZX=x`9XkqROFp>|qwX69|MEs$`? z30G}O!^D$qKY2gw^77FL;(c#_o*YC|i#G?jZ$Z~aH)s(@oDxDOZ zRP@}MugDBd16_I^^?6g%*blmpZ+j$g`9u#f)OMA$ex`o;>WBo4B6OLpsf#(EG zz2PRp#=Zg_cT3THmZ>$vz>ds|$Uy1BGnae}6zQp=aOpRjVpc{ZS-IP7T7vR`9tw@z zl;C#j711W1bz@eB%6!bpe3fdZl7Sd&&Ph#3yS-A>uU2i(?O@I}L9FJ?vPH@us$V5` zE!=d`Myk@uC`}+BLndxUC#DuTk2a@YF$J-iKIums`u@DBtf5*7&4y%ct?62sd_Ci* zMn*qdKwLwV=PwisdlbRsMrcdtRU6@7R9(a`N<=#|MaWRu8~8CQwOj-w|n^ zRfVV|kCL7eC=Zax7zj~47?mJ9385649^Dq97RsuuO#fEO1Yrm~+9uyvMiVZV@+a?y z#Do4i*-fPPdv{voIVdzf&MvSMlgB z%bnnp_dD@Im9&7PA%rw0R@*e4D-+OW=JdiAR5Il@iPo!C5cm!aVw8I1INEWQ6ZpZr zd)P~sGQ8i8wAl*20J^YzvCkWiljn`Are?&ykpTqm61&&7`$XzP?~BDdZ9dE!^OEg`*$Oph$pT zS-9?#_d8V9$PBQ5?2FamFzLqc#;o8(QfK2(&Aj)-G}{sW!~3l|$yKTp2GbX_0l{Ne z$!59-@$o_iXlogw)(pe3`8iy>inxRC(Uyag#iFEHiUmeyUbjP5@D^2{EW}?S7HU~N zCvoM&s0MhWk-|Q?r>LyJ5h3|HkXXr4SQg9ia!RtQJy9I>Ud53z6RJo0;vhkrG=voq z$ERkzH5^RZNq0{WS7h%{QsiDMC=I(YE!&BU68iAM9f2EZbY8OUf z?@%@X(oyUq=}o_5LYIZZ!(XU?%_*GO7Z62RD+tTxBm2QgJy#G0cV$XE+h@!sI|7xq zglb(%d?)`60e%er$O>*8rbN}G^cd8VIzu-(KYJqo7^@kL?E}uJgGXA0VG?9&LpAuV>qP)KO2OlZZhHvz^!<25#{`tl8= zRsMlhMn?(QTYCz%e?QJ2gs0loTe{2d73tb@yJPKz=OgBeD6l6^RIa81ej(U<#oUc0RleY}K%#Fv z?Ur5M@}=3Sk{J3Ng@IC8e@UsIU#dM}7$|v8*)xBsIWv#UvF~)nZlaNS)-s}j%-1o- z;$SeyFtsd=kSMG)EUY6lF|SBUwTm;P2oe2fHy44jfq3(5-W6>raUBl?im-9cwEc?J z)piQ}_0BWbvsDq^v1A>$>ofH*Do#$Su=>n#KRe+7-boJT^~AW8#`9>Wv19ZfruE3f z4}y%K`-*D7O!0Y85jl}@p(0R*MjU#AgB7Ip*gtUlxhe{r9CM?Kt~^|^y*J*(+PK?y zh*sU>C)(`chgyAPOG}}^JlMn)Z&BjnJRSOp_c6d@75ODcm+FI7$+pF(1bCHN;YM`^@!;8{xq&JewioF0f*nzAg` z`5${S+#*bD&fRVd@8aizWe_jAq8XM8BRn0?k`wL-j?E*mX>=;fCr4w5Zy-5;d&UX} z`^O!Ze*ez!PIWvu)Cfb7&vW@#A`Lah2iX zqvP#q&4sVc_w_^pFr=1CC$5JMP7_B6F)U3n6>Wf2fpRW3(y>v`#)e-Ut~kR-9f#Ne;Jbi^RDtEq5baR{Dq;Dki^2#r;F30mimA@GCk1!PWS^G{#=y2v#}jc`PF zY91$=IeCjCmN=ajA#M?n%StL_>(fUa#0-P|I+1Csak*?ttyY;sjw~xZF)V`-<{uz* znZ4X*8B+VcRJ1*M&hV0>y;x_Xex4G)3K=lEfTio*EM(O(-??{>N?uu6QgNP)hc5dx z2);A6FhS>LuX7CAoKM6aqIOntfKrs*FZ%;{Lm^0VS3d-bQ!R}o_CDNrdmqML7Ue{* zR|Hq5@YlDXVPch#KzS1kPQ_i-S{AE^cwkkRk0pw;H{J!yd<#;f%5+LI0D(NY=Go2v7kU-vM9LP3AGh>Jy4 z&ugnXHYn6)wH7AUU;U*ZC$;O_{-toVE$1bI(o-_|ykTp!52a~O8>V?R>Ejt0MceZf zywe4vU*qVnd$SW23sz88SoDXIly;BnIChLhv`re-$u$M72mKRE+@zo1r?e4Hw~QVG zq5YmhNbtgRk%)!~v|>Uxm4ScT3nV{rVM(C=5>@QIWvWIjyB=PNF04NH!Sl3gPnJ5a zvN{|{HG=dx?t~;Lgv7~6fvj-9*z`(KapO~wNx`V1rJ`{ZRFSJ`KnxE8u!}|2}}Zuoqw_uqN4~6E{TQ2Mx`KClC#-) z3h6;NpY|weY@0A$E5piXm9;a>T&6snL6$&Qj>Q!g>H|u%?<}*d*bojKn)pMDQ0D70 zx_IqvQ+#+ha`O@FVtiWJ>^K$~88bi{iGiXsrL$XHf&ncXB9i3MP#5_zERqA&Tz{YH zVWq}x4F3Q#!#g_@B$3VUyF>Hg56^f}_%udUC~QL{KOK3f!%tqz>9;?b9a{bVxrU^V zJzY`$BSxk!G3l~!*OzAiVFwu@msaskBe^~1uKzlrmhb-NKxG)`EzR=Hd=a#E0Y2MP zTwK)C0sm@{rJ!)dA&zXP13h4g{bm@>>b!oCYK<8>7YMT!lc zD0|h8EGh6H73AWD`}Eu5A*Uf8r^?sN)c)|B0Db`T(U{?epK>M$eC{<@_uV-|KfX6c z)KKpnArLfZRK{cp84^T%1gzKbfgtMe_;lwjS8bq&RX zic-6UdCSWE+tc--xqu7kcY+d?Q9QlghS+%8PWFXG_k)32jEKlzbAAas8;58gk-Qzz zOvs?i&Scu>TA$ZF&3xau57&o`Ps=KF?V-55Va`mkn}G#ogFj(Yu+m0X z({02jtWD z%R#~Aem*)_msT5w^y^NLNVKnVVcv*ooufX(4Y>kpJ5r)FQ5qwBk67IIyLc%%a~N}b zL9ANAju6}dqtYy|IBf`m!4XY$>ZoRqS zafk#&j~JC&N&2o|Bk9`Mczk>fp!k$Ze`9^?uo1Z;A2gBq}>F5K5bSv%3??4u@4mYFg0)3>!U(Uh-zulKXToSIc+HVma6c@zFl$@?$ot6i}9 zh^BFkHExkeV?@uPwV7OghzKV8^{jlZ!=#S{%C+L&BlrhT;llAAi*uD<<`!#q5+(D%oqC>t>1qsxXY=5StHE=W|9V^CiSR$Vo^qFaR zI2Kne54XotSYBSjn1g+C4sN;vK)W&a;Y-Bd9Qt$xOYU*8*UT@yNvo-_nd( z-@mQ2mXcrmAv!glw2wSysY{eE@3PP2SE`pNtzxMsU!b`3l+JUl+^_UR5vx0tJ7fd zOC8h@v-z{l+PU%@9`6ZvxmA?(=@&Zfb2Nr3Ja4`EvTQ#O2!=ABC-b4%g(~m`Sh~F% zsg>iVY)i_eHv9SelIf0>g)Ga!`79XpY6B-*@|%EhP0_JEf%&i0MusqueesttkZH8c zA~=BF5M?&O48Ueq%rL2{kjhM%)5hdyAS(qB zb~>qU41;2yRr$qvi8?$)!<|OW@i5N*EN6XB;?UHy?ttEDeqPGTQbLQ)N|~2uZ6vi2 zGoqal#;-t!y^$m>M?*%c(#s>9XSs(MQ}9q7usb%1@G?oL&ex| zwDlAG@|L{`W}7%4sa?S-A4~Zy6Pf2Fy|S6$^@ef2j zf$gDiE`5r0=|(eWx%vFbZ{qwX-}BttwX@)Z4^n;sbdY9Z?Jrq7xbWl|MDjv*V-#H! zs)1j&lxWO{*prj^&R^iOa3@Ewf=$)22@nx=xk%~*+ohXM^Aabk1IIVeD!6{n^@ofX z6$O;T7|+dExDR`a{dmcgMGrfaLYT3{1-muU!VUVx)vrK%;{_+t!4%FWx&WuFQf-~) ze%DF~qvSC1;h~>^g{1ukz7-E&{J`&Umd+p7gsA9iLHD!VH~@9=x9MT^OMF9wo|h4> zH?QonRPF=n;)ZQ>!`LGp_&P1uapEdfo~aSoyA|SH z<31}u5&hq6MvvyvPoe#r7W&V$l$mGEo2AxrG>YGe34ik4X%LUF31ZB|){4h7aC%E% z1BF?*HeWc#k9s*svmVRmawk7z^=p{*7WI_1L!LMojTv3MJD=y;FR_;0h z;2E2eAM`CP0LhMugcF_Db!C)Gp@JW^1w{{2LD|Rc!w0L zZw07U3to5_C|7%+F(?and0tENw@IFMfVFWzuQk{iSJfLD*3)m;Gl)QPQq(8XT?6EX z%HbDf((w8opIy@Aj=e^w6P=a1ySq=YX+vW+JAPK@+gx_yoOc%8dzrq0(fnX|c$9Jy z4oxVp4j9(N_ZXCQj{@H3_LT7Orpij2=E^ZBh|)31cvy63W-5@0>06`ex8sKyRXPYl zh3`zijv^Qr<2F7LXlS2tXyCAA?mWft3Rp8Cp5UCr*#q7@p8KS^#m7d)xVe8OC0SiC znuG)yWlSt>Z7so^AY{7m@cFzWG_AciJ5Aw=OFrce0MvB0Q-yEY!C*1OAOrXzc?MdT zU4f*wEkE2%tf!sX55}irJTph<4XNZFze>ln+#}`10;5LzkL%Tde8a`2en2=$ctk0! zf8>3B9=D4pFxkA`MZL+KO`||K`)$v%g53qmUO6!OTiwo%S<2Y5?IC%qcU@|&4iXBZ z{=ngw!#0d$qg7&30$I$CA5{b%X9J+vFZ{rJt0p>gx;nZVdAj6aJTcsqS+c67 zSzB6^U-RRx#Ivuc;WV)!vR^^b($dnt-AN(5w6LMONM)G@ViD0}v3tjiD>Q;w51r3f z;tg|d$QNS7Whv}+di)?HcWCW=vtu*^zB2n8h7$EMZoYS=Aj#mNCZ@b5E^F5%y3_S$<)_V z6rtE24~}^UVpo#e{zdJYkw$Kx-!*MsYbDEQT>RCMQD*hD>u;N9EiLu+GRia88Uw#D ze)5}X*Vj0gl5klY8qQ~NUyqwH>*pWS2;l5a2~!B-5dUzGC-u(fOUK;*(Qx6sdNP^E z){O8if}q946Vwpj%>P3~U|)~?8ZyGbz(Aiksf0mcWb9(>k;A1`a`0dcpJ;>RX#Gbe zGDCTL+ki^Cwvv;D*+^@dy+du|(`4D)sJ(-^laYqLcN|q-i9p<{s_rr}=A3D%DN~vT z^*#$Y7+ccf1KEX?^jB7c%;fmwXe-I0{^n*8#rRyi>hbTQaRC0ouegEF`u^l9Ss&5X z`(2t5B0?Ys+E!diHxE$7C7VpNkR-8td_ihOMpajjclD_ z5SsD1;is~Mhu|U-mY%~MV>aCY+HOL}&v~ffrVfnu39*1UN4HLq2#sCHoiYPX(Fn1- z-LTm&0k9lca|O6rFJKY?0sI}YH3k9X?{kvyCS5?1HzPOQKNNU&eSstka|)uv-n++S zbO`~!FiII`E?m=&B1DXRanat=Qqlrak1$Gha1E=0W~MPpibXH301l4h0#6DvFYV*F zq*#P8cJ1dFgwTw~q#^aXzCWIJ-M-IK;@kTQbZfA?zDMeAy~AM0$Qeo$#xr@<0o85U zSE<7eiCib8BY%B&ebju;gs0`X2a}$I!$I^~O1EL_}Rwv|BV8Lv}!2}I`X$j%RZGCt|eY4H;BlJC3hxRQ> zC^aLz#}N2&?twVR>>HB@2xJ>SF;hatc=;-{P|);`zyR29)ncp_Ri&C6*6nQX5S2fC z_#3-VAKc^x9iEe@hnouNJpaf;#RF1~ujCxnSvcH8_zuup~kfbY_r{1wA)_=%jQ3ORHjb2wL_I((( zJpPO@9pzUm5|TvU6IdeUdhvSd^wjNCZXsU?x7rr(?}`_ZN|rj0^`xeYJAWG7%e|$T z&5>k~+KBC>d6Rf9y)BDOHX608YcqN=WEiC%U61Xccu~7=vDBIzecSE1rfd<+Cj6Q* z6^}8xID#-1GeVH^A^)EFO7=u^{d1Rqk_9G0U;e6L+pEjWJK~M)Mf;naY7upP68I>L zQ9zySQn^-S*G|D)$b&6|qi8MbMkec|QH9FNtzDFx%B&TBny2f?-w+Cl_zzNu zhCZVEAV)IIynYvm$|ZJ_FL+Y$_lH3ihU$Toe4Q-ST$^W3_(Ce%$>(*^a`2I@IX|Wp zk<{VB?QU}*?a_67=(WB24jSEAX0-C&Vinq#^ujEDfkd$CGc%*s@evWdzOt>00eR2A zd5BYU--b|1LL(ksrn!^LxDYATs29G9>W!6AQ5{VY#)T47Fk8mFe5@}W;^o9PYkuTU zUq`0=+$TcA{LKhelNF#|O5UlQw>}#-;#9INv*)c)HafVZ_B5KT%%R*@%9R~7@OGR` z9a;gvTCh+)6tQ_y?*@_NqZ@f^t~vg$l-#0?12<@grQGNJebx^F+TBeA86w&vtp&2L z9}cqAMp_$AE^C#;Ze>5r8yo+)@296mEVu3RWo^?JFQ+#8+20z?m77vOV874|1?41h zot7%(EmaU+N7SB|&QkJclxbn0!;aV7?o`|}`bMjZQ0Aj_O@D_^vZ;jDC%9HJ^{1X! zAJJ1O;F!6L)30ebQlZ7uC(FA>fgzq?RW$b}Of3`Xwp8y29y#=*k|>L{G%ZTth{}S1 zx5*NRs-)#1h%r8bgr*< zI7tm@*hQV54GO)JBTk@q7ZOz@`rOfWauz5oi*|8HvM9utVbZX)SPQ?A-c+7C3$5Vy zX-hNbLOBa#DK!mEfvc$lNhd|NYrSXrHrIKwFE)5 z%Gnf5r-j-JsxIkLh3|>>OUs^6YVxKzb-{4^Vf>Z9;xL$ukvfn?Lxdbh^1g{AB}~xw z!FZ7GQF7=jsgA)YHc z3ilRP7&+75FW}d}a%PQa96;el<`vFS-8B&hFHFnS^*?Uu-@bAwwGz6(DFK%QhOC9Y z7CZxg{gL3wly9173c<=v7D!#qsK67c=g@Ve{rk2lhYe{LUq#L#06#(e7qCalDyR_KD59dkdA}-cDTdX`%LpQ1#Tc-GJ#-0d z__qLI)16FZde;?w=lwA?r5eBSd@X0#DEWgAWh=eO_ zEPehbY{DZ%Gx_g z=TG+8@T5b}S5e9&a~=fR<@r}s70?!wzUOv1qKT=~x(=~A-)8;*0?@p}mU6+bs8T#; zUaPHyxZO297+9GNc+@S1Q_N7A&H~cC7Ah_Gw0=U1b8pAR9xWdet$cG>#xm)ywQ`() z!nD1R=h1M9Tn_#{;E}z!roU*h*LY8sqiotz8NUB6$DwgV<}mA+K6=JAmg&GtL?W5} z;)=ilvY>gxGFz%k)-Fy_Ujc5ZIP5$)+1aSV{^Q)Fxp<;}cD)j7PzvLhGhF&}&QM;5 zB8IB^x}Vcr$mEHeS7Ter2Ym~bai?b0&&a|>W!)SYhl_QZ6tCN?OcE25tB154F6gBM zz(EjRyfb&&;A@M2zgt&c=7FWrnV{xQU2|GJYSW-|bji|hPE~bT!x#bVyD29-4K0c) zjPlEB$6{plLx+dOD)fx{Ts~;>*JFn!^taR;*xNwHaMLwa2Z>bu22SY8lQ%0oZq-;v z?t~W2Ia+brgEftVt{H)~54q?Pec4~?8^7NSt6T)S)9y?pW)B7 znQq%7c1Vw>m#3Yz+5Y_fD?P9@V3R-4WtpH)J!YAzqiO4H(3#%L`})*d?rdN11x~BM zqC**USMp4HbF&}{e#=(2^N$;~%e`mF~d=B>Rn45%+T*OVTP*ZqS z*U)Rq3hMl?)kO*MwOV#TU9cg^^SwK3`!3t-$)Cnwp;zB?4%Uy%0Tg$}uhfA~+7w-^$4cP%ENIaPbF_+I@H?FdhHl^bRb<$kEX-M<;H$yPd#Jj`-OUX&NO|v$gic_28_^gR z3;M3?dG%n$l`v9Wbt5=HvYRtMsNaMm#e-N{Iu0vgnR>L77tz+i*frMii40cN-EPZS zQzBwE+nfDz(5ZNzt>r>#_+Dk77aiZf;B1{GeGLfobRP;e?2J8v|J56HQI0}5<=dH& zGj({STjKg#;Wld@+c}NGx_=CXJuks^yMFto+hbtSR~KUgl#kXM##5>t!c)5)*c0X6 zecfn!$4%8$739G!#5dV^40&rXj8R4#4pA*rnhORV@U9$-vjwE@&WEr z{bI3u!@UcX@8S*rQTzgYlV+W!TeU;g&!@n~uY;Y(^kMyW9prt{L)^8FgaOoBKnHkL zyo-*sI++06TgDsrcY@bu84pCepf^l2kq!7d(FKt8XnnWCHP5Bo6V~P$^5-4=cF5FG z)YLItj<^Sqi-ZT@ixdQ)E~2h4uhFl3mq<^-n+DfRn<&@Nn+eF?BRL}`ep8ZN)u0=(mr9*vACR7!opkN+THrbhg5%sT zTra+=PE{Z}Yc1HWvIp3kev`nPjae3c(pg5EQYPT8iU%CF1@%Pj=TX|btseOvAg-zp z^qT=|kbeA!eAHZP1WrD1&sXO;f?ac7(6{`pA7GtLCJ2l6AzcUc9*|Gx==zb#|`g@1e^Gyi}*fB7Az zzf8&}zVZ*N^OxUY{{xe-Gk=mIoPUrO&QGF+{V&GC#Qce|urdCHZ2nR>f7z9PD4ajY z$tOwkhjU^1i__Wc@qnAN1yLZvUV+f3^QXZ`l4>#h-|O8KHmB8`eKznE#SBe;4u}lQsVXrTN>F z^?yNWnE$j7{x6i~Z^KAgBS$?$Jx9I&?pCTMEAy{bPK3;yjQ<0v;o*0+)iba#awOFM z?AL3}OMKDRMNDXB$V;rsD$6KqD{N$HCgE;xr06cEWZ-UL!1)Og^YOTHxmwv;eTE@) zwX(E!;Bw_9*89`tm+RC1<7OZx{5!$KEQ!PaM~f(D<`=_kTM-@hMiGKa>C82|Nsc0{>eg|61I?dH!c=|7_Ez#^+A` zYsvp-YOepDn=6;BqNKd8jGn%crLHjxKu_G*dqBGLh=V0aJ zWMu?!80d;x>*-q>8S**WI~o1W_CJ^SyF35)(?7pFLt}4A~6~8JU<2^;y{e!~Fgg;Xlkz z(7^HYYx|iY8^Fkzk(t?uj+uj%m5#+gpPf#R1zSDl82@38 z!k=A!Kbv$5nK?S}G5&`+{JRFs|MhA4*FiQk`0vAF>tt{F_pgJY0fW&$O~f5O&kNJv zvutR0)B0I$rfH4l#WeEeX)_p6Om-W5Revb z^5s0G+LbQrE%%ogD_UB%tdGE(*V$e>2yx=rt-7CYe*-GX$!>I$Y==9ZSmXy5K(E|g*;eYOyL}IIF!eMVHH8Pod zIebsZKRFpY;ML$?UNt5(cbAa4TX3V>Z7M={cQV0wmliSuS|E>EuA2h|Nl$VD{LqMX zEwz>C67bS>xSZFa4XN5lMZH|@u4;Ex)B_b2uFkG%cU9B_6&0?|uFATruuqoB9zAyH$9*bXBZ2vT)Y3Clb>vyf6AB%8gt^0M9ZCr-~8gw z(H%}{*S_qmoZg9a}TV~ChLNp;~l#~k($eqy6vsoFUZ@*n-|8L z=X;iBVoP%mEzR#AoS7b+>l&Q3Ez}@MH5*6ba&9u1dLIZC*^5@^@0Q zC}SGD#XEPE$P@)8hD~M|@NlSH#+VKncFJW!$Oe+BI?PL3IfJ44gn?}lCPVe9vWn_c zWdq9;fpt0bi_I3u_=7l9okLoZa+_7tfK^g1(-=eT!chauxQunVj>kh*I371e5HgjJ zX@pECWCkHKu?!x(qB#T)5V4GyYn91Qjutcy7yeH~E_b1)fmFxca98w3Xh)%5gf4B;&KEBBRC-T!MX$o zBRCX-17ajp9}#mgYCp&0Ybdb4*YNJvLJyHy|#jqgW3#QJlk70c)$g z4Xzr%i)1L4;t&qmV@HsI-ze&4(veu)O1qps-_{{S{jX0Rg)D!Obfyp$!xLWvqT;r; 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 70/87] .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 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 71/87] .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 72/87] 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 73/87] 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 4d4e3ad9015c2c9fb09c05ed71e6712ab3e4a56e Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:20:45 +0100 Subject: [PATCH 74/87] .Net: OpenAI V2 Version Update and Adjustments (#8392) ### Motivation and Context - Update Azure SDK to `beta.3` - Update OpenAI SDK to `beta.10` - Make Azure OpenAI Package Resilient for mismatching OpenAI SDK versions - Adapt to latest breaking changes from `OpenAI beta.6` + - Adapt to latest breaking changes from `System.ClientModel beta.5` + ### Impact Some changes introduced by `OpenAI SDK beta.9` cannot be recovered for custom Endpoints, enforcing all endpoints to have `v1/` prefix, removal of non `v1` test scenarios was necessary. --- dotnet/Directory.Packages.props | 6 +-- .../Step11_AssistantTool_FileSearch.cs | 4 +- .../Extensions/KernelFunctionExtensions.cs | 11 ++++- .../OpenAI/Internal/AssistantThreadActions.cs | 45 ++++++++++++------- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 9 ++-- .../src/Agents/OpenAI/OpenAIClientProvider.cs | 11 +++-- .../Core/AzureClientCore.ChatCompletion.cs | 31 +------------ .../Core/ClientCoreTests.cs | 4 +- .../OpenAIChatCompletionServiceTests.cs | 14 ++---- .../Core/ClientCore.ChatCompletion.cs | 4 +- .../Core/ClientCore.TextToAudio.cs | 2 +- .../Connectors.OpenAI/Core/ClientCore.cs | 4 +- .../Services/OpenAIChatCompletionService.cs | 22 +++------ 13 files changed, 73 insertions(+), 94 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 0b996a99c192..7a07062811db 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -5,10 +5,10 @@ true - - + + - + diff --git a/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs index d34cadaf3707..70985d0fc27b 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs @@ -62,9 +62,9 @@ await agent.CreateThreadAsync( finally { await agent.DeleteThreadAsync(threadId); - await agent.DeleteAsync(); + await agent.DeleteAsync(CancellationToken.None); 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/Extensions/KernelFunctionExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs index 97a439729ff3..c4acca58770f 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs @@ -46,10 +46,17 @@ public static FunctionToolDefinition ToToolDefinition(this KernelFunction functi required, }; - return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName), function.Description, BinaryData.FromObjectAsJson(spec)); + return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName)) + { + Description = function.Description, + Parameters = BinaryData.FromObjectAsJson(spec) + }; } - return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName), function.Description); + return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName)) + { + Description = 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 d66f54917d3f..06c49f7a1905 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.ClientModel; using System.Collections.Generic; using System.Linq; using System.Net; @@ -52,7 +53,9 @@ public static async Task CreateThreadAsync(AssistantClient client, OpenA { foreach (ChatMessageContent message in options.Messages) { - ThreadInitializationMessage threadMessage = new(AssistantMessageFactory.GetMessageContents(message)); + ThreadInitializationMessage threadMessage = new( + role: message.Role == AuthorRole.User ? MessageRole.User : MessageRole.Assistant, + content: AssistantMessageFactory.GetMessageContents(message)); createOptions.InitialMessages.Add(threadMessage); } } @@ -89,6 +92,7 @@ public static async Task CreateMessageAsync(AssistantClient client, string threa await client.CreateMessageAsync( threadId, + message.Role == AuthorRole.User ? MessageRole.User : MessageRole.Assistant, AssistantMessageFactory.GetMessageContents(message), options, cancellationToken).ConfigureAwait(false); @@ -105,28 +109,31 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist { Dictionary agentNames = []; // Cache agent names by their identifier - await foreach (ThreadMessage message in client.GetMessagesAsync(threadId, ListOrder.NewestFirst, cancellationToken).ConfigureAwait(false)) + await foreach (PageResult page in client.GetMessagesAsync(threadId, new() { Order = ListOrder.NewestFirst }, cancellationToken).ConfigureAwait(false)) { - AuthorRole role = new(message.Role.ToString()); - - string? assistantName = null; - if (!string.IsNullOrWhiteSpace(message.AssistantId) && - !agentNames.TryGetValue(message.AssistantId, out assistantName)) + foreach (var message in page.Values) { - 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)) + AuthorRole role = new(message.Role.ToString()); + + string? assistantName = null; + if (!string.IsNullOrWhiteSpace(message.AssistantId) && + !agentNames.TryGetValue(message.AssistantId, out assistantName)) { - agentNames.Add(assistant.Id, assistant.Name); + Assistant assistant = await client.GetAssistantAsync(message.AssistantId, cancellationToken).ConfigureAwait(false); + if (!string.IsNullOrWhiteSpace(assistant.Name)) + { + agentNames.Add(assistant.Id, assistant.Name); + } } - } - assistantName ??= message.AssistantId; + assistantName ??= message.AssistantId; - ChatMessageContent content = GenerateMessageContent(assistantName, message); + ChatMessageContent content = GenerateMessageContent(assistantName, message); - if (content.Items.Count > 0) - { - yield return content; + if (content.Items.Count > 0) + { + yield return content; + } } } } @@ -190,7 +197,11 @@ 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); + List steps = []; + await foreach (var page in client.GetRunStepsAsync(run).ConfigureAwait(false)) + { + steps.AddRange(page.Values); + }; // Is tool action required? if (run.Status == RunStatus.RequiresAction) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index f5c4a3588cf8..28c8dba9e3a8 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -108,9 +108,12 @@ public static async IAsyncEnumerable ListDefinitionsA AssistantClient client = CreateClient(provider); // Query and enumerate assistant definitions - await foreach (Assistant model in client.GetAssistantsAsync(ListOrder.NewestFirst, cancellationToken).ConfigureAwait(false)) + await foreach (var page in client.GetAssistantsAsync(new AssistantCollectionOptions() { Order = ListOrder.NewestFirst }, cancellationToken).ConfigureAwait(false)) { - yield return CreateAssistantDefinition(model); + foreach (Assistant model in page.Values) + { + yield return CreateAssistantDefinition(model); + } } } @@ -132,7 +135,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 diff --git a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs index 3e2e395a77ea..0b60b66fa84a 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs @@ -50,7 +50,7 @@ public static OpenAIClientProvider ForAzureOpenAI(ApiKeyCredential apiKey, Uri e Verify.NotNull(apiKey, nameof(apiKey)); Verify.NotNull(endpoint, nameof(endpoint)); - AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(endpoint, httpClient); + AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(httpClient); return new(new AzureOpenAIClient(endpoint, apiKey!, clientOptions), CreateConfigurationKeys(endpoint, httpClient)); } @@ -66,7 +66,7 @@ public static OpenAIClientProvider ForAzureOpenAI(TokenCredential credential, Ur Verify.NotNull(credential, nameof(credential)); Verify.NotNull(endpoint, nameof(endpoint)); - AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(endpoint, httpClient); + AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(httpClient); return new(new AzureOpenAIClient(endpoint, credential, clientOptions), CreateConfigurationKeys(endpoint, httpClient)); } @@ -102,12 +102,11 @@ public static OpenAIClientProvider FromClient(OpenAIClient client) return new(client, [client.GetType().FullName!, client.GetHashCode().ToString()]); } - private static AzureOpenAIClientOptions CreateAzureClientOptions(Uri? endpoint, HttpClient? httpClient) + private static AzureOpenAIClientOptions CreateAzureClientOptions(HttpClient? httpClient) { AzureOpenAIClientOptions options = new() { - ApplicationId = HttpHeaderConstant.Values.UserAgent, - Endpoint = endpoint, + ApplicationId = HttpHeaderConstant.Values.UserAgent }; ConfigureClientOptions(httpClient, options); @@ -128,7 +127,7 @@ private static OpenAIClientOptions CreateOpenAIClientOptions(Uri? endpoint, Http return options; } - private static void ConfigureClientOptions(HttpClient? httpClient, OpenAIClientOptions options) + private static void ConfigureClientOptions(HttpClient? httpClient, ClientPipelineOptions options) { options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs index 8a8fc8dfedca..f0c7cdf4250d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs @@ -1,13 +1,11 @@ // 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 @@ -18,34 +16,9 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// 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. - } + => AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); /// protected override Activity? StartCompletionActivity(ChatHistory chatHistory, PromptExecutionSettings settings) @@ -71,7 +44,7 @@ protected override ChatCompletionOptions CreateChatCompletionOptions( FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, PresencePenalty = (float?)executionSettings.PresencePenalty, Seed = executionSettings.Seed, - User = executionSettings.User, + EndUserId = executionSettings.User, TopLogProbabilityCount = executionSettings.TopLogprobs, IncludeLogProbabilities = executionSettings.Logprobs, ResponseFormat = GetResponseFormat(azureSettings) ?? ChatResponseFormat.Text, diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/ClientCoreTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/ClientCoreTests.cs index bf6caf1ee3f2..f41b204058ed 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/ClientCoreTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/ClientCoreTests.cs @@ -67,9 +67,9 @@ public void ItUsesEndpointAsExpected(string? clientBaseAddress, string? provided 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); + Assert.Equal(endpoint ?? client?.BaseAddress ?? new Uri("https://api.openai.com/"), 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]); + Assert.Equal(endpoint?.ToString() ?? client?.BaseAddress?.ToString() ?? "https://api.openai.com/", clientCore.Attributes[AIServiceExtensions.EndpointKey]); client?.Dispose(); } diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs index ccda12afe6a6..326b14bc7368 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs @@ -76,13 +76,10 @@ public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) } [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:1234/v1/chat/completions", "http://localhost:1234/v1/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 @@ -98,13 +95,10 @@ public async Task ItUsesCustomEndpointsWhenProvidedDirectlyAsync(string endpoint } [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:1234/v1/chat/completions", "http://localhost:1234/v1/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 diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs index 1177fb7ec846..6546bd291235 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs @@ -364,7 +364,7 @@ internal async IAsyncEnumerable GetStreamingC using (var activity = this.StartCompletionActivity(chat, chatExecutionSettings)) { // Make the request. - AsyncResultCollection response; + AsyncCollectionResult response; try { response = RunRequest(() => this.Client!.GetChatClient(targetModel).CompleteChatStreamingAsync(chatForRequest, chatOptions, cancellationToken)); @@ -644,7 +644,7 @@ protected virtual ChatCompletionOptions CreateChatCompletionOptions( FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, PresencePenalty = (float?)executionSettings.PresencePenalty, Seed = executionSettings.Seed, - User = executionSettings.User, + EndUserId = executionSettings.User, TopLogProbabilityCount = executionSettings.TopLogprobs, IncludeLogProbabilities = executionSettings.Logprobs, ResponseFormat = GetResponseFormat(executionSettings) ?? ChatResponseFormat.Text, diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.TextToAudio.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.TextToAudio.cs index c0fd15380dfb..1a34fe7a0230 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.TextToAudio.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.TextToAudio.cs @@ -40,7 +40,7 @@ internal async Task> GetAudioContentsAsync( Speed = audioExecutionSettings.Speed, }; - ClientResult response = await RunRequestAsync(() => this.Client!.GetAudioClient(targetModel).GenerateSpeechFromTextAsync(prompt, GetGeneratedSpeechVoice(audioExecutionSettings?.Voice), options, cancellationToken)).ConfigureAwait(false); + ClientResult response = await RunRequestAsync(() => this.Client!.GetAudioClient(targetModel).GenerateSpeechAsync(prompt, GetGeneratedSpeechVoice(audioExecutionSettings?.Voice), options, cancellationToken)).ConfigureAwait(false); return [new AudioContent(response.Value.ToArray(), mimeType)]; } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.cs index 843768bc17c2..271aa2321ea2 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.cs @@ -36,7 +36,7 @@ internal partial class ClientCore /// /// Default OpenAI API endpoint. /// - private const string OpenAIV1Endpoint = "https://api.openai.com/v1"; + private const string OpenAIEndpoint = "https://api.openai.com/"; /// /// Identifier of the default model to use @@ -104,7 +104,7 @@ internal ClientCore( if (this.Endpoint is null) { Verify.NotNullOrWhiteSpace(apiKey); // For Public OpenAI Endpoint a key must be provided. - this.Endpoint = new Uri(OpenAIV1Endpoint); + this.Endpoint = new Uri(OpenAIEndpoint); } else if (string.IsNullOrEmpty(apiKey)) { diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAIChatCompletionService.cs index 08de7612b078..f544d8a5c61c 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAIChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAIChatCompletionService.cs @@ -71,25 +71,17 @@ public OpenAIChatCompletionService( 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 == "/") + // As OpenAI Client automatically adds the chat completions endpoint, we remove it to avoid duplication. + const string PathAndQueryPattern = "v1/chat/completions"; + var providedEndpointText = providedEndpoint.ToString(); + int index = providedEndpointText.IndexOf(PathAndQueryPattern, StringComparison.OrdinalIgnoreCase); + if (index >= 0) { - internalClientEndpoint = new Uri(providedEndpoint, "/v1/"); + internalClientEndpoint = new Uri($"{providedEndpointText.Substring(0, index)}{providedEndpointText.Substring(index + PathAndQueryPattern.Length)}"); } 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; - } + internalClientEndpoint = providedEndpoint; } } From 335fafbc7c69b0e7f61835082e3603fb344c3940 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 27 Aug 2024 18:49:11 +0100 Subject: [PATCH 75/87] Python: .Net: OpenAI V2 Main Merge Conflict Fix (#8399) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Motivation and Context Check Tests and Pipeline execution fixes. --------- Signed-off-by: dependabot[bot] Co-authored-by: Dr. Artificial曾小健 <875100501@qq.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Co-authored-by: Tao Chen Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Co-authored-by: Maurycy Markowski Co-authored-by: gparmigiani Co-authored-by: Atiqur Rahman Foyshal <113086917+atiq-bs23@users.noreply.github.com> Co-authored-by: Eduard van Valkenburg Co-authored-by: Andrew Desousa <33275002+andrewldesousa@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/python-build-wheel.yml | 45 - .../workflows/python-integration-tests.yml | 122 +- .github/workflows/python-lint.yml | 27 +- .github/workflows/python-unit-tests.yml | 59 +- .gitignore | 1 + .vscode/tasks.json | 117 +- README.md | 2 +- .../0051-entity-framework-as-connector.md | 119 + dotnet/Directory.Packages.props | 8 +- dotnet/SK-dotnet.sln | 207 +- dotnet/SK-dotnet.sln.DotSettings | 1 - .../huggingFaceChatCompletion.fsx | 86 + .../Demos/TelemetryWithAppInsights/README.md | 6 +- .../.editorconfig | 6 + .../AzureCosmosDBMongoDBHotelModel.cs | 42 + ...osDBMongoDBKernelBuilderExtensionsTests.cs | 36 + ...MongoDBServiceCollectionExtensionsTests.cs | 36 + ...MongoDBVectorStoreRecordCollectionTests.cs | 651 ++ ...osDBMongoDBVectorStoreRecordMapperTests.cs | 89 + .../AzureCosmosDBMongoDBVectorStoreTests.cs | 103 + ...tors.AzureCosmosDBMongoDB.UnitTests.csproj | 32 + .../.editorconfig | 6 + .../AzureCosmosDBNoSQLHotel.cs | 44 + ...smosDBNoSQLKernelBuilderExtensionsTests.cs | 36 + ...DBNoSQLServiceCollectionExtensionsTests.cs | 36 + ...DBNoSQLVectorStoreRecordCollectionTests.cs | 647 ++ ...smosDBNoSQLVectorStoreRecordMapperTests.cs | 79 + .../AzureCosmosDBNoSQLVectorStoreTests.cs | 126 + ...ectors.AzureCosmosDBNoSQL.UnitTests.csproj | 32 + ...zureAISearchServiceCollectionExtensions.cs | 27 +- .../AzureAISearchVectorStore.cs | 12 +- .../AzureAISearchVectorStoreOptions.cs | 12 +- ...earchVectorStoreRecordCollectionOptions.cs | 3 +- .../AzureCosmosDBMongoDBConstants.cs | 12 + ...eCosmosDBMongoDBKernelBuilderExtensions.cs | 51 + .../AzureCosmosDBMongoDBNamingConvention.cs | 25 + ...mosDBMongoDBServiceCollectionExtensions.cs | 77 + .../AzureCosmosDBMongoDBVectorStore.cs | 77 + .../AzureCosmosDBMongoDBVectorStoreOptions.cs | 14 + ...mosDBMongoDBVectorStoreRecordCollection.cs | 431 + ...ngoDBVectorStoreRecordCollectionOptions.cs | 42 + ...eCosmosDBMongoDBVectorStoreRecordMapper.cs | 86 + ...nectors.Memory.AzureCosmosDBMongoDB.csproj | 4 + ...ngoDBVectorStoreRecordCollectionFactory.cs | 25 + .../AzureCosmosDBNoSQLCompositeKey.cs | 19 + .../AzureCosmosDBNoSQLConstants.cs | 9 + ...ureCosmosDBNoSQLKernelBuilderExtensions.cs | 56 + ...osmosDBNoSQLServiceCollectionExtensions.cs | 77 + .../AzureCosmosDBNoSQLVectorStore.cs | 86 + .../AzureCosmosDBNoSQLVectorStoreOptions.cs | 21 + ...osmosDBNoSQLVectorStoreRecordCollection.cs | 657 ++ ...NoSQLVectorStoreRecordCollectionOptions.cs | 59 + ...ureCosmosDBNoSQLVectorStoreRecordMapper.cs | 71 + ...onnectors.Memory.AzureCosmosDBNoSQL.csproj | 4 + .../CosmosSystemTextJSonSerializer.cs | 1 + ...NoSQLVectorStoreRecordCollectionFactory.cs | 28 + .../PineconeVectorStoreOptions.cs | 2 +- .../QdrantVectorStore.cs | 6 +- .../QdrantVectorStoreOptions.cs | 2 +- ...RedisHashSetVectorStoreRecordCollection.cs | 2 +- .../RedisJsonVectorStoreRecordCollection.cs | 2 +- .../RedisVectorStore.cs | 8 +- ...RedisVectorStoreCollectionCreateMapping.cs | 14 +- .../RedisVectorStoreOptions.cs | 2 +- .../Connectors.Memory.Sqlite/Database.cs | 35 + .../SqliteMemoryStore.cs | 29 +- .../Connectors/Connectors.Oobabooga/README.md | 3 - ...HashSetVectorStoreRecordCollectionTests.cs | 6 +- ...VectorStoreCollectionCreateMappingTests.cs | 42 +- .../Extensions/ApiManifestKernelExtensions.cs | 9 +- .../Model/RestApiOperation.cs | 37 +- .../Model/RestApiOperationServer.cs | 38 + .../Model/RestApiOperationServerVariable.cs | 49 + .../OpenApi/OpenApiDocumentParser.cs | 21 +- .../RestApiOperationExtensionsTests.cs | 2 +- .../OpenApi/OpenApiDocumentParserV20Tests.cs | 4 +- .../OpenApi/OpenApiDocumentParserV30Tests.cs | 6 +- .../OpenApi/OpenApiDocumentParserV31Tests.cs | 6 +- .../OpenApi/RestApiOperationRunnerTests.cs | 56 +- .../OpenApi/RestApiOperationTests.cs | 96 +- ...eCosmosDBMongoDBMemoryStoreTestsFixture.cs | 2 +- ...osDBMongoDBVectorStoreCollectionFixture.cs | 9 + .../AzureCosmosDBMongoDBVectorStoreFixture.cs | 129 + ...MongoDBVectorStoreRecordCollectionTests.cs | 392 + .../AzureCosmosDBMongoDBVectorStoreTests.cs | 29 + .../AzureCosmosDBNoSQLHotel.cs | 46 + ...ureCosmosDBNoSQLMemoryStoreTestsFixture.cs | 2 +- ...smosDBNoSQLVectorStoreCollectionFixture.cs | 9 + .../AzureCosmosDBNoSQLVectorStoreFixture.cs | 79 + ...DBNoSQLVectorStoreRecordCollectionTests.cs | 296 + .../AzureCosmosDBNoSQLVectorStoreTests.cs | 35 + .../CosmosSystemTextJsonSerializer.cs | 131 + .../Weaviate/WeaviateMemoryStoreTests.cs | 10 +- .../{ => Memory}/Weaviate/docker-compose.yml | 0 .../Plugins/OpenApi/repair-service.json | 6 +- dotnet/src/IntegrationTests/README.md | 5 +- dotnet/src/IntegrationTests/testsettings.json | 5 +- .../Data/RecordDefinition/IndexKind.cs | 17 + .../Functions/KernelFunction.cs | 4 +- python/.conf/packages_list.json | 14 - python/.cspell.json | 3 +- python/.env.example | 30 +- .../.pre-commit-config.yaml | 18 +- python/.vscode/tasks.json | 37 +- python/DEV_SETUP.md | 228 +- python/Makefile | 102 +- python/README.md | 19 +- python/log.txt | 0 python/mypy.ini | 1 - python/poetry.lock | 7715 ----------------- python/pyproject.toml | 314 +- .../chat_completion_function_termination.py | 133 + .../agents/mixed_chat_agents_plugins.py | 118 + .../concepts/agents/mixed_chat_files.py | 17 +- .../concepts/plugins/openapi/README.md | 4 +- python/samples/concepts/setup/ALL_SETTINGS.md | 8 + .../.env.example | 1 + .../README.md | 49 + .../main.py | 168 + .../repo_utils.py | 27 + .../telemetry_sample_settings.py | 20 + .../getting_started/00-getting-started.ipynb | 2 +- .../01-basic-loading-the-kernel.ipynb | 2 +- .../02-running-prompts-from-file.ipynb | 2 +- .../03-prompt-function-inline.ipynb | 2 +- .../04-kernel-arguments-chat.ipynb | 2 +- .../05-using-the-planner.ipynb | 2 +- .../06-memory-and-embeddings.ipynb | 4 +- .../07-hugging-face-for-plugins.ipynb | 2 +- .../08-native-function-inline.ipynb | 2 +- .../09-groundedness-checking.ipynb | 2 +- .../10-multiple-results-per-prompt.ipynb | 2 +- .../11-streaming-completions.ipynb | 2 +- .../weaviate-persistent-memory.ipynb | 2 +- python/semantic_kernel/__init__.py | 3 +- .../channels/open_ai_assistant_channel.py | 4 + .../open_ai/assistant_content_generation.py | 6 +- .../azure_ai_inference_chat_completion.py | 15 +- .../connectors/ai/function_calling_utils.py | 22 + .../services/google_ai_chat_completion.py | 11 +- .../services/vertex_ai_chat_completion.py | 11 +- .../services/open_ai_chat_completion_base.py | 40 +- .../memory/chroma/chroma_memory_store.py | 9 +- .../contents/utils/data_uri.py | 2 +- .../sessions_python_tool/README.md | 2 +- .../functions/kernel_function.py | 112 +- .../functions/kernel_function_log_messages.py | 63 + python/setup_dev.sh | 7 - ...t_chat_completion_with_function_calling.py | 11 +- .../agents/test_open_ai_assistant_base.py | 4 +- .../test_google_ai_chat_completion.py | 4 - .../test_vertex_ai_chat_completion.py | 4 - python/uv.lock | 5347 ++++++++++++ 153 files changed, 12431 insertions(+), 8770 deletions(-) delete mode 100644 .github/workflows/python-build-wheel.yml create mode 100644 docs/decisions/0051-entity-framework-as-connector.md create mode 100644 dotnet/samples/Demos/FSharpScripts/huggingFaceChatCompletion.fsx create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/.editorconfig create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBHotelModel.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBKernelBuilderExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBServiceCollectionExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreRecordCollectionTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreRecordMapperTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/Connectors.AzureCosmosDBMongoDB.UnitTests.csproj create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/.editorconfig create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLHotel.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLKernelBuilderExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLServiceCollectionExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLVectorStoreRecordCollectionTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLVectorStoreRecordMapperTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLVectorStoreTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/Connectors.AzureCosmosDBNoSQL.UnitTests.csproj create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBConstants.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBKernelBuilderExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBNamingConvention.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBServiceCollectionExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStore.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreOptions.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollection.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollectionOptions.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordMapper.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/IAzureCosmosDBMongoDBVectorStoreRecordCollectionFactory.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLCompositeKey.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLConstants.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLKernelBuilderExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLServiceCollectionExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStore.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreOptions.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollection.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollectionOptions.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordMapper.cs create mode 100644 dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/IAzureCosmosDBNoSQLVectorStoreRecordCollectionFactory.cs delete mode 100644 dotnet/src/Connectors/Connectors.Oobabooga/README.md create mode 100644 dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationServer.cs create mode 100644 dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationServerVariable.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreCollectionFixture.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreFixture.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollectionTests.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreTests.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLHotel.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreCollectionFixture.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreFixture.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollectionTests.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreTests.cs create mode 100644 dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/CosmosSystemTextJsonSerializer.cs rename dotnet/src/IntegrationTests/Connectors/{ => Memory}/Weaviate/WeaviateMemoryStoreTests.cs (96%) rename dotnet/src/IntegrationTests/Connectors/{ => Memory}/Weaviate/docker-compose.yml (100%) delete mode 100644 python/.conf/packages_list.json rename .pre-commit-config.yaml => python/.pre-commit-config.yaml (79%) delete mode 100644 python/log.txt delete mode 100644 python/poetry.lock create mode 100644 python/samples/concepts/agents/chat_completion_function_termination.py create mode 100644 python/samples/concepts/agents/mixed_chat_agents_plugins.py create mode 100644 python/samples/demos/telemetry_with_application_insights/.env.example create mode 100644 python/samples/demos/telemetry_with_application_insights/README.md create mode 100644 python/samples/demos/telemetry_with_application_insights/main.py create mode 100644 python/samples/demos/telemetry_with_application_insights/repo_utils.py create mode 100644 python/samples/demos/telemetry_with_application_insights/telemetry_sample_settings.py create mode 100644 python/semantic_kernel/functions/kernel_function_log_messages.py delete mode 100644 python/setup_dev.sh create mode 100644 python/uv.lock diff --git a/.github/workflows/python-build-wheel.yml b/.github/workflows/python-build-wheel.yml deleted file mode 100644 index 5752dee8ace9..000000000000 --- a/.github/workflows/python-build-wheel.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: python-build-wheel - -on: - workflow_dispatch: - push: - branches: [ "python_preview" ] - -permissions: - contents: read - -jobs: - build-wheel: - - runs-on: ubuntu-latest - - defaults: - run: - working-directory: python - - steps: - - uses: actions/checkout@v4 - - - run: echo "/root/.local/bin" >> $GITHUB_PATH - - - name: Install poetry - run: pipx install poetry - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - cache: "poetry" - cache-dependency-path: "python/pyproject.toml" - - - name: Install Semantic Kernel - run: poetry install --no-ansi - - - name: Build wheel - run: poetry build - - - name: Upload wheel file to artifacts - uses: actions/upload-artifact@v4 - with: - name: dist - path: python/dist/* diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml index 1b1a44fa1bb6..38b31dbfa847 100644 --- a/.github/workflows/python-integration-tests.yml +++ b/.github/workflows/python-integration-tests.yml @@ -16,6 +16,10 @@ on: permissions: contents: read +env: + # Configure a constant location for the uv cache + UV_CACHE_DIR: /tmp/.uv-cache + jobs: paths-filter: runs-on: ubuntu-latest @@ -49,27 +53,37 @@ jobs: matrix: python-version: ["3.11"] os: [ubuntu-latest] + defaults: + run: + working-directory: python steps: - uses: actions/checkout@v4 - - name: Install poetry - run: pipx install poetry + - name: Set up uv + if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' }} + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Set up uv + if: ${{ matrix.os == 'windows-latest' }} + run: irm https://astral.sh/uv/install.ps1 | iex + shell: powershell - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: "poetry" + - name: Restore uv cache + id: cache + uses: actions/cache@v4 + with: + path: ${{ env.UV_CACHE_DIR }} + key: uv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/uv.lock') }} - name: Install dependencies with hnswlib native disabled if: matrix.os == 'macos-latest' && matrix.python-version == '3.11' run: | export HNSWLIB_NO_NATIVE=1 - python -m pip install --upgrade pip setuptools wheel - cd python && poetry install --with tests + uv sync --all-extras --dev - name: Install dependencies with hnswlib native enabled if: matrix.os != 'macos-latest' || matrix.python-version != '3.11' run: | - python -m pip install --upgrade pip setuptools wheel - cd python - poetry install --with tests + uv sync --all-extras --dev - name: Install Ollama if: matrix.os == 'ubuntu-latest' run: | @@ -98,7 +112,7 @@ jobs: - name: Run Integration Tests id: run_tests shell: bash - env: # Set Azure credentials secret as an input + env: HNSWLIB_NO_NATIVE: 1 Python_Integration_Tests: Python_Integration_Tests AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }} # azure-text-embedding-ada-002 @@ -135,26 +149,19 @@ jobs: VERTEX_AI_GEMINI_MODEL_ID: ${{ vars.VERTEX_AI_GEMINI_MODEL_ID }} VERTEX_AI_EMBEDDING_MODEL_ID: ${{ vars.VERTEX_AI_EMBEDDING_MODEL_ID }} REDIS_CONNECTION_STRING: ${{ vars.REDIS_CONNECTION_STRING }} - run: | - cd python - poetry run pytest -n logical --dist loadfile --dist worksteal ./tests/integration ./tests/samples -v --junitxml=pytest.xml + run: | + uv run pytest -n logical --dist loadfile --dist worksteal ./tests/integration ./tests/samples -v --junitxml=pytest.xml - name: Surface failing tests if: always() uses: pmeier/pytest-results-action@main with: - # A list of JUnit XML files, directories containing the former, and wildcard - # patterns to process. - # See @actions/glob for supported patterns. path: python/pytest.xml - # (Optional) Add a summary of the results at the top of the report summary: true - # (Optional) Select which results should be included in the report. - # Follows the same syntax as `pytest -r` display-options: fEX - # (Optional) Fail the workflow if no JUnit XML was found. fail-on-empty: true - # (Optional) Title of the test results section in the workflow summary title: Test results + - name: Minimize uv cache + run: uv cache prune --ci python-integration-tests: needs: paths-filter @@ -166,54 +173,66 @@ jobs: matrix: python-version: ["3.10", "3.11", "3.12"] os: [ubuntu-latest, windows-latest, macos-latest] + defaults: + run: + working-directory: python steps: - uses: actions/checkout@v4 - - name: Install poetry - run: pipx install poetry + - name: Set up uv + if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' }} + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Set up uv + if: ${{ matrix.os == 'windows-latest' }} + run: irm https://astral.sh/uv/install.ps1 | iex + shell: powershell - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: "poetry" + - name: Restore uv cache + id: cache + uses: actions/cache@v4 + with: + path: ${{ env.UV_CACHE_DIR }} + key: uv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/uv.lock') }} - name: Install dependencies with hnswlib native disabled if: matrix.os == 'macos-latest' && matrix.python-version == '3.11' run: | export HNSWLIB_NO_NATIVE=1 - python -m pip install --upgrade pip setuptools wheel - cd python && poetry install --with tests - + uv sync --all-extras --dev - name: Install dependencies with hnswlib native enabled if: matrix.os != 'macos-latest' || matrix.python-version != '3.11' run: | - python -m pip install --upgrade pip setuptools wheel - cd python && poetry install --with tests - + uv sync --all-extras --dev - name: Install Ollama if: matrix.os == 'ubuntu-latest' run: | - curl -fsSL https://ollama.com/install.sh | sh - ollama serve & - sleep 5 - + if ${{ vars.OLLAMA_MODEL != '' }}; then + curl -fsSL https://ollama.com/install.sh | sh + ollama serve & + sleep 5 + fi - name: Pull model in Ollama if: matrix.os == 'ubuntu-latest' run: | - ollama pull ${{ vars.OLLAMA_MODEL }} - ollama list - + if ${{ vars.OLLAMA_MODEL != '' }}; then + ollama pull ${{ vars.OLLAMA_MODEL }} + ollama list + fi - name: Google auth uses: google-github-actions/auth@v2 with: project_id: ${{ vars.VERTEX_AI_PROJECT_ID }} credentials_json: ${{ secrets.VERTEX_AI_SERVICE_ACCOUNT_KEY }} - - name: Set up gcloud - uses: google-github-actions/setup-gcloud@v2 - + uses: google-github-actions/setup-gcloud@v2 + - name: Setup Redis Stack Server + if: matrix.os == 'ubuntu-latest' + run: docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest - name: Run Integration Tests id: run_tests shell: bash - env: # Set Azure credentials secret as an input + env: HNSWLIB_NO_NATIVE: 1 Python_Integration_Tests: Python_Integration_Tests AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }} # azure-text-embedding-ada-002 @@ -240,6 +259,8 @@ jobs: MISTRALAI_API_KEY: ${{secrets.MISTRALAI_API_KEY}} MISTRALAI_CHAT_MODEL_ID: ${{ vars.MISTRALAI_CHAT_MODEL_ID }} MISTRALAI_EMBEDDING_MODEL_ID: ${{ vars.MISTRALAI_EMBEDDING_MODEL_ID }} + ANTHROPIC_API_KEY: ${{secrets.ANTHROPIC_API_KEY}} + ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }} OLLAMA_MODEL: "${{ matrix.os == 'ubuntu-latest' && vars.OLLAMA_MODEL || '' }}" # phi3 GOOGLE_AI_GEMINI_MODEL_ID: ${{ vars.GOOGLE_AI_GEMINI_MODEL_ID }} GOOGLE_AI_EMBEDDING_MODEL_ID: ${{ vars.GOOGLE_AI_EMBEDDING_MODEL_ID }} @@ -248,16 +269,19 @@ jobs: VERTEX_AI_GEMINI_MODEL_ID: ${{ vars.VERTEX_AI_GEMINI_MODEL_ID }} VERTEX_AI_EMBEDDING_MODEL_ID: ${{ vars.VERTEX_AI_EMBEDDING_MODEL_ID }} REDIS_CONNECTION_STRING: ${{ vars.REDIS_CONNECTION_STRING }} - ANTHROPIC_API_KEY: ${{secrets.ANTHROPIC_API_KEY}} - ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }} run: | - if ${{ matrix.os == 'ubuntu-latest' }}; then - docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest - fi - - cd python - poetry run pytest -n logical --dist loadfile --dist worksteal ./tests/integration -v - poetry run pytest -n logical --dist loadfile --dist worksteal ./tests/samples -v + uv run pytest -n logical --dist loadfile --dist worksteal ./tests/integration ./tests/samples -v --junitxml=pytest.xml + - name: Surface failing tests + if: always() + uses: pmeier/pytest-results-action@main + with: + path: python/pytest.xml + summary: true + display-options: fEX + fail-on-empty: true + title: Test results + - name: Minimize uv cache + run: uv cache prune --ci # This final job is required to satisfy the merge queue. It must only run (or succeed) if no tests failed python-integration-tests-check: diff --git a/.github/workflows/python-lint.yml b/.github/workflows/python-lint.yml index 3f20ae2f0d02..39549589e69f 100644 --- a/.github/workflows/python-lint.yml +++ b/.github/workflows/python-lint.yml @@ -15,14 +15,29 @@ jobs: python-version: ["3.10"] runs-on: ubuntu-latest continue-on-error: true + defaults: + run: + working-directory: python + env: + # Configure a constant location for the uv cache + UV_CACHE_DIR: /tmp/.uv-cache steps: - uses: actions/checkout@v4 - - name: Install poetry - run: pipx install poetry - - uses: actions/setup-python@v5 + - name: Set up uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: "poetry" - - name: Install dependencies - run: cd python && poetry install + - name: Restore uv cache + uses: actions/cache@v4 + with: + path: ${{ env.UV_CACHE_DIR }} + key: uv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/uv.lock') }} + - name: Install the project + run: uv sync --all-extras --dev - uses: pre-commit/action@v3.0.1 + with: + extra_args: --config python/.pre-commit-config.yaml + - name: Minimize uv cache + run: uv cache prune --ci diff --git a/.github/workflows/python-unit-tests.yml b/.github/workflows/python-unit-tests.yml index 4137270c3796..85e7f8090e3e 100644 --- a/.github/workflows/python-unit-tests.yml +++ b/.github/workflows/python-unit-tests.yml @@ -5,6 +5,9 @@ on: branches: ["main", "feature*"] paths: - "python/**" +env: + # Configure a constant location for the uv cache + UV_CACHE_DIR: /tmp/.uv-cache jobs: python-unit-tests: @@ -28,34 +31,38 @@ jobs: working-directory: python steps: - uses: actions/checkout@v4 - - name: Install poetry - run: pipx install poetry + - name: Set up uv + if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' }} + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Set up uv + if: ${{ matrix.os == 'windows-latest' }} + run: irm https://astral.sh/uv/install.ps1 | iex + shell: powershell - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: "poetry" - - name: Install dependencies - run: poetry install --with unit-tests + - name: Restore uv cache + id: cache + uses: actions/cache@v4 + with: + path: ${{ env.UV_CACHE_DIR }} + key: uv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/uv.lock') }} + - name: Install the project + run: uv sync --all-extras --dev - name: Test with pytest - run: poetry run pytest --junitxml=pytest.xml ./tests/unit + run: uv run pytest --junitxml=pytest.xml ./tests/unit - name: Surface failing tests if: always() uses: pmeier/pytest-results-action@main with: - # A list of JUnit XML files, directories containing the former, and wildcard - # patterns to process. - # See @actions/glob for supported patterns. path: python/pytest.xml - # (Optional) Add a summary of the results at the top of the report summary: true - # (Optional) Select which results should be included in the report. - # Follows the same syntax as `pytest -r` display-options: fEX - # (Optional) Fail the workflow if no JUnit XML was found. fail-on-empty: true - # (Optional) Title of the test results section in the workflow summary title: Test results + - name: Minimize uv cache + run: uv cache prune --ci python-test-coverage: name: Python Test Coverage runs-on: [ubuntu-latest] @@ -65,21 +72,27 @@ jobs: defaults: run: working-directory: python + env: + PYTHON_VERSION: "3.10" steps: - uses: actions/checkout@v4 - name: Setup filename variables run: echo "FILE_ID=${{ github.event.number }}" >> $GITHUB_ENV - - name: Install poetry - run: pipx install poetry - - name: Set up Python 3.10 + - name: Set up uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v5 with: - python-version: "3.10" - cache: "poetry" - - name: Install dependencies - run: poetry install --with unit-tests + python-version: ${{ env.PYTHON_VERSION }} + - name: Restore uv cache + uses: actions/cache@v4 + with: + path: ${{ env.UV_CACHE_DIR }} + key: uv-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/uv.lock') }} + - name: Install the project + run: uv sync --all-extras --dev - name: Test with pytest - run: poetry run pytest -q --junitxml=pytest.xml --cov=semantic_kernel --cov-report=term-missing:skip-covered ./tests/unit | tee python-coverage.txt + run: uv run pytest -q --junitxml=pytest.xml --cov=semantic_kernel --cov-report=term-missing:skip-covered ./tests/unit | tee python-coverage.txt - name: Upload coverage if: always() uses: actions/upload-artifact@v4 @@ -96,3 +109,5 @@ jobs: path: python/pytest.xml overwrite: true retention-days: 1 + - name: Minimize uv cache + run: uv cache prune --ci diff --git a/.gitignore b/.gitignore index d37a856dbc26..3912623b85f8 100644 --- a/.gitignore +++ b/.gitignore @@ -461,6 +461,7 @@ env/ venv/ myvenv/ ENV/ +.venv*/ # Python dist dist/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 91ff88105299..28d543bbc52b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -8,7 +8,10 @@ "label": "setup (contributing-R#)", "detail": "", "group": "build", - "dependsOn": ["new tool-manifest", "# Setup"], + "dependsOn": [ + "new tool-manifest", + "# Setup" + ], "dependsOrder": "sequence" }, { @@ -16,7 +19,10 @@ "detail": "Install ReSharper Global Tools", "command": "dotnet", "type": "process", - "args": ["new", "tool-manifest"], + "args": [ + "new", + "tool-manifest" + ], "options": { "cwd": "${workspaceFolder}/dotnet" } @@ -88,54 +94,6 @@ ], "dependsOrder": "sequence" }, - // ***************************** - // Contributing (python) - Setup - // ***************************** - { - "label": "setup (contributing-python)", - "detail": "", - "group": "build", - "dependsOn": ["install poetry", "install python packages"], - "dependsOrder": "sequence" - }, - { - "label": "install poetry", - "detail": "Install poetry", - "command": "pip3", - "type": "shell", - "args": ["install", "poetry"], - "options": { - "cwd": "${workspaceFolder}/python" - } - }, - { - "label": "install python packages", - "detail": "Install python packages", - "command": "poetry", - "type": "shell", - "args": ["install"], - "options": { - "cwd": "${workspaceFolder}/python" - } - }, - // Formatting - { - "label": "validate (contributing-python)", - "command": "poetry", - "type": "shell", - "group": "build", - "args": [ - "run", - "pre-commit", - "run", - "-c", - ".conf/.pre-commit-config.yaml", - "-a" - ], - "options": { - "cwd": "${workspaceFolder}/python" - } - }, // *************** // Kernel (dotnet) // *************** @@ -163,7 +121,10 @@ "label": "test (Semantic-Kernel)", "command": "dotnet", "type": "process", - "args": ["test", "SemanticKernel.UnitTests.csproj"], + "args": [ + "test", + "SemanticKernel.UnitTests.csproj" + ], "problemMatcher": "$msCompile", "group": "test", "presentation": { @@ -271,58 +232,6 @@ } }, // **************** - // Kernel (python) - // **************** - // Test - { - "label": "test (Semantic-Kernel-Python)", - "command": "poetry", - "type": "shell", - "args": ["run", "pytest", "tests/unit"], - "problemMatcher": "$msCompile", - "group": "test", - "presentation": { - "reveal": "always", - "panel": "shared", - "group": "PR-Validate" - }, - "options": { - "cwd": "${workspaceFolder}/python" - } - }, - { - "label": "test (Semantic-Kernel-Python Integration)", - "command": "poetry", - "type": "shell", - "args": ["run", "pytest", "tests/integration", "-k", "${input:filter}"], - "problemMatcher": "$msCompile", - "group": "test", - "presentation": { - "reveal": "always", - "panel": "shared", - "group": "PR-Validate" - }, - "options": { - "cwd": "${workspaceFolder}/python" - } - }, - { - "label": "test (Semantic-Kernel-Python ALL)", - "command": "poetry", - "type": "shell", - "args": ["run", "pytest", "tests", "-k", "${input:filter}"], - "problemMatcher": "$msCompile", - "group": "test", - "presentation": { - "reveal": "always", - "panel": "shared", - "group": "PR-Validate" - }, - "options": { - "cwd": "${workspaceFolder}/python" - } - }, - // **************** // Samples (dotnet) // **************** // Kernel Syntax Examples @@ -380,4 +289,4 @@ "description": "Enter a filter to pass as argument or filter" } ] -} +} \ No newline at end of file diff --git a/README.md b/README.md index be235ed20926..f24c54591367 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Semantic Kernel was designed to be future proof, easily connecting your code to ## Getting started with Semantic Kernel -The Semantic Kernel SDK is available in C#, Python, and Java. To get started, choose your preferred language below. See the [Feature Matrix](https://learn.microsoft.com/en-us/semantic-kernel/get-started/supported-languages) to see a breakdown of +The Semantic Kernel SDK is available in C#, Python, and Java. To get started, choose your preferred language below. See the [Feature Matrix](https://learn.microsoft.com/en-us/semantic-kernel/get-started/supported-languages) for a breakdown of feature parity between our currently supported languages. diff --git a/docs/decisions/0051-entity-framework-as-connector.md b/docs/decisions/0051-entity-framework-as-connector.md new file mode 100644 index 000000000000..f4a5b8f5d9b7 --- /dev/null +++ b/docs/decisions/0051-entity-framework-as-connector.md @@ -0,0 +1,119 @@ +--- +# These are optional elements. Feel free to remove any of them. +status: proposed +contact: dmytrostruk +date: 2024-08-20 +deciders: sergeymenshykh, markwallace, rbarreto, westey-m +--- + +# Entity Framework as Vector Store Connector + +## Context and Problem Statement + +This ADR contains investigation results about adding Entity Framework as Vector Store connector to the Semantic Kernel codebase. + +Entity Framework is a modern object-relation mapper that allows to build a clean, portable, and high-level data access layer with .NET (C#) across a variety of databases, including SQL Database (on-premises and Azure), SQLite, MySQL, PostgreSQL, Azure Cosmos DB and more. It supports LINQ queries, change tracking, updates and schema migrations. + +One of the huge benefits of Entity Framework for Semantic Kernel is the support of multiple databases. In theory, one Entity Framework connector can work as a hub to multiple databases at the same time, which should simplify the development and maintenance of integration with these databases. + +However, there are some limitations, which won't allow Entity Framework to fit in updated Vector Store design. + +### Collection Creation + +In new Vector Store design, interface `IVectorStoreRecordCollection` contains methods to manipulate with database collections: +- `CollectionExistsAsync` +- `CreateCollectionAsync` +- `CreateCollectionIfNotExistsAsync` +- `DeleteCollectionAsync` + +In Entity Framework, collection (also known as schema/table) creation using programmatic approach is not recommended in production scenarios. The recommended approach is to use Migrations (in case of code-first approach), or to use Reverse Engineering (also known as scaffolding/database-first approach). Programmatic schema creation is recommended only for testing/local scenarios. Also, collection creation process differs for different databases. For example, MongoDB EF Core provider doesn't support schema migrations or database-first/model-first approaches. Instead, the collection is created automatically when a document is inserted for the first time, if collection doesn't already exist. This brings the complexity around methods such as `CreateCollectionAsync` from `IVectorStoreRecordCollection` interface, since there is no abstraction around collection management in EF that will work for most databases. For such cases, the recommended approach is to rely on automatic creation or handle collection creation individually for each database. As an example, in MongoDB it's recommended to use MongoDB C# Driver directly. + +Sources: +- https://learn.microsoft.com/en-us/ef/core/managing-schemas/ +- https://learn.microsoft.com/en-us/ef/core/managing-schemas/ensure-created +- https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying?tabs=dotnet-core-cli#apply-migrations-at-runtime +- https://github.com/mongodb/mongo-efcore-provider?tab=readme-ov-file#not-supported--out-of-scope-features + +### Key Management + +It won't be possible to define one set of valid key types, since not all databases support all types as keys. In such case, it will be possible to support only standard type for keys such as `string`, and then the conversion should be performed to satisfy key restrictions for specific database. This removes the advantage of unified connector implementation, since key management should be handled for each database individually. + +Sources: +- https://learn.microsoft.com/en-us/ef/core/modeling/keys?tabs=data-annotations + +### Vector Management + +`ReadOnlyMemory` type, which is used in most SK connectors today to hold embeddings is not supported in Entity Framework out-of-the-box. When trying to use this type, the following error occurs: + +``` +The property '{Property Name}' could not be mapped because it is of type 'ReadOnlyMemory?', which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'. +``` + +However, it's possible to use `byte[]` type or create explicit mapping to support `ReadOnlyMemory`. It's already implemented in `pgvector` package, but it's not clear whether it will work with different databases. + +Sources: +- https://github.com/pgvector/pgvector-dotnet/blob/master/README.md#entity-framework-core +- https://github.com/pgvector/pgvector-dotnet/blob/master/src/Pgvector/Vector.cs +- https://github.com/pgvector/pgvector-dotnet/blob/master/src/Pgvector.EntityFrameworkCore/VectorTypeMapping.cs + +### Testing + +Create Entity Framework connector and write the tests using SQLite database doesn't mean that this integration will work for other EF-supported databases. Each database implements its own set of Entity Framework features, so in order to ensure that Entity Framework connector covers main use-cases with specific database, unit/integration tests should be added using each database separately. + +Sources: +- https://github.com/mongodb/mongo-efcore-provider?tab=readme-ov-file#supported-features + +### Compatibility + +It's not possible to use latest Entity Framework Core package and develop it for .NET Standard. Last version of EF Core which supports .NET Standard was version 5.0 (latest EF Core version is 8.0). Which means that Entity Framework connector can target .NET 8.0 only (which is different from other available SK connectors today, which target both net8.0 and netstandard2.0). + +Another way would be to use Entity Framework 6, which can target both net8.0 and netstandard2.0, but this version of Entity Framework is no longer being actively developed. Entity Framework Core offers new features that won't be implemented in EF6. + +Sources: +- https://learn.microsoft.com/en-us/ef/core/miscellaneous/platforms +- https://learn.microsoft.com/en-us/ef/efcore-and-ef6/ + +### Existence of current SK connectors + +Taking into account that Semantic Kernel already has some integration with databases, which are also supported Entity Framework, there are multiple options how to proceed: +- Support both Entity Framework and DB connector (e.g. `Microsoft.SemanticKernel.Connectors.EntityFramework` and `Microsoft.SemanticKernel.Connectors.MongoDB`) - in this case both connectors should produce exactly the same outcome, so additional work will be required (such as implementing the same set of unit/integration tests) to ensure this state. Also, any modifications to the logic should be applied in both connectors. +- Support just one Entity Framework connector (e.g. `Microsoft.SemanticKernel.Connectors.EntityFramework`) - in this case, existing DB connector should be removed, which may be a breaking change to existing customers. An additional work will be required to ensure that Entity Framework covers exactly the same set of features as previous DB connector. +- Support just one DB connector (e.g. `Microsoft.SemanticKernel.Connectors.MongoDB`) - in this case, if such connector already exists - no additional work is required. If such connector doesn't exist and it's important to add it - additional work is required to implement that DB connector. + + +Table with Entity Framework and Semantic Kernel database support (only for databases which support vector search): + +|Database Engine|Maintainer / Vendor|Supported in EF|Supported in SK|Updated to SK memory v2 design +|-|-|-|-|-| +|Azure Cosmos|Microsoft|Yes|Yes|Yes| +|Azure SQL and SQL Server|Microsoft|Yes|Yes|No| +|SQLite|Microsoft|Yes|Yes|No| +|PostgreSQL|Npgsql Development Team|Yes|Yes|No| +|MongoDB|MongoDB|Yes|Yes|No| +|MySQL|Oracle|Yes|No|No| +|Oracle DB|Oracle|Yes|No|No| +|Google Cloud Spanner|Cloud Spanner Ecosystem|Yes|No|No| + +**Note**: +One database engine can have multiple Entity Framework integrations, which can be maintained by different vendors (e.g. there are 2 MySQL EF NuGet packages - one is maintained by Oracle and another one is maintained by Pomelo Foundation Project). + +Vector DB connectors which are additionally supported in Semantic Kernel: +- Azure AI Search +- Chroma +- Milvus +- Pinecone +- Qdrant +- Redis +- Weaviate + +Sources: +- https://learn.microsoft.com/en-us/ef/core/providers/?tabs=dotnet-core-cli#current-providers + +## Considered Options + +- Add new `Microsoft.SemanticKernel.Connectors.EntityFramework` connector. +- Do not add `Microsoft.SemanticKernel.Connectors.EntityFramework` connector, but add a new connector for individual database when needed. + +## Decision Outcome + +Based on the above investigation, the decision is not to add Entity Framework connector, but to add a new connector for individual database when needed. The reason for this decision is that Entity Framework providers do not uniformly support collection management operations and will require database specific code for key handling and object mapping. These factors will make use of an Entity Framework connector unreliable and it will not abstract away the underlying database. Additionally the number of vector databases that Entity Framework supports that Semantic Kernel does not have a memory connector for is very small. diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 7a07062811db..07663ec833e5 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -28,7 +28,7 @@ - + @@ -53,14 +53,14 @@ - + - + @@ -99,7 +99,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index b4580b4d1146..cb04656ffb01 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -62,6 +62,8 @@ 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}" @@ -90,7 +92,6 @@ 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.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 @@ -277,7 +278,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}" @@ -304,65 +314,23 @@ 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.OpenAI", "src\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj", "{8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}" -EndProject -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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Redis.UnitTests", "src\Connectors\Connectors.Redis.UnitTests\Connectors.Redis.UnitTests.csproj", "{1D4667B9-9381-4E32-895F-123B94253EE8}" 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 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Qdrant.UnitTests", "src\Connectors\Connectors.Qdrant.UnitTests\Connectors.Qdrant.UnitTests.csproj", "{E92AE954-8F3A-4A6F-A4F9-DC12017E5AAF}" 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 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{38374C62-0263-4FE8-A18C-70FC8132912B}" 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.AzureCosmosDBMongoDB.UnitTests", "src\Connectors\Connectors.AzureCosmosDBMongoDB.UnitTests\Connectors.AzureCosmosDBMongoDB.UnitTests.csproj", "{2918478E-BC86-4D53-9D01-9C318F80C14F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Qdrant.UnitTests", "src\Connectors\Connectors.Qdrant.UnitTests\Connectors.Qdrant.UnitTests.csproj", "{8642A03F-D840-4B2E-B092-478300000F83}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIModelRouter", "samples\Demos\AIModelRouter\AIModelRouter.csproj", "{E06818E3-00A5-41AC-97ED-9491070CDEA1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Redis.UnitTests", "src\Connectors\Connectors.Redis.UnitTests\Connectors.Redis.UnitTests.csproj", "{ACD8C464-AEC9-45F6-A458-50A84F353DB7}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.AzureCosmosDBNoSQL.UnitTests", "src\Connectors\Connectors.AzureCosmosDBNoSQL.UnitTests\Connectors.AzureCosmosDBNoSQL.UnitTests.csproj", "{385A8FE5-87E2-4458-AE09-35E10BD2E67F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AIModelRouter", "samples\Demos\AIModelRouter\AIModelRouter.csproj", "{E06818E3-00A5-41AC-97ED-9491070CDEA1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.OpenAI.UnitTests", "src\Connectors\Connectors.OpenAI.UnitTests\Connectors.OpenAI.UnitTests.csproj", "{36DDC119-C030-407E-AC51-A877E9E0F660}" 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 -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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.AzureOpenAI", "src\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj", "{7AAD7388-307D-41FB-B80A-EF9E3A4E31F0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.AzureOpenAI.UnitTests", "src\Connectors\Connectors.AzureOpenAI.UnitTests\Connectors.AzureOpenAI.UnitTests.csproj", "{8CF06B22-50F3-4F71-A002-622DB49DF0F5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -455,6 +423,12 @@ 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 @@ -815,66 +789,60 @@ 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 = 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 - {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 - {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 - {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 - {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 - {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 + {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 + {2918478E-BC86-4D53-9D01-9C318F80C14F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2918478E-BC86-4D53-9D01-9C318F80C14F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2918478E-BC86-4D53-9D01-9C318F80C14F}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {2918478E-BC86-4D53-9D01-9C318F80C14F}.Publish|Any CPU.Build.0 = Debug|Any CPU + {2918478E-BC86-4D53-9D01-9C318F80C14F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2918478E-BC86-4D53-9D01-9C318F80C14F}.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 {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Publish|Any CPU.Build.0 = Debug|Any CPU {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Release|Any CPU.Build.0 = 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 - {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 + {385A8FE5-87E2-4458-AE09-35E10BD2E67F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {385A8FE5-87E2-4458-AE09-35E10BD2E67F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {385A8FE5-87E2-4458-AE09-35E10BD2E67F}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {385A8FE5-87E2-4458-AE09-35E10BD2E67F}.Publish|Any CPU.Build.0 = Debug|Any CPU + {385A8FE5-87E2-4458-AE09-35E10BD2E67F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {385A8FE5-87E2-4458-AE09-35E10BD2E67F}.Release|Any CPU.Build.0 = Release|Any CPU + {36DDC119-C030-407E-AC51-A877E9E0F660}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {36DDC119-C030-407E-AC51-A877E9E0F660}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36DDC119-C030-407E-AC51-A877E9E0F660}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {36DDC119-C030-407E-AC51-A877E9E0F660}.Publish|Any CPU.Build.0 = Debug|Any CPU + {36DDC119-C030-407E-AC51-A877E9E0F660}.Release|Any CPU.ActiveCfg = Release|Any CPU + {36DDC119-C030-407E-AC51-A877E9E0F660}.Release|Any CPU.Build.0 = Release|Any CPU + {7AAD7388-307D-41FB-B80A-EF9E3A4E31F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AAD7388-307D-41FB-B80A-EF9E3A4E31F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AAD7388-307D-41FB-B80A-EF9E3A4E31F0}.Publish|Any CPU.ActiveCfg = Publish|Any CPU + {7AAD7388-307D-41FB-B80A-EF9E3A4E31F0}.Publish|Any CPU.Build.0 = Publish|Any CPU + {7AAD7388-307D-41FB-B80A-EF9E3A4E31F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AAD7388-307D-41FB-B80A-EF9E3A4E31F0}.Release|Any CPU.Build.0 = Release|Any CPU + {8CF06B22-50F3-4F71-A002-622DB49DF0F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CF06B22-50F3-4F71-A002-622DB49DF0F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CF06B22-50F3-4F71-A002-622DB49DF0F5}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {8CF06B22-50F3-4F71-A002-622DB49DF0F5}.Publish|Any CPU.Build.0 = Debug|Any CPU + {8CF06B22-50F3-4F71-A002-622DB49DF0F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CF06B22-50F3-4F71-A002-622DB49DF0F5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -897,6 +865,7 @@ 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} @@ -960,7 +929,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} = {F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098} + {E6204E79-EFBF-499E-9743-85199310A455} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {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} @@ -980,23 +949,15 @@ 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} - {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} - {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} + {2918478E-BC86-4D53-9D01-9C318F80C14F} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} {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} + {385A8FE5-87E2-4458-AE09-35E10BD2E67F} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} + {36DDC119-C030-407E-AC51-A877E9E0F660} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} + {7AAD7388-307D-41FB-B80A-EF9E3A4E31F0} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} + {8CF06B22-50F3-4F71-A002-622DB49DF0F5} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/SK-dotnet.sln.DotSettings b/dotnet/SK-dotnet.sln.DotSettings index 091a6854bc6b..d8964e230315 100644 --- a/dotnet/SK-dotnet.sln.DotSettings +++ b/dotnet/SK-dotnet.sln.DotSettings @@ -221,7 +221,6 @@ public void It$SOMENAME$() True True True - True True True True diff --git a/dotnet/samples/Demos/FSharpScripts/huggingFaceChatCompletion.fsx b/dotnet/samples/Demos/FSharpScripts/huggingFaceChatCompletion.fsx new file mode 100644 index 000000000000..60c35ac38598 --- /dev/null +++ b/dotnet/samples/Demos/FSharpScripts/huggingFaceChatCompletion.fsx @@ -0,0 +1,86 @@ +#r "nuget: Microsoft.Extensions.DependencyInjection" +#r "nuget: Microsoft.Extensions.Http" +#r "nuget: Microsoft.Extensions.Logging.Console" +#r "nuget: Microsoft.Extensions.Logging" +#r "nuget: Microsoft.SemanticKernel.Connectors.HuggingFace, 1.12.0-preview" + + +open Microsoft.SemanticKernel +open Microsoft.SemanticKernel.ChatCompletion +open Microsoft.Extensions.Logging +open System +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Http.Logging +open System.Net.Http +open System.Net.Http.Json +open Microsoft.Extensions.Http +open System.Threading.Tasks + +let builder = + // TODO: request your API key in your 🤗 hugging face private settings + let API_KEY = "TODO_REPLACE_ME" + let MODEL_ID = "microsoft/Phi-3-mini-4k-instruct" // pick the model you prefer! + let API_URL = $"https://api-inference.huggingface.co/" + + let b = Kernel.CreateBuilder().AddHuggingFaceChatCompletion( + MODEL_ID, + API_URL |> Uri, + API_KEY) + + b.Services + .AddLogging(fun b -> + + b.AddFilter("System.Net.Http.HttpClient", + LogLevel.Debug) |> ignore + b.AddFilter("Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware", + LogLevel.Debug) |> ignore + + b.AddConsole() |> ignore + b.SetMinimumLevel(LogLevel.Information) |> ignore + + |> ignore + )|> ignore + + b + +let kernel = builder.Build() + +let chatCompletion = + kernel.GetRequiredService() + +let chatHistory = + new ChatHistory(""" + You are an expert in F#, dotnet, aspnet and .fsx and scripting with nuget! + always reply in this example format for conversations + + question: `how do i declare a record in F#?` + --- + answer: + ```fsharp + type Car = { Brand: string } + ``` + + try to keep answers as short and relevant as possible, if you do NOT know, + ASK for more details to the user and wait for the next input + """) + +let mutable exit = false + +while not exit do + printfn "I am an F# assistant, ask me anything!" + + let question = System.Console.ReadLine() + chatHistory.Add(new ChatMessageContent(AuthorRole.Assistant, question)) + + let result = + chatCompletion.GetChatMessageContentAsync(chatHistory) + |> Async.AwaitTask + |> Async.RunSynchronously + + Console.WriteLine(result.Role) + Console.WriteLine(result.Content) + + printfn "another round? y/n" + printfn "\r\n" + let reply = Console.ReadKey() + exit <- reply.KeyChar.ToString().ToLower() <> "y" diff --git a/dotnet/samples/Demos/TelemetryWithAppInsights/README.md b/dotnet/samples/Demos/TelemetryWithAppInsights/README.md index 0194af9dc0ef..f177f98fc43d 100644 --- a/dotnet/samples/Demos/TelemetryWithAppInsights/README.md +++ b/dotnet/samples/Demos/TelemetryWithAppInsights/README.md @@ -1,6 +1,6 @@ # Semantic Kernel Telemetry with AppInsights -This example project shows how an application can be configured to send Semantic Kernel telemetry to Application Insights. +This sample project shows how a .Net application can be configured to send Semantic Kernel telemetry to Application Insights. > Note that it is also possible to use other Application Performance Management (APM) vendors. An example is [Prometheus](https://prometheus.io/docs/introduction/overview/). Please refer to this [link](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-collection#configure-the-example-app-to-use-opentelemetrys-prometheus-exporter) on how to do it. @@ -16,7 +16,7 @@ For more information, please refer to the following articles: ## What to expect -The Semantic Kernel SDK is designed to efficiently generate comprehensive logs, traces, and metrics throughout the flow of function execution and model invocation. This allows you to effectively monitor your AI application's performance and accurately track token consumption. +The Semantic Kernel .Net SDK is designed to efficiently generate comprehensive logs, traces, and metrics throughout the flow of function execution and model invocation. This allows you to effectively monitor your AI application's performance and accurately track token consumption. > `ActivitySource.StartActivity` internally determines if there are any listeners recording the Activity. If there are no registered listeners or there are listeners that are not interested, StartActivity() will return null and avoid creating the Activity object. Read more [here](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs). @@ -74,7 +74,7 @@ dotnet user-secrets set "MistralAI:ApiKey" "..." dotnet user-secrets set "ApplicationInsights:ConnectionString" "..." ``` -## Running the example +## Running the sample Simply run `dotnet run` under this directory if the command line interface is preferred. Otherwise, this example can also be run in Visual Studio. diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/.editorconfig b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/.editorconfig new file mode 100644 index 000000000000..394eef685f21 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.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.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBHotelModel.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBHotelModel.cs new file mode 100644 index 000000000000..6d7db223d41b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBHotelModel.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.SemanticKernel.Data; + +namespace SemanticKernel.Connectors.AzureCosmosDBMongoDB.UnitTests; + +public class AzureCosmosDBMongoDBHotelModel(string hotelId) +{ + /// The key of the record. + [VectorStoreRecordKey] + public string HotelId { get; init; } = hotelId; + + /// A string metadata field. + [VectorStoreRecordData] + public string? HotelName { get; set; } + + /// An int metadata field. + [VectorStoreRecordData] + public int HotelCode { get; set; } + + /// A float metadata field. + [VectorStoreRecordData] + public float? HotelRating { get; set; } + + /// A bool metadata field. + [VectorStoreRecordData(StoragePropertyName = "parking_is_included")] + public bool ParkingIncluded { get; set; } + + /// An array metadata field. + [VectorStoreRecordData] + public List Tags { get; set; } = []; + + /// A data field. + [VectorStoreRecordData] + public string? Description { get; set; } + + /// A vector field. + [VectorStoreRecordVector(Dimensions: 4, IndexKind: IndexKind.IvfFlat, DistanceFunction: DistanceFunction.CosineDistance)] + public ReadOnlyMemory? DescriptionEmbedding { get; set; } +} diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBKernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBKernelBuilderExtensionsTests.cs new file mode 100644 index 000000000000..86b64257988f --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBKernelBuilderExtensionsTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; +using Microsoft.SemanticKernel.Data; +using MongoDB.Driver; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.AzureCosmosDBMongoDB.UnitTests; + +/// +/// Unit tests for class. +/// +public sealed class AzureCosmosDBMongoDBKernelBuilderExtensionsTests +{ + private readonly IKernelBuilder _kernelBuilder = Kernel.CreateBuilder(); + + [Fact] + public void AddVectorStoreRegistersClass() + { + // Arrange + this._kernelBuilder.Services.AddSingleton(Mock.Of()); + + // Act + this._kernelBuilder.AddAzureCosmosDBMongoDBVectorStore(); + + var kernel = this._kernelBuilder.Build(); + var vectorStore = kernel.Services.GetRequiredService(); + + // Assert + Assert.NotNull(vectorStore); + Assert.IsType(vectorStore); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBServiceCollectionExtensionsTests.cs new file mode 100644 index 000000000000..4e07f71b1d3a --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBServiceCollectionExtensionsTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; +using Microsoft.SemanticKernel.Data; +using MongoDB.Driver; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.AzureCosmosDBMongoDB.UnitTests; + +/// +/// Unit tests for class. +/// +public sealed class AzureCosmosDBMongoDBServiceCollectionExtensionsTests +{ + private readonly IServiceCollection _serviceCollection = new ServiceCollection(); + + [Fact] + public void AddVectorStoreRegistersClass() + { + // Arrange + this._serviceCollection.AddSingleton(Mock.Of()); + + // Act + this._serviceCollection.AddAzureCosmosDBMongoDBVectorStore(); + + var serviceProvider = this._serviceCollection.BuildServiceProvider(); + var vectorStore = serviceProvider.GetRequiredService(); + + // Assert + Assert.NotNull(vectorStore); + Assert.IsType(vectorStore); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreRecordCollectionTests.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreRecordCollectionTests.cs new file mode 100644 index 000000000000..ee5f74d79ddd --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreRecordCollectionTests.cs @@ -0,0 +1,651 @@ +// 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.Connectors.AzureCosmosDBMongoDB; +using Microsoft.SemanticKernel.Data; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.AzureCosmosDBMongoDB.UnitTests; + +/// +/// Unit tests for class. +/// +public sealed class AzureCosmosDBMongoDBVectorStoreRecordCollectionTests +{ + private readonly Mock _mockMongoDatabase = new(); + private readonly Mock> _mockMongoCollection = new(); + + public AzureCosmosDBMongoDBVectorStoreRecordCollectionTests() + { + this._mockMongoDatabase + .Setup(l => l.GetCollection(It.IsAny(), It.IsAny())) + .Returns(this._mockMongoCollection.Object); + } + + [Fact] + public void ConstructorForModelWithoutKeyThrowsException() + { + // Act & Assert + var exception = Assert.Throws(() => new AzureCosmosDBMongoDBVectorStoreRecordCollection(this._mockMongoDatabase.Object, "collection")); + Assert.Contains("No key property found", exception.Message); + } + + [Fact] + public void ConstructorWithDeclarativeModelInitializesCollection() + { + // Act & Assert + var collection = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection"); + + Assert.NotNull(collection); + } + + [Fact] + public void ConstructorWithImperativeModelInitializesCollection() + { + // Arrange + var definition = new VectorStoreRecordDefinition + { + Properties = [new VectorStoreRecordKeyProperty("Id", typeof(string))] + }; + + // Act + var collection = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection", + new() { VectorStoreRecordDefinition = definition }); + + // Assert + Assert.NotNull(collection); + } + + [Theory] + [MemberData(nameof(CollectionExistsData))] + public async Task CollectionExistsReturnsValidResultAsync(List collections, string collectionName, bool expectedResult) + { + // Arrange + var mockCursor = new Mock>(); + + mockCursor + .Setup(l => l.MoveNextAsync(It.IsAny())) + .ReturnsAsync(true); + + mockCursor + .Setup(l => l.Current) + .Returns(collections); + + this._mockMongoDatabase + .Setup(l => l.ListCollectionNamesAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockCursor.Object); + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + collectionName); + + // Act + var actualResult = await sut.CollectionExistsAsync(); + + // Assert + Assert.Equal(expectedResult, actualResult); + } + + [Theory] + [InlineData(true, 0)] + [InlineData(false, 1)] + public async Task CreateCollectionInvokesValidMethodsAsync(bool indexExists, int actualIndexCreations) + { + // Arrange + const string CollectionName = "collection"; + + List indexes = indexExists ? [new BsonDocument { ["name"] = "DescriptionEmbedding_" }] : []; + + var mockIndexCursor = new Mock>(); + mockIndexCursor + .SetupSequence(l => l.MoveNext(It.IsAny())) + .Returns(true) + .Returns(false); + + mockIndexCursor + .Setup(l => l.Current) + .Returns(indexes); + + var mockMongoIndexManager = new Mock>(); + + mockMongoIndexManager + .Setup(l => l.ListAsync(It.IsAny())) + .ReturnsAsync(mockIndexCursor.Object); + + this._mockMongoCollection + .Setup(l => l.Indexes) + .Returns(mockMongoIndexManager.Object); + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection(this._mockMongoDatabase.Object, CollectionName); + + // Act + await sut.CreateCollectionAsync(); + + // Assert + this._mockMongoDatabase.Verify(l => l.CreateCollectionAsync( + CollectionName, + It.IsAny(), + It.IsAny()), Times.Once()); + + this._mockMongoDatabase.Verify(l => l.RunCommandAsync( + It.Is>(command => + command.Document["createIndexes"] == CollectionName && + command.Document["indexes"].GetType() == typeof(BsonArray) && + ((BsonArray)command.Document["indexes"]).Count == 1), + It.IsAny(), + It.IsAny()), Times.Exactly(actualIndexCreations)); + } + + [Theory] + [MemberData(nameof(CreateCollectionIfNotExistsData))] + public async Task CreateCollectionIfNotExistsInvokesValidMethodsAsync(List collections, int actualCollectionCreations) + { + // Arrange + const string CollectionName = "collection"; + + var mockCursor = new Mock>(); + mockCursor + .Setup(l => l.MoveNextAsync(It.IsAny())) + .ReturnsAsync(true); + + mockCursor + .Setup(l => l.Current) + .Returns(collections); + + this._mockMongoDatabase + .Setup(l => l.ListCollectionNamesAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockCursor.Object); + + var mockIndexCursor = new Mock>(); + mockIndexCursor + .SetupSequence(l => l.MoveNext(It.IsAny())) + .Returns(true) + .Returns(false); + + mockIndexCursor + .Setup(l => l.Current) + .Returns([]); + + var mockMongoIndexManager = new Mock>(); + + mockMongoIndexManager + .Setup(l => l.ListAsync(It.IsAny())) + .ReturnsAsync(mockIndexCursor.Object); + + this._mockMongoCollection + .Setup(l => l.Indexes) + .Returns(mockMongoIndexManager.Object); + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + CollectionName); + + // Act + await sut.CreateCollectionIfNotExistsAsync(); + + // Assert + this._mockMongoDatabase.Verify(l => l.CreateCollectionAsync( + CollectionName, + It.IsAny(), + It.IsAny()), Times.Exactly(actualCollectionCreations)); + } + + [Fact] + public async Task DeleteInvokesValidMethodsAsync() + { + // Arrange + const string RecordKey = "key"; + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection"); + + var serializerRegistry = BsonSerializer.SerializerRegistry; + var documentSerializer = serializerRegistry.GetSerializer(); + var expectedDefinition = Builders.Filter.Eq(document => document["_id"], RecordKey); + + // Act + await sut.DeleteAsync(RecordKey); + + // Assert + this._mockMongoCollection.Verify(l => l.DeleteOneAsync( + It.Is>(definition => + definition.Render(documentSerializer, serializerRegistry) == + expectedDefinition.Render(documentSerializer, serializerRegistry)), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task DeleteBatchInvokesValidMethodsAsync() + { + // Arrange + List recordKeys = ["key1", "key2"]; + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection"); + + var serializerRegistry = BsonSerializer.SerializerRegistry; + var documentSerializer = serializerRegistry.GetSerializer(); + var expectedDefinition = Builders.Filter.In(document => document["_id"].AsString, recordKeys); + + // Act + await sut.DeleteBatchAsync(recordKeys); + + // Assert + this._mockMongoCollection.Verify(l => l.DeleteManyAsync( + It.Is>(definition => + definition.Render(documentSerializer, serializerRegistry) == + expectedDefinition.Render(documentSerializer, serializerRegistry)), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task DeleteCollectionInvokesValidMethodsAsync() + { + // Arrange + const string CollectionName = "collection"; + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + CollectionName); + + // Act + await sut.DeleteCollectionAsync(); + + // Assert + this._mockMongoDatabase.Verify(l => l.DropCollectionAsync( + It.Is(name => name == CollectionName), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GetReturnsValidRecordAsync() + { + // Arrange + const string RecordKey = "key"; + + var document = new BsonDocument { ["_id"] = RecordKey, ["HotelName"] = "Test Name" }; + + var mockCursor = new Mock>(); + mockCursor + .Setup(l => l.MoveNextAsync(It.IsAny())) + .ReturnsAsync(true); + + mockCursor + .Setup(l => l.Current) + .Returns([document]); + + this._mockMongoCollection + .Setup(l => l.FindAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(mockCursor.Object); + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection"); + + // Act + var result = await sut.GetAsync(RecordKey); + + // Assert + Assert.NotNull(result); + Assert.Equal(RecordKey, result.HotelId); + Assert.Equal("Test Name", result.HotelName); + } + + [Fact] + public async Task GetBatchReturnsValidRecordAsync() + { + // Arrange + var document1 = new BsonDocument { ["_id"] = "key1", ["HotelName"] = "Test Name 1" }; + var document2 = new BsonDocument { ["_id"] = "key2", ["HotelName"] = "Test Name 2" }; + var document3 = new BsonDocument { ["_id"] = "key3", ["HotelName"] = "Test Name 3" }; + + var mockCursor = new Mock>(); + mockCursor + .SetupSequence(l => l.MoveNextAsync(It.IsAny())) + .ReturnsAsync(true) + .ReturnsAsync(false); + + mockCursor + .Setup(l => l.Current) + .Returns([document1, document2, document3]); + + this._mockMongoCollection + .Setup(l => l.FindAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(mockCursor.Object); + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection"); + + // Act + var results = await sut.GetBatchAsync(["key1", "key2", "key3"]).ToListAsync(); + + // Assert + Assert.NotNull(results[0]); + Assert.Equal("key1", results[0].HotelId); + Assert.Equal("Test Name 1", results[0].HotelName); + + Assert.NotNull(results[1]); + Assert.Equal("key2", results[1].HotelId); + Assert.Equal("Test Name 2", results[1].HotelName); + + Assert.NotNull(results[2]); + Assert.Equal("key3", results[2].HotelId); + Assert.Equal("Test Name 3", results[2].HotelName); + } + + [Fact] + public async Task UpsertReturnsRecordKeyAsync() + { + // Arrange + var hotel = new AzureCosmosDBMongoDBHotelModel("key") { HotelName = "Test Name" }; + + var serializerRegistry = BsonSerializer.SerializerRegistry; + var documentSerializer = serializerRegistry.GetSerializer(); + var expectedDefinition = Builders.Filter.Eq(document => document["_id"], "key"); + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection"); + + // Act + var result = await sut.UpsertAsync(hotel); + + // Assert + Assert.Equal("key", result); + + this._mockMongoCollection.Verify(l => l.ReplaceOneAsync( + It.Is>(definition => + definition.Render(documentSerializer, serializerRegistry) == + expectedDefinition.Render(documentSerializer, serializerRegistry)), + It.Is(document => + document["_id"] == "key" && + document["HotelName"] == "Test Name"), + It.IsAny(), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task UpsertBatchReturnsRecordKeysAsync() + { + // Arrange + var hotel1 = new AzureCosmosDBMongoDBHotelModel("key1") { HotelName = "Test Name 1" }; + var hotel2 = new AzureCosmosDBMongoDBHotelModel("key2") { HotelName = "Test Name 2" }; + var hotel3 = new AzureCosmosDBMongoDBHotelModel("key3") { HotelName = "Test Name 3" }; + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection"); + + // Act + var results = await sut.UpsertBatchAsync([hotel1, hotel2, hotel3]).ToListAsync(); + + // Assert + Assert.NotNull(results); + Assert.Equal(3, results.Count); + + Assert.Equal("key1", results[0]); + Assert.Equal("key2", results[1]); + Assert.Equal("key3", results[2]); + } + + [Fact] + public async Task UpsertWithModelWorksCorrectlyAsync() + { + var definition = new VectorStoreRecordDefinition + { + Properties = new List + { + new VectorStoreRecordKeyProperty("Id", typeof(string)), + new VectorStoreRecordDataProperty("HotelName", typeof(string)) + } + }; + + await this.TestUpsertWithModeAsync( + dataModel: new TestModel { Id = "key", HotelName = "Test Name" }, + expectedPropertyName: "HotelName", + definition: definition); + } + + [Fact] + public async Task UpsertWithVectorStoreModelWorksCorrectlyAsync() + { + await this.TestUpsertWithModeAsync( + dataModel: new VectorStoreTestModel { Id = "key", HotelName = "Test Name" }, + expectedPropertyName: "hotel_name"); + } + + [Fact] + public async Task UpsertWithBsonModelWorksCorrectlyAsync() + { + var definition = new VectorStoreRecordDefinition + { + Properties = new List + { + new VectorStoreRecordKeyProperty("Id", typeof(string)), + new VectorStoreRecordDataProperty("HotelName", typeof(string)) + } + }; + + await this.TestUpsertWithModeAsync( + dataModel: new BsonTestModel { Id = "key", HotelName = "Test Name" }, + expectedPropertyName: "hotel_name", + definition: definition); + } + + [Fact] + public async Task UpsertWithBsonVectorStoreModelWorksCorrectlyAsync() + { + await this.TestUpsertWithModeAsync( + dataModel: new BsonVectorStoreTestModel { Id = "key", HotelName = "Test Name" }, + expectedPropertyName: "hotel_name"); + } + + [Fact] + public async Task UpsertWithBsonVectorStoreWithNameModelWorksCorrectlyAsync() + { + await this.TestUpsertWithModeAsync( + dataModel: new BsonVectorStoreWithNameTestModel { Id = "key", HotelName = "Test Name" }, + expectedPropertyName: "bson_hotel_name"); + } + + [Fact] + public async Task UpsertWithCustomMapperWorksCorrectlyAsync() + { + // Arrange + var hotel = new AzureCosmosDBMongoDBHotelModel("key") { HotelName = "Test Name" }; + + var mockMapper = new Mock>(); + + mockMapper + .Setup(l => l.MapFromDataToStorageModel(It.IsAny())) + .Returns(new BsonDocument { ["_id"] = "key", ["my_name"] = "Test Name" }); + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection", + new() { BsonDocumentCustomMapper = mockMapper.Object }); + + // Act + var result = await sut.UpsertAsync(hotel); + + // Assert + Assert.Equal("key", result); + + this._mockMongoCollection.Verify(l => l.ReplaceOneAsync( + It.IsAny>(), + It.Is(document => + document["_id"] == "key" && + document["my_name"] == "Test Name"), + It.IsAny(), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task GetWithCustomMapperWorksCorrectlyAsync() + { + // Arrange + const string RecordKey = "key"; + + var document = new BsonDocument { ["_id"] = RecordKey, ["my_name"] = "Test Name" }; + + var mockCursor = new Mock>(); + mockCursor + .Setup(l => l.MoveNextAsync(It.IsAny())) + .ReturnsAsync(true); + + mockCursor + .Setup(l => l.Current) + .Returns([document]); + + this._mockMongoCollection + .Setup(l => l.FindAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(mockCursor.Object); + + var mockMapper = new Mock>(); + + mockMapper + .Setup(l => l.MapFromStorageToDataModel(It.IsAny(), It.IsAny())) + .Returns(new AzureCosmosDBMongoDBHotelModel(RecordKey) { HotelName = "Name from mapper" }); + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection", + new() { BsonDocumentCustomMapper = mockMapper.Object }); + + // Act + var result = await sut.GetAsync(RecordKey); + + // Assert + Assert.NotNull(result); + Assert.Equal(RecordKey, result.HotelId); + Assert.Equal("Name from mapper", result.HotelName); + } + + public static TheoryData, string, bool> CollectionExistsData => new() + { + { ["collection-2"], "collection-2", true }, + { [], "non-existent-collection", false } + }; + + public static TheoryData, int> CreateCollectionIfNotExistsData => new() + { + { ["collection"], 0 }, + { [], 1 } + }; + + #region private + + private async Task TestUpsertWithModeAsync( + TDataModel dataModel, + string expectedPropertyName, + VectorStoreRecordDefinition? definition = null) + where TDataModel : class + { + // Arrange + var serializerRegistry = BsonSerializer.SerializerRegistry; + var documentSerializer = serializerRegistry.GetSerializer(); + var expectedDefinition = Builders.Filter.Eq(document => document["_id"], "key"); + + AzureCosmosDBMongoDBVectorStoreRecordCollectionOptions? options = definition != null ? + new() { VectorStoreRecordDefinition = definition } : + null; + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection", + options); + + // Act + var result = await sut.UpsertAsync(dataModel); + + // Assert + Assert.Equal("key", result); + + this._mockMongoCollection.Verify(l => l.ReplaceOneAsync( + It.Is>(definition => + definition.Render(documentSerializer, serializerRegistry) == + expectedDefinition.Render(documentSerializer, serializerRegistry)), + It.Is(document => + document["_id"] == "key" && + document.Contains(expectedPropertyName) && + document[expectedPropertyName] == "Test Name"), + It.IsAny(), + It.IsAny()), Times.Once()); + } + +#pragma warning disable CA1812 + private sealed class TestModel + { + public string? Id { get; set; } + + public string? HotelName { get; set; } + } + + private sealed class VectorStoreTestModel + { + [VectorStoreRecordKey] + public string? Id { get; set; } + + [VectorStoreRecordData(StoragePropertyName = "hotel_name")] + public string? HotelName { get; set; } + } + + private sealed class BsonTestModel + { + [BsonId] + public string? Id { get; set; } + + [BsonElement("hotel_name")] + public string? HotelName { get; set; } + } + + private sealed class BsonVectorStoreTestModel + { + [BsonId] + [VectorStoreRecordKey] + public string? Id { get; set; } + + [BsonElement("hotel_name")] + [VectorStoreRecordData] + public string? HotelName { get; set; } + } + + private sealed class BsonVectorStoreWithNameTestModel + { + [BsonId] + [VectorStoreRecordKey] + public string? Id { get; set; } + + [BsonElement("bson_hotel_name")] + [VectorStoreRecordData(StoragePropertyName = "storage_hotel_name")] + public string? HotelName { get; set; } + } +#pragma warning restore CA1812 + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreRecordMapperTests.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreRecordMapperTests.cs new file mode 100644 index 000000000000..e561b6f32d4e --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreRecordMapperTests.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; +using Microsoft.SemanticKernel.Data; +using MongoDB.Bson; +using Xunit; + +namespace SemanticKernel.Connectors.AzureCosmosDBMongoDB.UnitTests; + +/// +/// Unit tests for class. +/// +public sealed class AzureCosmosDBMongoDBVectorStoreRecordMapperTests +{ + private readonly AzureCosmosDBMongoDBVectorStoreRecordMapper _sut; + + public AzureCosmosDBMongoDBVectorStoreRecordMapperTests() + { + var definition = new VectorStoreRecordDefinition + { + Properties = + [ + new VectorStoreRecordKeyProperty("HotelId", typeof(string)), + new VectorStoreRecordDataProperty("HotelName", typeof(string)), + new VectorStoreRecordDataProperty("Tags", typeof(List)), + new VectorStoreRecordVectorProperty("DescriptionEmbedding", typeof(ReadOnlyMemory?)) { StoragePropertyName = "description_embedding " } + ] + }; + + var storagePropertyNames = new Dictionary + { + ["HotelId"] = "HotelId", + ["HotelName"] = "HotelName", + ["Tags"] = "Tags", + ["DescriptionEmbedding"] = "description_embedding", + }; + + this._sut = new(definition, storagePropertyNames); + } + + [Fact] + public void MapFromDataToStorageModelReturnsValidObject() + { + // Arrange + var hotel = new AzureCosmosDBMongoDBHotelModel("key") + { + HotelName = "Test Name", + Tags = ["tag1", "tag2"], + DescriptionEmbedding = new ReadOnlyMemory([1f, 2f, 3f]) + }; + + // Act + var document = this._sut.MapFromDataToStorageModel(hotel); + + // Assert + Assert.NotNull(document); + + Assert.Equal("key", document["_id"]); + Assert.Equal("Test Name", document["HotelName"]); + Assert.Equal(["tag1", "tag2"], document["Tags"].AsBsonArray); + Assert.Equal([1f, 2f, 3f], document["description_embedding"].AsBsonArray); + } + + [Fact] + public void MapFromStorageToDataModelReturnsValidObject() + { + // Arrange + var document = new BsonDocument + { + ["_id"] = "key", + ["HotelName"] = "Test Name", + ["Tags"] = BsonArray.Create(new List { "tag1", "tag2" }), + ["description_embedding"] = BsonArray.Create(new List { 1f, 2f, 3f }) + }; + + // Act + var hotel = this._sut.MapFromStorageToDataModel(document, new()); + + // Assert + Assert.NotNull(hotel); + + Assert.Equal("key", hotel.HotelId); + Assert.Equal("Test Name", hotel.HotelName); + Assert.Equal(["tag1", "tag2"], hotel.Tags); + Assert.True(new ReadOnlyMemory([1f, 2f, 3f]).Span.SequenceEqual(hotel.DescriptionEmbedding!.Value.Span)); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreTests.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreTests.cs new file mode 100644 index 000000000000..3bc2049bf2c9 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/AzureCosmosDBMongoDBVectorStoreTests.cs @@ -0,0 +1,103 @@ +// 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.Connectors.AzureCosmosDBMongoDB; +using Microsoft.SemanticKernel.Data; +using MongoDB.Driver; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.AzureCosmosDBMongoDB.UnitTests; + +/// +/// Unit tests for class. +/// +public sealed class AzureCosmosDBMongoDBVectorStoreTests +{ + private readonly Mock _mockMongoDatabase = new(); + + [Fact] + public void GetCollectionWithNotSupportedKeyThrowsException() + { + // Arrange + var sut = new AzureCosmosDBMongoDBVectorStore(this._mockMongoDatabase.Object); + + // Act & Assert + Assert.Throws(() => sut.GetCollection("collection")); + } + + [Fact] + public void GetCollectionWithFactoryReturnsCustomCollection() + { + // Arrange + var mockFactory = new Mock(); + var mockRecordCollection = new Mock>(); + + mockFactory + .Setup(l => l.CreateVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection", + It.IsAny())) + .Returns(mockRecordCollection.Object); + + var sut = new AzureCosmosDBMongoDBVectorStore( + this._mockMongoDatabase.Object, + new AzureCosmosDBMongoDBVectorStoreOptions { VectorStoreCollectionFactory = mockFactory.Object }); + + // Act + var collection = sut.GetCollection("collection"); + + // Assert + Assert.Same(mockRecordCollection.Object, collection); + mockFactory.Verify(l => l.CreateVectorStoreRecordCollection( + this._mockMongoDatabase.Object, + "collection", + It.IsAny()), Times.Once()); + } + + [Fact] + public void GetCollectionWithoutFactoryReturnsDefaultCollection() + { + // Arrange + var sut = new AzureCosmosDBMongoDBVectorStore(this._mockMongoDatabase.Object); + + // Act + var collection = sut.GetCollection("collection"); + + // Assert + Assert.NotNull(collection); + } + + [Fact] + public async Task ListCollectionNamesReturnsCollectionNamesAsync() + { + // Arrange + var expectedCollectionNames = new List { "collection-1", "collection-2", "collection-3" }; + + var mockCursor = new Mock>(); + mockCursor + .SetupSequence(l => l.MoveNextAsync(It.IsAny())) + .ReturnsAsync(true) + .ReturnsAsync(false); + + mockCursor + .Setup(l => l.Current) + .Returns(expectedCollectionNames); + + this._mockMongoDatabase + .Setup(l => l.ListCollectionNamesAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(mockCursor.Object); + + var sut = new AzureCosmosDBMongoDBVectorStore(this._mockMongoDatabase.Object); + + // Act + var actualCollectionNames = await sut.ListCollectionNamesAsync().ToListAsync(); + + // Assert + Assert.Equal(expectedCollectionNames, actualCollectionNames); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/Connectors.AzureCosmosDBMongoDB.UnitTests.csproj b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/Connectors.AzureCosmosDBMongoDB.UnitTests.csproj new file mode 100644 index 000000000000..a31e4b802b52 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBMongoDB.UnitTests/Connectors.AzureCosmosDBMongoDB.UnitTests.csproj @@ -0,0 +1,32 @@ + + + + SemanticKernel.Connectors.AzureCosmosDBMongoDB.UnitTests + SemanticKernel.Connectors.AzureCosmosDBMongoDB.UnitTests + net8.0 + true + enable + disable + false + $(NoWarn);SKEXP0001,SKEXP0020 + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/.editorconfig b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/.editorconfig new file mode 100644 index 000000000000..394eef685f21 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.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.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLHotel.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLHotel.cs new file mode 100644 index 000000000000..951eca4bb016 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLHotel.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.Data; + +namespace SemanticKernel.Connectors.AzureCosmosDBNoSQL.UnitTests; + +public class AzureCosmosDBNoSQLHotel(string hotelId) +{ + /// The key of the record. + [VectorStoreRecordKey] + public string HotelId { get; init; } = hotelId; + + /// A string metadata field. + [VectorStoreRecordData] + public string? HotelName { get; set; } + + /// An int metadata field. + [VectorStoreRecordData] + public int HotelCode { get; set; } + + /// A float metadata field. + [VectorStoreRecordData] + public float? HotelRating { get; set; } + + /// A bool metadata field. + [VectorStoreRecordData(StoragePropertyName = "parking_is_included")] + public bool ParkingIncluded { get; set; } + + /// An array metadata field. + [VectorStoreRecordData] + public List Tags { get; set; } = []; + + /// A data field. + [VectorStoreRecordData] + public string? Description { get; set; } + + /// A vector field. + [JsonPropertyName("description_embedding")] + [VectorStoreRecordVector(Dimensions: 4, IndexKind: IndexKind.Flat, DistanceFunction: DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory? DescriptionEmbedding { get; set; } +} diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLKernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLKernelBuilderExtensionsTests.cs new file mode 100644 index 000000000000..9ef22ae67767 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLKernelBuilderExtensionsTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; +using Microsoft.SemanticKernel.Data; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.AzureCosmosDBNoSQL.UnitTests; + +/// +/// Unit tests for class. +/// +public sealed class AzureCosmosDBNoSQLKernelBuilderExtensionsTests +{ + private readonly IKernelBuilder _kernelBuilder = Kernel.CreateBuilder(); + + [Fact] + public void AddVectorStoreRegistersClass() + { + // Arrange + this._kernelBuilder.Services.AddSingleton(Mock.Of()); + + // Act + this._kernelBuilder.AddAzureCosmosDBNoSQLVectorStore(); + + var kernel = this._kernelBuilder.Build(); + var vectorStore = kernel.Services.GetRequiredService(); + + // Assert + Assert.NotNull(vectorStore); + Assert.IsType(vectorStore); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLServiceCollectionExtensionsTests.cs new file mode 100644 index 000000000000..0c5b0a7409e8 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLServiceCollectionExtensionsTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; +using Microsoft.SemanticKernel.Data; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.AzureCosmosDBNoSQL.UnitTests; + +/// +/// Unit tests for class. +/// +public sealed class AzureCosmosDBNoSQLServiceCollectionExtensionsTests +{ + private readonly IServiceCollection _serviceCollection = new ServiceCollection(); + + [Fact] + public void AddVectorStoreRegistersClass() + { + // Arrange + this._serviceCollection.AddSingleton(Mock.Of()); + + // Act + this._serviceCollection.AddAzureCosmosDBNoSQLVectorStore(); + + var serviceProvider = this._serviceCollection.BuildServiceProvider(); + var vectorStore = serviceProvider.GetRequiredService(); + + // Assert + Assert.NotNull(vectorStore); + Assert.IsType(vectorStore); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLVectorStoreRecordCollectionTests.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLVectorStoreRecordCollectionTests.cs new file mode 100644 index 000000000000..52333e03b969 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLVectorStoreRecordCollectionTests.cs @@ -0,0 +1,647 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Cosmos; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; +using Microsoft.SemanticKernel.Data; +using Moq; +using Xunit; +using DistanceFunction = Microsoft.SemanticKernel.Data.DistanceFunction; +using IndexKind = Microsoft.SemanticKernel.Data.IndexKind; + +namespace SemanticKernel.Connectors.AzureCosmosDBNoSQL.UnitTests; + +/// +/// Unit tests for class. +/// +public sealed class AzureCosmosDBNoSQLVectorStoreRecordCollectionTests +{ + private readonly Mock _mockDatabase = new(); + private readonly Mock _mockContainer = new(); + + public AzureCosmosDBNoSQLVectorStoreRecordCollectionTests() + { + this._mockDatabase + .Setup(l => l.GetContainer(It.IsAny())) + .Returns(this._mockContainer.Object); + } + + [Fact] + public void ConstructorForModelWithoutKeyThrowsException() + { + // Act & Assert + var exception = Assert.Throws(() => new AzureCosmosDBNoSQLVectorStoreRecordCollection(this._mockDatabase.Object, "collection")); + Assert.Contains("No key property found", exception.Message); + } + + [Fact] + public void ConstructorWithDeclarativeModelInitializesCollection() + { + // Act & Assert + var collection = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection"); + + Assert.NotNull(collection); + } + + [Fact] + public void ConstructorWithImperativeModelInitializesCollection() + { + // Arrange + var definition = new VectorStoreRecordDefinition + { + Properties = [new VectorStoreRecordKeyProperty("Id", typeof(string))] + }; + + // Act + var collection = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection", + new() { VectorStoreRecordDefinition = definition }); + + // Assert + Assert.NotNull(collection); + } + + [Theory] + [MemberData(nameof(CollectionExistsData))] + public async Task CollectionExistsReturnsValidResultAsync(List collections, string collectionName, bool expectedResult) + { + // Arrange + var mockFeedResponse = new Mock>(); + mockFeedResponse + .Setup(l => l.Resource) + .Returns(collections); + + var mockFeedIterator = new Mock>(); + mockFeedIterator + .SetupSequence(l => l.HasMoreResults) + .Returns(true) + .Returns(false); + + mockFeedIterator + .Setup(l => l.ReadNextAsync(It.IsAny())) + .ReturnsAsync(mockFeedResponse.Object); + + this._mockDatabase + .Setup(l => l.GetContainerQueryIterator( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockFeedIterator.Object); + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + collectionName); + + // Act + var actualResult = await sut.CollectionExistsAsync(); + + // Assert + Assert.Equal(expectedResult, actualResult); + } + + [Theory] + [InlineData(IndexingMode.Consistent)] + [InlineData(IndexingMode.Lazy)] + [InlineData(IndexingMode.None)] + public async Task CreateCollectionUsesValidContainerPropertiesAsync(IndexingMode indexingMode) + { + // Arrange + const string CollectionName = "collection"; + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + CollectionName, + new() { IndexingMode = indexingMode, Automatic = indexingMode != IndexingMode.None }); + + var expectedVectorEmbeddingPolicy = new VectorEmbeddingPolicy( + [ + new Embedding + { + DataType = VectorDataType.Float16, + Dimensions = 1, + DistanceFunction = Microsoft.Azure.Cosmos.DistanceFunction.Cosine, + Path = "/DescriptionEmbedding1" + }, + new Embedding + { + DataType = VectorDataType.Float32, + Dimensions = 2, + DistanceFunction = Microsoft.Azure.Cosmos.DistanceFunction.Cosine, + Path = "/DescriptionEmbedding2" + }, + new Embedding + { + DataType = VectorDataType.Uint8, + Dimensions = 3, + DistanceFunction = Microsoft.Azure.Cosmos.DistanceFunction.DotProduct, + Path = "/DescriptionEmbedding3" + }, + new Embedding + { + DataType = VectorDataType.Int8, + Dimensions = 4, + DistanceFunction = Microsoft.Azure.Cosmos.DistanceFunction.Euclidean, + Path = "/DescriptionEmbedding4" + }, + ]); + + var expectedIndexingPolicy = new IndexingPolicy + { + VectorIndexes = + [ + new VectorIndexPath { Type = VectorIndexType.Flat, Path = "/DescriptionEmbedding1" }, + new VectorIndexPath { Type = VectorIndexType.Flat, Path = "/DescriptionEmbedding2" }, + new VectorIndexPath { Type = VectorIndexType.QuantizedFlat, Path = "/DescriptionEmbedding3" }, + new VectorIndexPath { Type = VectorIndexType.DiskANN, Path = "/DescriptionEmbedding4" }, + ], + IndexingMode = indexingMode, + Automatic = indexingMode != IndexingMode.None + }; + + if (indexingMode != IndexingMode.None) + { + expectedIndexingPolicy.IncludedPaths.Add(new IncludedPath { Path = "/IndexableData1/?" }); + expectedIndexingPolicy.IncludedPaths.Add(new IncludedPath { Path = "/IndexableData2/?" }); + expectedIndexingPolicy.IncludedPaths.Add(new IncludedPath { Path = "/" }); + + expectedIndexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = "/DescriptionEmbedding1/*" }); + expectedIndexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = "/DescriptionEmbedding2/*" }); + expectedIndexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = "/DescriptionEmbedding3/*" }); + expectedIndexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = "/DescriptionEmbedding4/*" }); + } + + var expectedContainerProperties = new ContainerProperties(CollectionName, "/id") + { + VectorEmbeddingPolicy = expectedVectorEmbeddingPolicy, + IndexingPolicy = expectedIndexingPolicy + }; + + // Act + await sut.CreateCollectionAsync(); + + // Assert + this._mockDatabase.Verify(l => l.CreateContainerAsync( + It.Is(properties => this.VerifyContainerProperties(expectedContainerProperties, properties)), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once()); + } + + [Theory] + [MemberData(nameof(CreateCollectionIfNotExistsData))] + public async Task CreateCollectionIfNotExistsInvokesValidMethodsAsync(List collections, int actualCollectionCreations) + { + // Arrange + const string CollectionName = "collection"; + + var mockFeedResponse = new Mock>(); + mockFeedResponse + .Setup(l => l.Resource) + .Returns(collections); + + var mockFeedIterator = new Mock>(); + mockFeedIterator + .SetupSequence(l => l.HasMoreResults) + .Returns(true) + .Returns(false); + + mockFeedIterator + .Setup(l => l.ReadNextAsync(It.IsAny())) + .ReturnsAsync(mockFeedResponse.Object); + + this._mockDatabase + .Setup(l => l.GetContainerQueryIterator( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockFeedIterator.Object); + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + CollectionName); + + // Act + await sut.CreateCollectionIfNotExistsAsync(); + + // Assert + this._mockDatabase.Verify(l => l.CreateContainerAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Exactly(actualCollectionCreations)); + } + + [Theory] + [InlineData("recordKey", false)] + [InlineData("partitionKey", true)] + public async Task DeleteInvokesValidMethodsAsync( + string expectedPartitionKey, + bool useCompositeKeyCollection) + { + // Arrange + const string RecordKey = "recordKey"; + const string PartitionKey = "partitionKey"; + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection"); + + // Act + if (useCompositeKeyCollection) + { + await ((IVectorStoreRecordCollection)sut).DeleteAsync( + new AzureCosmosDBNoSQLCompositeKey(RecordKey, PartitionKey)); + } + else + { + await ((IVectorStoreRecordCollection)sut).DeleteAsync( + RecordKey); + } + + // Assert + this._mockContainer.Verify(l => l.DeleteItemAsync( + RecordKey, + new PartitionKey(expectedPartitionKey), + It.IsAny(), + It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task DeleteBatchInvokesValidMethodsAsync() + { + // Arrange + List recordKeys = ["key1", "key2"]; + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection"); + + // Act + await sut.DeleteBatchAsync(recordKeys); + + // Assert + foreach (var key in recordKeys) + { + this._mockContainer.Verify(l => l.DeleteItemAsync( + key, + new PartitionKey(key), + It.IsAny(), + It.IsAny()), + Times.Once()); + } + } + + [Fact] + public async Task DeleteCollectionInvokesValidMethodsAsync() + { + // Arrange + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection"); + + // Act + await sut.DeleteCollectionAsync(); + + // Assert + this._mockContainer.Verify(l => l.DeleteContainerAsync( + It.IsAny(), + It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task GetReturnsValidRecordAsync() + { + // Arrange + const string RecordKey = "key"; + + var jsonObject = new JsonObject { ["id"] = RecordKey, ["HotelName"] = "Test Name" }; + + var mockFeedResponse = new Mock>(); + mockFeedResponse + .Setup(l => l.Resource) + .Returns([jsonObject]); + + var mockFeedIterator = new Mock>(); + mockFeedIterator + .SetupSequence(l => l.HasMoreResults) + .Returns(true) + .Returns(false); + + mockFeedIterator + .Setup(l => l.ReadNextAsync(It.IsAny())) + .ReturnsAsync(mockFeedResponse.Object); + + this._mockContainer + .Setup(l => l.GetItemQueryIterator( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockFeedIterator.Object); + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection"); + + // Act + var result = await sut.GetAsync(RecordKey); + + // Assert + Assert.NotNull(result); + Assert.Equal(RecordKey, result.HotelId); + Assert.Equal("Test Name", result.HotelName); + } + + [Fact] + public async Task GetBatchReturnsValidRecordAsync() + { + // Arrange + var jsonObject1 = new JsonObject { ["id"] = "key1", ["HotelName"] = "Test Name 1" }; + var jsonObject2 = new JsonObject { ["id"] = "key2", ["HotelName"] = "Test Name 2" }; + var jsonObject3 = new JsonObject { ["id"] = "key3", ["HotelName"] = "Test Name 3" }; + + var mockFeedResponse = new Mock>(); + mockFeedResponse + .Setup(l => l.Resource) + .Returns([jsonObject1, jsonObject2, jsonObject3]); + + var mockFeedIterator = new Mock>(); + mockFeedIterator + .SetupSequence(l => l.HasMoreResults) + .Returns(true) + .Returns(false); + + mockFeedIterator + .Setup(l => l.ReadNextAsync(It.IsAny())) + .ReturnsAsync(mockFeedResponse.Object); + + this._mockContainer + .Setup(l => l.GetItemQueryIterator( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockFeedIterator.Object); + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection"); + + // Act + var results = await sut.GetBatchAsync(["key1", "key2", "key3"]).ToListAsync(); + + // Assert + Assert.NotNull(results[0]); + Assert.Equal("key1", results[0].HotelId); + Assert.Equal("Test Name 1", results[0].HotelName); + + Assert.NotNull(results[1]); + Assert.Equal("key2", results[1].HotelId); + Assert.Equal("Test Name 2", results[1].HotelName); + + Assert.NotNull(results[2]); + Assert.Equal("key3", results[2].HotelId); + Assert.Equal("Test Name 3", results[2].HotelName); + } + + [Fact] + public async Task UpsertReturnsRecordKeyAsync() + { + // Arrange + var hotel = new AzureCosmosDBNoSQLHotel("key") { HotelName = "Test Name" }; + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection"); + + // Act + var result = await sut.UpsertAsync(hotel); + + // Assert + Assert.Equal("key", result); + + this._mockContainer.Verify(l => l.UpsertItemAsync( + It.Is(node => + node["id"]!.ToString() == "key" && + node["HotelName"]!.ToString() == "Test Name"), + new PartitionKey("key"), + It.IsAny(), + It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task UpsertBatchReturnsRecordKeysAsync() + { + // Arrange + var hotel1 = new AzureCosmosDBNoSQLHotel("key1") { HotelName = "Test Name 1" }; + var hotel2 = new AzureCosmosDBNoSQLHotel("key2") { HotelName = "Test Name 2" }; + var hotel3 = new AzureCosmosDBNoSQLHotel("key3") { HotelName = "Test Name 3" }; + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection"); + + // Act + var results = await sut.UpsertBatchAsync([hotel1, hotel2, hotel3]).ToListAsync(); + + // Assert + Assert.NotNull(results); + Assert.Equal(3, results.Count); + + Assert.Equal("key1", results[0]); + Assert.Equal("key2", results[1]); + Assert.Equal("key3", results[2]); + } + + [Fact] + public async Task UpsertWithCustomMapperWorksCorrectlyAsync() + { + // Arrange + var hotel = new AzureCosmosDBNoSQLHotel("key") { HotelName = "Test Name" }; + + var mockMapper = new Mock>(); + + mockMapper + .Setup(l => l.MapFromDataToStorageModel(It.IsAny())) + .Returns(new JsonObject { ["id"] = "key", ["my_name"] = "Test Name" }); + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection", + new() { JsonObjectCustomMapper = mockMapper.Object }); + + // Act + var result = await sut.UpsertAsync(hotel); + + // Assert + Assert.Equal("key", result); + + this._mockContainer.Verify(l => l.UpsertItemAsync( + It.Is(node => + node["id"]!.ToString() == "key" && + node["my_name"]!.ToString() == "Test Name"), + new PartitionKey("key"), + It.IsAny(), + It.IsAny()), + Times.Once()); + } + + [Fact] + public async Task GetWithCustomMapperWorksCorrectlyAsync() + { + // Arrange + const string RecordKey = "key"; + + var jsonObject = new JsonObject { ["id"] = RecordKey, ["HotelName"] = "Test Name" }; + + var mockFeedResponse = new Mock>(); + mockFeedResponse + .Setup(l => l.Resource) + .Returns([jsonObject]); + + var mockFeedIterator = new Mock>(); + mockFeedIterator + .SetupSequence(l => l.HasMoreResults) + .Returns(true) + .Returns(false); + + mockFeedIterator + .Setup(l => l.ReadNextAsync(It.IsAny())) + .ReturnsAsync(mockFeedResponse.Object); + + this._mockContainer + .Setup(l => l.GetItemQueryIterator( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockFeedIterator.Object); + + var mockMapper = new Mock>(); + + mockMapper + .Setup(l => l.MapFromStorageToDataModel(It.IsAny(), It.IsAny())) + .Returns(new AzureCosmosDBNoSQLHotel(RecordKey) { HotelName = "Name from mapper" }); + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection", + new() { JsonObjectCustomMapper = mockMapper.Object }); + + // Act + var result = await sut.GetAsync(RecordKey); + + // Assert + Assert.NotNull(result); + Assert.Equal(RecordKey, result.HotelId); + Assert.Equal("Name from mapper", result.HotelName); + } + + public static TheoryData, string, bool> CollectionExistsData => new() + { + { ["collection-2"], "collection-2", true }, + { [], "non-existent-collection", false } + }; + + public static TheoryData, int> CreateCollectionIfNotExistsData => new() + { + { ["collection"], 0 }, + { [], 1 } + }; + + #region + + private bool VerifyContainerProperties(ContainerProperties expected, ContainerProperties actual) + { + Assert.Equal(expected.Id, actual.Id); + Assert.Equal(expected.PartitionKeyPath, actual.PartitionKeyPath); + Assert.Equal(expected.IndexingPolicy.IndexingMode, actual.IndexingPolicy.IndexingMode); + Assert.Equal(expected.IndexingPolicy.Automatic, actual.IndexingPolicy.Automatic); + + for (var i = 0; i < expected.VectorEmbeddingPolicy.Embeddings.Count; i++) + { + var expectedEmbedding = expected.VectorEmbeddingPolicy.Embeddings[i]; + var actualEmbedding = actual.VectorEmbeddingPolicy.Embeddings[i]; + + Assert.Equal(expectedEmbedding.DataType, actualEmbedding.DataType); + Assert.Equal(expectedEmbedding.Dimensions, actualEmbedding.Dimensions); + Assert.Equal(expectedEmbedding.DistanceFunction, actualEmbedding.DistanceFunction); + Assert.Equal(expectedEmbedding.Path, actualEmbedding.Path); + } + + for (var i = 0; i < expected.IndexingPolicy.VectorIndexes.Count; i++) + { + var expectedIndexPath = expected.IndexingPolicy.VectorIndexes[i]; + var actualIndexPath = actual.IndexingPolicy.VectorIndexes[i]; + + Assert.Equal(expectedIndexPath.Type, actualIndexPath.Type); + Assert.Equal(expectedIndexPath.Path, actualIndexPath.Path); + } + + for (var i = 0; i < expected.IndexingPolicy.IncludedPaths.Count; i++) + { + var expectedIncludedPath = expected.IndexingPolicy.IncludedPaths[i].Path; + var actualIncludedPath = actual.IndexingPolicy.IncludedPaths[i].Path; + + Assert.Equal(expectedIncludedPath, actualIncludedPath); + } + + for (var i = 0; i < expected.IndexingPolicy.ExcludedPaths.Count; i++) + { + var expectedExcludedPath = expected.IndexingPolicy.ExcludedPaths[i].Path; + var actualExcludedPath = actual.IndexingPolicy.ExcludedPaths[i].Path; + + Assert.Equal(expectedExcludedPath, actualExcludedPath); + } + + return true; + } + +#pragma warning disable CA1812 + private sealed class TestModel + { + public string? Id { get; set; } + + public string? HotelName { get; set; } + } + + private sealed class TestIndexingModel + { + [VectorStoreRecordKey] + public string? Id { get; set; } + + [VectorStoreRecordVector(Dimensions: 1, IndexKind: IndexKind.Flat, DistanceFunction: DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory? DescriptionEmbedding1 { get; set; } + + [VectorStoreRecordVector(Dimensions: 2, IndexKind: IndexKind.Flat, DistanceFunction: DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory? DescriptionEmbedding2 { get; set; } + + [VectorStoreRecordVector(Dimensions: 3, IndexKind: IndexKind.QuantizedFlat, DistanceFunction: DistanceFunction.DotProductSimilarity)] + public ReadOnlyMemory? DescriptionEmbedding3 { get; set; } + + [VectorStoreRecordVector(Dimensions: 4, IndexKind: IndexKind.DiskAnn, DistanceFunction: DistanceFunction.EuclideanDistance)] + public ReadOnlyMemory? DescriptionEmbedding4 { get; set; } + + [VectorStoreRecordData(IsFilterable = true)] + public string? IndexableData1 { get; set; } + + [VectorStoreRecordData(IsFullTextSearchable = true)] + public string? IndexableData2 { get; set; } + + [VectorStoreRecordData] + public string? NonIndexableData1 { get; set; } + } +#pragma warning restore CA1812 + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLVectorStoreRecordMapperTests.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLVectorStoreRecordMapperTests.cs new file mode 100644 index 000000000000..9c2b7de29b41 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLVectorStoreRecordMapperTests.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; +using Xunit; + +namespace SemanticKernel.Connectors.AzureCosmosDBNoSQL.UnitTests; + +/// +/// Unit tests for class. +/// +public sealed class AzureCosmosDBNoSQLVectorStoreRecordMapperTests +{ + private readonly AzureCosmosDBNoSQLVectorStoreRecordMapper _sut; + + public AzureCosmosDBNoSQLVectorStoreRecordMapperTests() + { + var storagePropertyNames = new Dictionary + { + ["HotelId"] = "HotelId", + ["HotelName"] = "HotelName", + ["Tags"] = "Tags", + ["DescriptionEmbedding"] = "description_embedding", + }; + + this._sut = new("HotelId", storagePropertyNames, JsonSerializerOptions.Default); + } + + [Fact] + public void MapFromDataToStorageModelReturnsValidObject() + { + // Arrange + var hotel = new AzureCosmosDBNoSQLHotel("key") + { + HotelName = "Test Name", + Tags = ["tag1", "tag2"], + DescriptionEmbedding = new ReadOnlyMemory([1f, 2f, 3f]) + }; + + // Act + var document = this._sut.MapFromDataToStorageModel(hotel); + + // Assert + Assert.NotNull(document); + + Assert.Equal("key", document["id"]!.GetValue()); + Assert.Equal("Test Name", document["HotelName"]!.GetValue()); + Assert.Equal(["tag1", "tag2"], document["Tags"]!.AsArray().Select(l => l!.GetValue())); + Assert.Equal([1f, 2f, 3f], document["description_embedding"]!.AsArray().Select(l => l!.GetValue())); + } + + [Fact] + public void MapFromStorageToDataModelReturnsValidObject() + { + // Arrange + var document = new JsonObject + { + ["id"] = "key", + ["HotelName"] = "Test Name", + ["Tags"] = new JsonArray(new List { "tag1", "tag2" }.Select(l => JsonValue.Create(l)).ToArray()), + ["description_embedding"] = new JsonArray(new List { 1f, 2f, 3f }.Select(l => JsonValue.Create(l)).ToArray()), + }; + + // Act + var hotel = this._sut.MapFromStorageToDataModel(document, new()); + + // Assert + Assert.NotNull(hotel); + + Assert.Equal("key", hotel.HotelId); + Assert.Equal("Test Name", hotel.HotelName); + Assert.Equal(["tag1", "tag2"], hotel.Tags); + Assert.True(new ReadOnlyMemory([1f, 2f, 3f]).Span.SequenceEqual(hotel.DescriptionEmbedding!.Value.Span)); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLVectorStoreTests.cs b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLVectorStoreTests.cs new file mode 100644 index 000000000000..79a74d541a86 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/AzureCosmosDBNoSQLVectorStoreTests.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Cosmos; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; +using Microsoft.SemanticKernel.Data; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.AzureCosmosDBNoSQL.UnitTests; + +/// +/// Unit tests for class. +/// +public sealed class AzureCosmosDBNoSQLVectorStoreTests +{ + private readonly Mock _mockDatabase = new(); + + [Fact] + public void GetCollectionWithNotSupportedKeyThrowsException() + { + // Arrange + var sut = new AzureCosmosDBNoSQLVectorStore(this._mockDatabase.Object); + + // Act & Assert + Assert.Throws(() => sut.GetCollection("collection")); + } + + [Fact] + public void GetCollectionWithSupportedKeyReturnsCollection() + { + // Arrange + var sut = new AzureCosmosDBNoSQLVectorStore(this._mockDatabase.Object); + + // Act + var collectionWithStringKey = sut.GetCollection("collection1"); + var collectionWithCompositeKey = sut.GetCollection("collection1"); + + // Assert + Assert.NotNull(collectionWithStringKey); + Assert.NotNull(collectionWithCompositeKey); + } + + [Fact] + public void GetCollectionWithFactoryReturnsCustomCollection() + { + // Arrange + var mockFactory = new Mock(); + var mockRecordCollection = new Mock>(); + + mockFactory + .Setup(l => l.CreateVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection", + It.IsAny())) + .Returns(mockRecordCollection.Object); + + var sut = new AzureCosmosDBNoSQLVectorStore( + this._mockDatabase.Object, + new AzureCosmosDBNoSQLVectorStoreOptions { VectorStoreCollectionFactory = mockFactory.Object }); + + // Act + var collection = sut.GetCollection("collection"); + + // Assert + Assert.Same(mockRecordCollection.Object, collection); + mockFactory.Verify(l => l.CreateVectorStoreRecordCollection( + this._mockDatabase.Object, + "collection", + It.IsAny()), Times.Once()); + } + + [Fact] + public void GetCollectionWithoutFactoryReturnsDefaultCollection() + { + // Arrange + var sut = new AzureCosmosDBNoSQLVectorStore(this._mockDatabase.Object); + + // Act + var collection = sut.GetCollection("collection"); + + // Assert + Assert.NotNull(collection); + } + + [Fact] + public async Task ListCollectionNamesReturnsCollectionNamesAsync() + { + // Arrange + var expectedCollectionNames = new List { "collection-1", "collection-2", "collection-3" }; + + var mockFeedResponse = new Mock>(); + mockFeedResponse + .Setup(l => l.Resource) + .Returns(expectedCollectionNames); + + var mockFeedIterator = new Mock>(); + mockFeedIterator + .SetupSequence(l => l.HasMoreResults) + .Returns(true) + .Returns(false); + + mockFeedIterator + .Setup(l => l.ReadNextAsync(It.IsAny())) + .ReturnsAsync(mockFeedResponse.Object); + + this._mockDatabase + .Setup(l => l.GetContainerQueryIterator( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockFeedIterator.Object); + + var sut = new AzureCosmosDBNoSQLVectorStore(this._mockDatabase.Object); + + // Act + var actualCollectionNames = await sut.ListCollectionNamesAsync().ToListAsync(); + + // Assert + Assert.Equal(expectedCollectionNames, actualCollectionNames); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/Connectors.AzureCosmosDBNoSQL.UnitTests.csproj b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/Connectors.AzureCosmosDBNoSQL.UnitTests.csproj new file mode 100644 index 000000000000..ff8643740f11 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureCosmosDBNoSQL.UnitTests/Connectors.AzureCosmosDBNoSQL.UnitTests.csproj @@ -0,0 +1,32 @@ + + + + SemanticKernel.Connectors.AzureCosmosDBNoSQL.UnitTests + SemanticKernel.Connectors.AzureCosmosDBNoSQL.UnitTests + net8.0 + true + enable + disable + false + $(NoWarn);SKEXP0001,SKEXP0020 + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchServiceCollectionExtensions.cs index 7e2de2e8e83e..ba518ddf6724 100644 --- a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchServiceCollectionExtensions.cs @@ -3,10 +3,13 @@ using System; using Azure; using Azure.Core; +using Azure.Core.Serialization; +using Azure.Search.Documents; using Azure.Search.Documents.Indexes; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel.Connectors.AzureAISearch; using Microsoft.SemanticKernel.Data; +using Microsoft.SemanticKernel.Http; namespace Microsoft.SemanticKernel; @@ -59,9 +62,19 @@ public static IServiceCollection AddAzureAISearchVectorStore(this IServiceCollec serviceId, (sp, obj) => { - var searchIndexClient = new SearchIndexClient(endpoint, tokenCredential); var selectedOptions = options ?? sp.GetService(); + // Build options for the Azure AI Search client and construct it. + var searchClientOptions = new SearchClientOptions(); + searchClientOptions.Diagnostics.ApplicationId = HttpHeaderConstant.Values.UserAgent; + if (selectedOptions?.JsonSerializerOptions != null) + { + searchClientOptions.Serializer = new JsonObjectSerializer(selectedOptions.JsonSerializerOptions); + } + + var searchIndexClient = new SearchIndexClient(endpoint, tokenCredential, searchClientOptions); + + // Construct the vector store. return new AzureAISearchVectorStore( searchIndexClient, selectedOptions); @@ -88,9 +101,19 @@ public static IServiceCollection AddAzureAISearchVectorStore(this IServiceCollec serviceId, (sp, obj) => { - var searchIndexClient = new SearchIndexClient(endpoint, credential); var selectedOptions = options ?? sp.GetService(); + // Build options for the Azure AI Search client and construct it. + var searchClientOptions = new SearchClientOptions(); + searchClientOptions.Diagnostics.ApplicationId = HttpHeaderConstant.Values.UserAgent; + if (selectedOptions?.JsonSerializerOptions != null) + { + searchClientOptions.Serializer = new JsonObjectSerializer(selectedOptions.JsonSerializerOptions); + } + + var searchIndexClient = new SearchIndexClient(endpoint, credential, searchClientOptions); + + // Construct the vector store. return new AzureAISearchVectorStore( searchIndexClient, selectedOptions); diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStore.cs b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStore.cs index 2ca2bf9577f5..5a6b7e73b229 100644 --- a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStore.cs +++ b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStore.cs @@ -56,8 +56,16 @@ public IVectorStoreRecordCollection GetCollection( return this._options.VectorStoreCollectionFactory.CreateVectorStoreRecordCollection(this._searchIndexClient, name, vectorStoreRecordDefinition); } - var directlyCreatedStore = new AzureAISearchVectorStoreRecordCollection(this._searchIndexClient, name, new AzureAISearchVectorStoreRecordCollectionOptions() { VectorStoreRecordDefinition = vectorStoreRecordDefinition }) as IVectorStoreRecordCollection; - return directlyCreatedStore!; + var recordCollection = new AzureAISearchVectorStoreRecordCollection( + this._searchIndexClient, + name, + new AzureAISearchVectorStoreRecordCollectionOptions() + { + JsonSerializerOptions = this._options.JsonSerializerOptions, + VectorStoreRecordDefinition = vectorStoreRecordDefinition + }) as IVectorStoreRecordCollection; + + return recordCollection!; } /// diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreOptions.cs b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreOptions.cs index e8d54c8b7740..06e099efc4fa 100644 --- a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreOptions.cs +++ b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreOptions.cs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using Azure.Search.Documents.Indexes; + namespace Microsoft.SemanticKernel.Connectors.AzureAISearch; /// @@ -8,7 +11,14 @@ namespace Microsoft.SemanticKernel.Connectors.AzureAISearch; public sealed class AzureAISearchVectorStoreOptions { /// - /// An optional factory to use for constructing instances, if custom options are required. + /// An optional factory to use for constructing instances, if a custom record collection is required. /// public IAzureAISearchVectorStoreRecordCollectionFactory? VectorStoreCollectionFactory { get; init; } + + /// + /// Gets or sets the JSON serializer options to use when converting between the data model and the Azure AI Search record. + /// Note that when using the default mapper and you are constructing your own , you will need + /// to provide the same set of both here and when constructing the . + /// + public JsonSerializerOptions? JsonSerializerOptions { get; init; } = null; } diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreRecordCollectionOptions.cs b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreRecordCollectionOptions.cs index 462dcd5d6e66..62e524a1c7b1 100644 --- a/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreRecordCollectionOptions.cs +++ b/dotnet/src/Connectors/Connectors.Memory.AzureAISearch/AzureAISearchVectorStoreRecordCollectionOptions.cs @@ -33,7 +33,8 @@ public sealed class AzureAISearchVectorStoreRecordCollectionOptions /// /// Gets or sets the JSON serializer options to use when converting between the data model and the Azure AI Search record. - /// Note that when using the default mapper, you will need to provide the same set of both here and when constructing the . + /// Note that when using the default mapper and you are constructing your own , you will need + /// to provide the same set of both here and when constructing the . /// public JsonSerializerOptions? JsonSerializerOptions { get; init; } = null; } diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBConstants.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBConstants.cs new file mode 100644 index 000000000000..a5bf87d1a960 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBConstants.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; + +/// +/// Constants for Azure CosmosDB MongoDB vector store implementation. +/// +internal static class AzureCosmosDBMongoDBConstants +{ + /// Reserved key property name in Azure CosmosDB MongoDB. + internal const string MongoReservedKeyPropertyName = "_id"; +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBKernelBuilderExtensions.cs new file mode 100644 index 000000000000..807bb030dcfc --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBKernelBuilderExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; +using Microsoft.SemanticKernel.Data; +using MongoDB.Driver; + +namespace Microsoft.SemanticKernel; + +/// +/// Extension methods to register Azure CosmosDB MongoDB instances on the . +/// +public static class AzureCosmosDBMongoDBKernelBuilderExtensions +{ + /// + /// Register a Azure CosmosDB MongoDB with the specified service ID + /// and where the Azure CosmosDB MongoDB is retrieved from the dependency injection container. + /// + /// The builder to register the on. + /// Optional options to further configure the . + /// An optional service id to use as the service key. + /// The kernel builder. + public static IKernelBuilder AddAzureCosmosDBMongoDBVectorStore( + this IKernelBuilder builder, + AzureCosmosDBMongoDBVectorStoreOptions? options = default, + string? serviceId = default) + { + builder.Services.AddAzureCosmosDBMongoDBVectorStore(options, serviceId); + return builder; + } + + /// + /// Register a Azure CosmosDB MongoDB with the specified service ID + /// and where the Azure CosmosDB MongoDB is constructed using the provided and . + /// + /// The builder to register the on. + /// Connection string required to connect to Azure CosmosDB MongoDB. + /// Database name for Azure CosmosDB MongoDB. + /// Optional options to further configure the . + /// An optional service id to use as the service key. + /// The kernel builder. + public static IKernelBuilder AddAzureCosmosDBMongoDBVectorStore( + this IKernelBuilder builder, + string connectionString, + string databaseName, + AzureCosmosDBMongoDBVectorStoreOptions? options = default, + string? serviceId = default) + { + builder.Services.AddAzureCosmosDBMongoDBVectorStore(connectionString, databaseName, options, serviceId); + return builder; + } +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBNamingConvention.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBNamingConvention.cs new file mode 100644 index 000000000000..bece10b432d6 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBNamingConvention.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; + +/// +/// Naming convention for storage properties based on provided name mapping. +/// +internal sealed class AzureCosmosDBMongoDBNamingConvention(IReadOnlyDictionary nameMapping) : IMemberMapConvention +{ + private readonly IReadOnlyDictionary _nameMapping = nameMapping; + + public string Name => nameof(AzureCosmosDBMongoDBNamingConvention); + + public void Apply(BsonMemberMap memberMap) + { + var memberName = memberMap.MemberName; + var name = this._nameMapping.TryGetValue(memberName, out var customName) ? customName : memberName; + + memberMap.SetElementName(name); + } +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBServiceCollectionExtensions.cs new file mode 100644 index 000000000000..02f26e85ee94 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBServiceCollectionExtensions.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; +using Microsoft.SemanticKernel.Data; +using MongoDB.Driver; + +namespace Microsoft.SemanticKernel; + +/// +/// Extension methods to register Azure CosmosDB MongoDB instances on an . +/// +public static class AzureCosmosDBMongoDBServiceCollectionExtensions +{ + /// + /// Register a Azure CosmosDB MongoDB with the specified service ID + /// and where the Azure CosmosDB MongoDB is retrieved from the dependency injection container. + /// + /// The to register the on. + /// Optional options to further configure the . + /// An optional service id to use as the service key. + /// Service collection. + public static IServiceCollection AddAzureCosmosDBMongoDBVectorStore( + this IServiceCollection services, + AzureCosmosDBMongoDBVectorStoreOptions? options = default, + string? serviceId = default) + { + // If we are not constructing MongoDatabase, add the IVectorStore as transient, since we + // cannot make assumptions about how MongoDatabase is being managed. + services.AddKeyedTransient( + serviceId, + (sp, obj) => + { + var database = sp.GetRequiredService(); + var selectedOptions = options ?? sp.GetService(); + + return new AzureCosmosDBMongoDBVectorStore(database, options); + }); + + return services; + } + + /// + /// Register a Azure CosmosDB MongoDB with the specified service ID + /// and where the Azure CosmosDB MongoDB is constructed using the provided and . + /// + /// The to register the on. + /// Connection string required to connect to Azure CosmosDB MongoDB. + /// Database name for Azure CosmosDB MongoDB. + /// Optional options to further configure the . + /// An optional service id to use as the service key. + /// Service collection. + public static IServiceCollection AddAzureCosmosDBMongoDBVectorStore( + this IServiceCollection services, + string connectionString, + string databaseName, + AzureCosmosDBMongoDBVectorStoreOptions? options = default, + string? serviceId = default) + { + // If we are constructing IMongoDatabase, add the IVectorStore as singleton, since we are managing the lifetime of it, + // and the recommendation from Mongo is to register it with a singleton lifetime. + services.AddKeyedSingleton( + serviceId, + (sp, obj) => + { + var settings = MongoClientSettings.FromConnectionString(connectionString); + var mongoClient = new MongoClient(settings); + var database = mongoClient.GetDatabase(databaseName); + + var selectedOptions = options ?? sp.GetService(); + + return new AzureCosmosDBMongoDBVectorStore(database, options); + }); + + return services; + } +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStore.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStore.cs new file mode 100644 index 000000000000..7f907d068983 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStore.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using Microsoft.SemanticKernel.Data; +using MongoDB.Driver; + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; + +/// +/// Class for accessing the list of collections in a Azure CosmosDB MongoDB vector store. +/// +/// +/// This class can be used with collections of any schema type, but requires you to provide schema information when getting a collection. +/// +public sealed class AzureCosmosDBMongoDBVectorStore : IVectorStore +{ + /// that can be used to manage the collections in Azure CosmosDB MongoDB. + private readonly IMongoDatabase _mongoDatabase; + + /// Optional configuration options for this class. + private readonly AzureCosmosDBMongoDBVectorStoreOptions _options; + + /// + /// Initializes a new instance of the class. + /// + /// that can be used to manage the collections in Azure CosmosDB MongoDB. + /// Optional configuration options for this class. + public AzureCosmosDBMongoDBVectorStore(IMongoDatabase mongoDatabase, AzureCosmosDBMongoDBVectorStoreOptions? options = default) + { + Verify.NotNull(mongoDatabase); + + this._mongoDatabase = mongoDatabase; + this._options = options ?? new(); + } + + /// + public IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null) + where TKey : notnull + where TRecord : class + { + if (typeof(TKey) != typeof(string)) + { + throw new NotSupportedException("Only string keys are supported."); + } + + if (this._options.VectorStoreCollectionFactory is not null) + { + return this._options.VectorStoreCollectionFactory.CreateVectorStoreRecordCollection(this._mongoDatabase, name, vectorStoreRecordDefinition); + } + + var recordCollection = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + this._mongoDatabase, + name, + new() { VectorStoreRecordDefinition = vectorStoreRecordDefinition }) as IVectorStoreRecordCollection; + + return recordCollection!; + } + + /// + public async IAsyncEnumerable ListCollectionNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + using var cursor = await this._mongoDatabase + .ListCollectionNamesAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + while (await cursor.MoveNextAsync(cancellationToken).ConfigureAwait(false)) + { + foreach (var name in cursor.Current) + { + yield return name; + } + } + } +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreOptions.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreOptions.cs new file mode 100644 index 000000000000..08df3aef81d8 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreOptions.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; + +/// +/// Options when creating a +/// +public sealed class AzureCosmosDBMongoDBVectorStoreOptions +{ + /// + /// An optional factory to use for constructing instances, if a custom record collection is required. + /// + public IAzureCosmosDBMongoDBVectorStoreRecordCollectionFactory? VectorStoreCollectionFactory { get; init; } +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollection.cs new file mode 100644 index 000000000000..2d409ef61c54 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollection.cs @@ -0,0 +1,431 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Data; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; + +/// +/// Service for storing and retrieving vector records, that uses Azure CosmosDB MongoDB as the underlying storage. +/// +/// The data model to use for adding, updating and retrieving data from storage. +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix +public sealed class AzureCosmosDBMongoDBVectorStoreRecordCollection : IVectorStoreRecordCollection where TRecord : class +#pragma warning restore CA1711 // Identifiers should not have incorrect suffix +{ + /// The name of this database for telemetry purposes. + private const string DatabaseName = "AzureCosmosDBMongoDB"; + + /// that can be used to manage the collections in Azure CosmosDB MongoDB. + private readonly IMongoDatabase _mongoDatabase; + + /// Azure CosmosDB MongoDB collection to perform record operations. + private readonly IMongoCollection _mongoCollection; + + /// Optional configuration options for this class. + private readonly AzureCosmosDBMongoDBVectorStoreRecordCollectionOptions _options; + + /// A definition of the current storage model. + private readonly VectorStoreRecordDefinition _vectorStoreRecordDefinition; + + /// Interface for mapping between a storage model, and the consumer record data model. + private readonly IVectorStoreRecordMapper _mapper; + + /// A dictionary that maps from a property name to the storage name that should be used when serializing it for data and vector properties. + private readonly Dictionary _storagePropertyNames; + + /// Collection of vector storage property names. + private readonly List _vectorStoragePropertyNames; + + /// Collection of record vector properties. + private readonly List _vectorProperties; + + /// + public string CollectionName { get; } + + /// + /// Initializes a new instance of the class. + /// + /// that can be used to manage the collections in Azure CosmosDB MongoDB. + /// The name of the collection that this will access. + /// Optional configuration options for this class. + public AzureCosmosDBMongoDBVectorStoreRecordCollection( + IMongoDatabase mongoDatabase, + string collectionName, + AzureCosmosDBMongoDBVectorStoreRecordCollectionOptions? options = default) + { + // Verify. + Verify.NotNull(mongoDatabase); + Verify.NotNullOrWhiteSpace(collectionName); + + // Assign. + this._mongoDatabase = mongoDatabase; + this._mongoCollection = mongoDatabase.GetCollection(collectionName); + this.CollectionName = collectionName; + this._options = options ?? new AzureCosmosDBMongoDBVectorStoreRecordCollectionOptions(); + this._vectorStoreRecordDefinition = this._options.VectorStoreRecordDefinition ?? VectorStoreRecordPropertyReader.CreateVectorStoreRecordDefinitionFromType(typeof(TRecord), true); + + var properties = VectorStoreRecordPropertyReader.SplitDefinitionAndVerify( + typeof(TRecord).Name, + this._vectorStoreRecordDefinition, + supportsMultipleVectors: true, + requiresAtLeastOneVector: false); + + this._storagePropertyNames = GetStoragePropertyNames(properties, typeof(TRecord)); + this._vectorProperties = properties.VectorProperties; + this._vectorStoragePropertyNames = this._vectorProperties.Select(property => this._storagePropertyNames[property.DataModelPropertyName]).ToList(); + + this._mapper = this._options.BsonDocumentCustomMapper ?? + new AzureCosmosDBMongoDBVectorStoreRecordMapper(this._vectorStoreRecordDefinition, this._storagePropertyNames); + } + + /// + public Task CollectionExistsAsync(CancellationToken cancellationToken = default) + => this.RunOperationAsync("ListCollectionNames", () => this.InternalCollectionExistsAsync(cancellationToken)); + + /// + public async Task CreateCollectionAsync(CancellationToken cancellationToken = default) + { + await this.RunOperationAsync("CreateCollection", + () => this._mongoDatabase.CreateCollectionAsync(this.CollectionName, cancellationToken: cancellationToken)).ConfigureAwait(false); + + await this.RunOperationAsync("CreateIndex", + () => this.CreateIndexAsync(this.CollectionName, cancellationToken: cancellationToken)).ConfigureAwait(false); + } + + /// + public async Task CreateCollectionIfNotExistsAsync(CancellationToken cancellationToken = default) + { + if (!await this.CollectionExistsAsync(cancellationToken).ConfigureAwait(false)) + { + await this.CreateCollectionAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + public async Task DeleteAsync(string key, DeleteRecordOptions? options = null, CancellationToken cancellationToken = default) + { + Verify.NotNullOrWhiteSpace(key); + + await this.RunOperationAsync("DeleteOne", () => this._mongoCollection.DeleteOneAsync(this.GetFilterById(key), cancellationToken)) + .ConfigureAwait(false); + } + + /// + public async Task DeleteBatchAsync(IEnumerable keys, DeleteRecordOptions? options = null, CancellationToken cancellationToken = default) + { + Verify.NotNull(keys); + + await this.RunOperationAsync("DeleteMany", () => this._mongoCollection.DeleteManyAsync(this.GetFilterByIds(keys), cancellationToken)) + .ConfigureAwait(false); + } + + /// + public Task DeleteCollectionAsync(CancellationToken cancellationToken = default) + => this.RunOperationAsync("DropCollection", () => this._mongoDatabase.DropCollectionAsync(this.CollectionName, cancellationToken)); + + /// + public async Task GetAsync(string key, GetRecordOptions? options = null, CancellationToken cancellationToken = default) + { + Verify.NotNullOrWhiteSpace(key); + + const string OperationName = "Find"; + + var record = await this.RunOperationAsync(OperationName, async () => + { + using var cursor = await this + .FindAsync(this.GetFilterById(key), options, cancellationToken) + .ConfigureAwait(false); + + return await cursor.SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + + if (record is null) + { + return null; + } + + return VectorStoreErrorHandler.RunModelConversion( + DatabaseName, + this.CollectionName, + OperationName, + () => this._mapper.MapFromStorageToDataModel(record, new())); + } + + /// + public async IAsyncEnumerable GetBatchAsync( + IEnumerable keys, + GetRecordOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + Verify.NotNull(keys); + + const string OperationName = "Find"; + + using var cursor = await this + .FindAsync(this.GetFilterByIds(keys), options, cancellationToken) + .ConfigureAwait(false); + + while (await cursor.MoveNextAsync(cancellationToken).ConfigureAwait(false)) + { + foreach (var record in cursor.Current) + { + if (record is not null) + { + yield return VectorStoreErrorHandler.RunModelConversion( + DatabaseName, + this.CollectionName, + OperationName, + () => this._mapper.MapFromStorageToDataModel(record, new())); + } + } + } + } + + /// + public Task UpsertAsync(TRecord record, UpsertRecordOptions? options = null, CancellationToken cancellationToken = default) + { + Verify.NotNull(record); + + const string OperationName = "ReplaceOne"; + + var replaceOptions = new ReplaceOptions { IsUpsert = true }; + var storageModel = VectorStoreErrorHandler.RunModelConversion( + DatabaseName, + this.CollectionName, + OperationName, + () => this._mapper.MapFromDataToStorageModel(record)); + + var key = storageModel[AzureCosmosDBMongoDBConstants.MongoReservedKeyPropertyName].AsString; + + return this.RunOperationAsync(OperationName, async () => + { + await this._mongoCollection + .ReplaceOneAsync(this.GetFilterById(key), storageModel, replaceOptions, cancellationToken) + .ConfigureAwait(false); + + return key; + }); + } + + /// + public async IAsyncEnumerable UpsertBatchAsync( + IEnumerable records, + UpsertRecordOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + Verify.NotNull(records); + + var tasks = records.Select(record => this.UpsertAsync(record, options, cancellationToken)); + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + + foreach (var result in results) + { + if (result is not null) + { + yield return result; + } + } + } + + #region private + + private async Task CreateIndexAsync(string collectionName, CancellationToken cancellationToken) + { + var indexCursor = await this._mongoCollection.Indexes.ListAsync(cancellationToken).ConfigureAwait(false); + var indexes = indexCursor.ToList(cancellationToken).Select(index => index["name"].ToString()) ?? []; + var uniqueIndexes = new HashSet(indexes); + + var indexArray = new BsonArray(); + + // Create separate index for each vector property + foreach (var property in this._vectorStoreRecordDefinition.Properties.OfType()) + { + // Use index name same as vector property name with underscore + var vectorPropertyName = this._storagePropertyNames[property.DataModelPropertyName]; + var indexName = $"{vectorPropertyName}_"; + + // If index already exists, proceed to the next vector property + if (uniqueIndexes.Contains(indexName)) + { + continue; + } + + // Otherwise, create a new index + var searchOptions = new BsonDocument + { + { "kind", GetIndexKind(property.IndexKind, vectorPropertyName) }, + { "numLists", this._options.NumLists }, + { "similarity", GetDistanceFunction(property.DistanceFunction, vectorPropertyName) }, + { "dimensions", property.Dimensions } + }; + + if (this._options.EfConstruction is not null) + { + searchOptions["efConstruction"] = this._options.EfConstruction; + } + + var indexDocument = new BsonDocument + { + ["name"] = indexName, + ["key"] = new BsonDocument { [vectorPropertyName] = "cosmosSearch" }, + ["cosmosSearchOptions"] = searchOptions + }; + + indexArray.Add(indexDocument); + } + + if (indexArray.Count > 0) + { + var createIndexCommand = new BsonDocument + { + { "createIndexes", collectionName }, + { "indexes", indexArray } + }; + + await this._mongoDatabase.RunCommandAsync(createIndexCommand, cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + + private async Task> FindAsync(FilterDefinition filter, GetRecordOptions? options, CancellationToken cancellationToken) + { + ProjectionDefinitionBuilder projectionBuilder = Builders.Projection; + ProjectionDefinition? projectionDefinition = null; + + var includeVectors = options?.IncludeVectors ?? false; + + if (!includeVectors && this._vectorStoragePropertyNames.Count > 0) + { + foreach (var vectorPropertyName in this._vectorStoragePropertyNames) + { + projectionDefinition = projectionDefinition is not null ? + projectionDefinition.Exclude(vectorPropertyName) : + projectionBuilder.Exclude(vectorPropertyName); + } + } + + var findOptions = projectionDefinition is not null ? + new FindOptions { Projection = projectionDefinition } : + null; + + return await this._mongoCollection.FindAsync(filter, findOptions, cancellationToken).ConfigureAwait(false); + } + + private FilterDefinition GetFilterById(string id) + => Builders.Filter.Eq(document => document[AzureCosmosDBMongoDBConstants.MongoReservedKeyPropertyName], id); + + private FilterDefinition GetFilterByIds(IEnumerable ids) + => Builders.Filter.In(document => document[AzureCosmosDBMongoDBConstants.MongoReservedKeyPropertyName].AsString, ids); + + private async Task InternalCollectionExistsAsync(CancellationToken cancellationToken) + { + var filter = new BsonDocument("name", this.CollectionName); + var options = new ListCollectionNamesOptions { Filter = filter }; + + using var cursor = await this._mongoDatabase.ListCollectionNamesAsync(options, cancellationToken: cancellationToken).ConfigureAwait(false); + + return await cursor.AnyAsync(cancellationToken).ConfigureAwait(false); + } + + private async Task RunOperationAsync(string operationName, Func operation) + { + try + { + await operation.Invoke().ConfigureAwait(false); + } + catch (Exception ex) + { + throw new VectorStoreOperationException("Call to vector store failed.", ex) + { + VectorStoreType = DatabaseName, + CollectionName = this.CollectionName, + OperationName = operationName + }; + } + } + + private async Task RunOperationAsync(string operationName, Func> operation) + { + try + { + return await operation.Invoke().ConfigureAwait(false); + } + catch (Exception ex) + { + throw new VectorStoreOperationException("Call to vector store failed.", ex) + { + VectorStoreType = DatabaseName, + CollectionName = this.CollectionName, + OperationName = operationName + }; + } + } + + /// + /// More information about Azure CosmosDB for MongoDB index kinds here: . + /// + private static string GetIndexKind(string? indexKind, string vectorPropertyName) + { + return indexKind switch + { + IndexKind.Hnsw => "vector-hnsw", + IndexKind.IvfFlat => "vector-ivf", + _ => throw new InvalidOperationException($"Index kind '{indexKind}' on {nameof(VectorStoreRecordVectorProperty)} '{vectorPropertyName}' is not supported by the Azure CosmosDB for MongoDB VectorStore.") + }; + } + + /// + /// More information about Azure CosmosDB for MongoDB distance functions here: . + /// + private static string GetDistanceFunction(string? distanceFunction, string vectorPropertyName) + { + return distanceFunction switch + { + DistanceFunction.CosineDistance => "COS", + DistanceFunction.DotProductSimilarity => "IP", + DistanceFunction.EuclideanDistance => "L2", + _ => throw new InvalidOperationException($"Distance function '{distanceFunction}' for {nameof(VectorStoreRecordVectorProperty)} '{vectorPropertyName}' is not supported by the Azure CosmosDB for MongoDB VectorStore.") + }; + } + + /// + /// Gets storage property names taking into account BSON serialization attributes. + /// + private static Dictionary GetStoragePropertyNames( + (VectorStoreRecordKeyProperty KeyProperty, List DataProperties, List VectorProperties) properties, + Type dataModel) + { + var storagePropertyNames = VectorStoreRecordPropertyReader.BuildPropertyNameToStorageNameMap(properties); + + var allProperties = new List([properties.KeyProperty]) + .Concat(properties.DataProperties) + .Concat(properties.VectorProperties); + + foreach (var property in allProperties) + { + var propertyInfo = dataModel.GetProperty(property.DataModelPropertyName); + + if (propertyInfo != null) + { + var bsonElementAttribute = propertyInfo.GetCustomAttribute(); + if (bsonElementAttribute is not null) + { + storagePropertyNames[property.DataModelPropertyName] = bsonElementAttribute.ElementName; + } + } + } + + return storagePropertyNames; + } + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollectionOptions.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollectionOptions.cs new file mode 100644 index 000000000000..11b21a1e84e7 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollectionOptions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Data; +using MongoDB.Bson; + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; + +/// +/// Options when creating a . +/// +public sealed class AzureCosmosDBMongoDBVectorStoreRecordCollectionOptions where TRecord : class +{ + /// + /// Gets or sets an optional custom mapper to use when converting between the data model and the Azure CosmosDB MongoDB BSON object. + /// + public IVectorStoreRecordMapper? BsonDocumentCustomMapper { get; init; } = null; + + /// + /// Gets or sets an optional record definition that defines the schema of the record type. + /// + /// + /// If not provided, the schema will be inferred from the record model class using reflection. + /// In this case, the record model properties must be annotated with the appropriate attributes to indicate their usage. + /// See , and . + /// + public VectorStoreRecordDefinition? VectorStoreRecordDefinition { get; init; } = null; + + /// + /// This integer is the number of clusters that the inverted file (IVF) index uses to group the vector data. Default is 1. + /// We recommend that numLists is set to documentCount/1000 for up to 1 million documents and to sqrt(documentCount) + /// for more than 1 million documents. Using a numLists value of 1 is akin to performing brute-force search, which has + /// limited performance. + /// + public int NumLists { get; set; } = 1; + + /// + /// The size of the dynamic candidate list for constructing the graph (64 by default, minimum value is 4, + /// maximum value is 1000). Higher ef_construction will result in better index quality and higher accuracy, but it will + /// also increase the time required to build the index. EfConstruction has to be at least 2 * m + /// + public int? EfConstruction { get; set; } = null; +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordMapper.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordMapper.cs new file mode 100644 index 000000000000..5e23aa506956 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordMapper.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.SemanticKernel.Data; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; + +internal sealed class AzureCosmosDBMongoDBVectorStoreRecordMapper : IVectorStoreRecordMapper + where TRecord : class +{ + /// A set of types that a key on the provided model may have. + private static readonly HashSet s_supportedKeyTypes = + [ + typeof(string) + ]; + + /// A set of types that data properties on the provided model may have. + private static readonly HashSet s_supportedDataTypes = + [ + typeof(bool), + typeof(bool?), + typeof(string), + typeof(int), + typeof(int?), + typeof(long), + typeof(long?), + typeof(float), + typeof(float?), + typeof(double), + typeof(double?), + typeof(decimal), + typeof(decimal?), + ]; + + /// A set of types that vectors on the provided model may have. + private static readonly HashSet s_supportedVectorTypes = + [ + typeof(ReadOnlyMemory), + typeof(ReadOnlyMemory?), + typeof(ReadOnlyMemory), + typeof(ReadOnlyMemory?) + ]; + + /// A dictionary that maps from a property name to the storage name. + private readonly Dictionary _storagePropertyNames; + + /// + /// Initializes a new instance of the class. + /// + /// The record definition that defines the schema of the record type. + /// A dictionary that maps from a property name to the configured name that should be used when storing it. + public AzureCosmosDBMongoDBVectorStoreRecordMapper(VectorStoreRecordDefinition vectorStoreRecordDefinition, Dictionary storagePropertyNames) + { + var (keyProperty, dataProperties, vectorProperties) = VectorStoreRecordPropertyReader.FindProperties(typeof(TRecord), vectorStoreRecordDefinition, supportsMultipleVectors: true); + + VectorStoreRecordPropertyReader.VerifyPropertyTypes([keyProperty], s_supportedKeyTypes, "Key"); + VectorStoreRecordPropertyReader.VerifyPropertyTypes(dataProperties, s_supportedDataTypes, "Data", supportEnumerable: true); + VectorStoreRecordPropertyReader.VerifyPropertyTypes(vectorProperties, s_supportedVectorTypes, "Vector"); + + this._storagePropertyNames = storagePropertyNames; + + // Use Mongo reserved key property name as storage key property name + this._storagePropertyNames[keyProperty.Name] = AzureCosmosDBMongoDBConstants.MongoReservedKeyPropertyName; + + var conventionPack = new ConventionPack + { + new IgnoreExtraElementsConvention(ignoreExtraElements: true), + new AzureCosmosDBMongoDBNamingConvention(this._storagePropertyNames) + }; + + ConventionRegistry.Register( + nameof(AzureCosmosDBMongoDBVectorStoreRecordMapper), + conventionPack, + type => type == typeof(TRecord)); + } + + public BsonDocument MapFromDataToStorageModel(TRecord dataModel) + => dataModel.ToBsonDocument(); + + public TRecord MapFromStorageToDataModel(BsonDocument storageModel, StorageToDataModelMapperOptions options) + => BsonSerializer.Deserialize(storageModel); +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/Connectors.Memory.AzureCosmosDBMongoDB.csproj b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/Connectors.Memory.AzureCosmosDBMongoDB.csproj index 747709f993cc..9ce9d24d1aed 100644 --- a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/Connectors.Memory.AzureCosmosDBMongoDB.csproj +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/Connectors.Memory.AzureCosmosDBMongoDB.csproj @@ -23,6 +23,10 @@ + + + + diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/IAzureCosmosDBMongoDBVectorStoreRecordCollectionFactory.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/IAzureCosmosDBMongoDBVectorStoreRecordCollectionFactory.cs new file mode 100644 index 000000000000..39231a8bf7a8 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBMongoDB/IAzureCosmosDBMongoDBVectorStoreRecordCollectionFactory.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Data; +using MongoDB.Driver; + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; + +/// +/// Interface for constructing Azure CosmosDB MongoDB instances when using to retrieve these. +/// +public interface IAzureCosmosDBMongoDBVectorStoreRecordCollectionFactory +{ + /// + /// Constructs a new instance of the . + /// + /// The data type of the record key. + /// The data model to use for adding, updating and retrieving data from storage. + /// that can be used to manage the collections in Azure CosmosDB MongoDB. + /// The name of the collection to connect to. + /// An optional record definition that defines the schema of the record type. If not present, attributes on will be used. + /// The new instance of . + IVectorStoreRecordCollection CreateVectorStoreRecordCollection(IMongoDatabase mongoDatabase, string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition) + where TKey : notnull + where TRecord : class; +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLCompositeKey.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLCompositeKey.cs new file mode 100644 index 000000000000..24ec91e9ba12 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLCompositeKey.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; + +/// +/// Composite key for Azure CosmosDB NoSQL record with record and partition keys. +/// +public sealed class AzureCosmosDBNoSQLCompositeKey(string recordKey, string partitionKey) +{ + /// + /// Value of record key. + /// + public string RecordKey { get; } = recordKey; + + /// + /// Value of partition key. + /// + public string PartitionKey { get; } = partitionKey; +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLConstants.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLConstants.cs new file mode 100644 index 000000000000..2c808307217f --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLConstants.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; + +internal static class AzureCosmosDBNoSQLConstants +{ + /// Reserved key property name in Azure CosmosDB NoSQL. + internal const string ReservedKeyPropertyName = "id"; +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLKernelBuilderExtensions.cs new file mode 100644 index 000000000000..0f1e3744f36c --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLKernelBuilderExtensions.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Azure.Cosmos; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; +using Microsoft.SemanticKernel.Data; + +namespace Microsoft.SemanticKernel; + +/// +/// Extension methods to register Azure CosmosDB NoSQL instances on the . +/// +public static class AzureCosmosDBNoSQLKernelBuilderExtensions +{ + /// + /// Register an Azure CosmosDB NoSQL with the specified service ID + /// and where the Azure CosmosDB NoSQL is retrieved from the dependency injection container. + /// + /// The builder to register the on. + /// Optional options to further configure the . + /// An optional service id to use as the service key. + /// The kernel builder. + public static IKernelBuilder AddAzureCosmosDBNoSQLVectorStore( + this IKernelBuilder builder, + AzureCosmosDBNoSQLVectorStoreOptions? options = default, + string? serviceId = default) + { + builder.Services.AddAzureCosmosDBNoSQLVectorStore(options, serviceId); + return builder; + } + + /// + /// Register an Azure CosmosDB NoSQL with the specified service ID + /// and where the Azure CosmosDB NoSQL is constructed using the provided and . + /// + /// The builder to register the on. + /// Connection string required to connect to Azure CosmosDB NoSQL. + /// Database name for Azure CosmosDB NoSQL. + /// Optional options to further configure the . + /// An optional service id to use as the service key. + /// The kernel builder. + public static IKernelBuilder AddAzureCosmosDBNoSQLVectorStore( + this IKernelBuilder builder, + string connectionString, + string databaseName, + AzureCosmosDBNoSQLVectorStoreOptions? options = default, + string? serviceId = default) + { + builder.Services.AddAzureCosmosDBNoSQLVectorStore( + connectionString, + databaseName, + options, + serviceId); + + return builder; + } +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLServiceCollectionExtensions.cs new file mode 100644 index 000000000000..26bcc7fd48cd --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLServiceCollectionExtensions.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; +using Microsoft.SemanticKernel.Data; + +namespace Microsoft.SemanticKernel; + +/// +/// Extension methods to register Azure CosmosDB NoSQL instances on an . +/// +public static class AzureCosmosDBNoSQLServiceCollectionExtensions +{ + /// + /// Register an Azure CosmosDB NoSQL with the specified service ID + /// and where the Azure CosmosDB NoSQL is retrieved from the dependency injection container. + /// + /// The to register the on. + /// Optional options to further configure the . + /// An optional service id to use as the service key. + /// Service collection. + public static IServiceCollection AddAzureCosmosDBNoSQLVectorStore( + this IServiceCollection services, + AzureCosmosDBNoSQLVectorStoreOptions? options = default, + string? serviceId = default) + { + // If we are not constructing Database, add the IVectorStore as transient, since we + // cannot make assumptions about how Database is being managed. + services.AddKeyedTransient( + serviceId, + (sp, obj) => + { + var database = sp.GetRequiredService(); + var selectedOptions = options ?? sp.GetService(); + return new AzureCosmosDBNoSQLVectorStore(database, options); + }); + + return services; + } + + /// + /// Register an Azure CosmosDB NoSQL with the specified service ID + /// and where the Azure CosmosDB NoSQL is constructed using the provided and . + /// + /// The to register the on. + /// Connection string required to connect to Azure CosmosDB NoSQL. + /// Database name for Azure CosmosDB NoSQL. + /// Optional options to further configure the . + /// An optional service id to use as the service key. + /// Service collection. + public static IServiceCollection AddAzureCosmosDBNoSQLVectorStore( + this IServiceCollection services, + string connectionString, + string databaseName, + AzureCosmosDBNoSQLVectorStoreOptions? options = default, + string? serviceId = default) + { + // If we are constructing Database, add the IVectorStore as singleton. + services.AddKeyedSingleton( + serviceId, + (sp, obj) => + { + var cosmosClient = new CosmosClient(connectionString, new() + { + Serializer = new CosmosSystemTextJsonSerializer(options?.JsonSerializerOptions ?? JsonSerializerOptions.Default) + }); + + var database = cosmosClient.GetDatabase(databaseName); + var selectedOptions = options ?? sp.GetService(); + return new AzureCosmosDBNoSQLVectorStore(database, options); + }); + + return services; + } +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStore.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStore.cs new file mode 100644 index 000000000000..ea1ec083a484 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStore.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using Microsoft.Azure.Cosmos; +using Microsoft.SemanticKernel.Data; + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; + +/// +/// Class for accessing the list of collections in a Azure CosmosDB NoSQL vector store. +/// +/// +/// This class can be used with collections of any schema type, but requires you to provide schema information when getting a collection. +/// +public sealed class AzureCosmosDBNoSQLVectorStore : IVectorStore +{ + /// that can be used to manage the collections in Azure CosmosDB NoSQL. + private readonly Database _database; + + /// Optional configuration options for this class. + private readonly AzureCosmosDBNoSQLVectorStoreOptions _options; + + /// + /// Initializes a new instance of the class. + /// + /// that can be used to manage the collections in Azure CosmosDB NoSQL. + /// Optional configuration options for this class. + public AzureCosmosDBNoSQLVectorStore(Database database, AzureCosmosDBNoSQLVectorStoreOptions? options = null) + { + Verify.NotNull(database); + + this._database = database; + this._options = options ?? new(); + } + + /// + public IVectorStoreRecordCollection GetCollection(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null) + where TKey : notnull + where TRecord : class + { + if (typeof(TKey) != typeof(string) && typeof(TKey) != typeof(AzureCosmosDBNoSQLCompositeKey)) + { + throw new NotSupportedException($"Only {nameof(String)} and {nameof(AzureCosmosDBNoSQLCompositeKey)} keys are supported."); + } + + if (this._options.VectorStoreCollectionFactory is not null) + { + return this._options.VectorStoreCollectionFactory.CreateVectorStoreRecordCollection( + this._database, + name, + vectorStoreRecordDefinition); + } + + var recordCollection = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + this._database, + name, + new() + { + VectorStoreRecordDefinition = vectorStoreRecordDefinition, + JsonSerializerOptions = this._options.JsonSerializerOptions + }) as IVectorStoreRecordCollection; + + return recordCollection!; + } + + /// + public async IAsyncEnumerable ListCollectionNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + const string Query = "SELECT VALUE(c.id) FROM c"; + + using var feedIterator = this._database.GetContainerQueryIterator(Query); + + while (feedIterator.HasMoreResults) + { + var next = await feedIterator.ReadNextAsync(cancellationToken).ConfigureAwait(false); + + foreach (var containerName in next.Resource) + { + yield return containerName; + } + } + } +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreOptions.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreOptions.cs new file mode 100644 index 000000000000..d6f1bef56e0b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; + +/// +/// Options when creating a . +/// +public sealed class AzureCosmosDBNoSQLVectorStoreOptions +{ + /// + /// An optional factory to use for constructing instances, if a custom record collection is required. + /// + public IAzureCosmosDBNoSQLVectorStoreRecordCollectionFactory? VectorStoreCollectionFactory { get; init; } + + /// + /// Gets or sets the JSON serializer options to use when converting between the data model and the Azure CosmosDB NoSQL record. + /// + public JsonSerializerOptions? JsonSerializerOptions { get; init; } +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollection.cs new file mode 100644 index 000000000000..cf443dfcf4ac --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollection.cs @@ -0,0 +1,657 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Cosmos; +using Microsoft.SemanticKernel.Data; +using DistanceFunction = Microsoft.Azure.Cosmos.DistanceFunction; +using IndexKind = Microsoft.SemanticKernel.Data.IndexKind; +using SKDistanceFunction = Microsoft.SemanticKernel.Data.DistanceFunction; + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; + +/// +/// Service for storing and retrieving vector records, that uses Azure CosmosDB NoSQL as the underlying storage. +/// +/// The data model to use for adding, updating and retrieving data from storage. +#pragma warning disable CA1711 // Identifiers should not have incorrect suffix +public sealed class AzureCosmosDBNoSQLVectorStoreRecordCollection : + IVectorStoreRecordCollection, + IVectorStoreRecordCollection + where TRecord : class +#pragma warning restore CA1711 // Identifiers should not have incorrect +{ + /// The name of this database for telemetry purposes. + private const string DatabaseName = "AzureCosmosDBNoSQL"; + + /// A set of types that a key on the provided model may have. + private static readonly HashSet s_supportedKeyTypes = + [ + typeof(string) + ]; + + /// A set of types that data properties on the provided model may have. + private static readonly HashSet s_supportedDataTypes = + [ + typeof(string), + typeof(int), + typeof(long), + typeof(double), + typeof(float), + typeof(bool), + typeof(DateTimeOffset), + typeof(int?), + typeof(long?), + typeof(double?), + typeof(float?), + typeof(bool?), + typeof(DateTimeOffset?), + ]; + + /// A set of types that vector properties on the provided model may have, based on enumeration. + private static readonly HashSet s_supportedVectorTypes = + [ + // Float16 +#if NET5_0_OR_GREATER + typeof(ReadOnlyMemory), + typeof(ReadOnlyMemory?), +#endif + // Float32 + typeof(ReadOnlyMemory), + typeof(ReadOnlyMemory?), + // Uint8 + typeof(ReadOnlyMemory), + typeof(ReadOnlyMemory?), + // Int8 + typeof(ReadOnlyMemory), + typeof(ReadOnlyMemory?), + ]; + + /// that can be used to manage the collections in Azure CosmosDB NoSQL. + private readonly Database _database; + + /// Optional configuration options for this class. + private readonly AzureCosmosDBNoSQLVectorStoreRecordCollectionOptions _options; + + /// A definition of the current storage model. + private readonly VectorStoreRecordDefinition _vectorStoreRecordDefinition; + + /// The storage names of all non vector fields on the current model. + private readonly List _nonVectorStoragePropertyNames = []; + + /// A dictionary that maps from a property name to the storage name that should be used when serializing it to json for data and vector properties. + private readonly Dictionary _storagePropertyNames = []; + + /// The storage name of the key field for the collections that this class is used with. + private readonly string _keyStoragePropertyName; + + /// The key property of the current storage model. + private readonly VectorStoreRecordKeyProperty _keyProperty; + + /// The property name to use as partition key. + private readonly string _partitionKeyPropertyName; + + /// The storage property name to use as partition key. + private readonly string _partitionKeyStoragePropertyName; + + /// The mapper to use when mapping between the consumer data model and the Azure CosmosDB NoSQL record. + private readonly IVectorStoreRecordMapper _mapper; + + /// + public string CollectionName { get; } + + /// + /// Initializes a new instance of the class. + /// + /// that can be used to manage the collections in Azure CosmosDB NoSQL. + /// The name of the collection that this will access. + /// Optional configuration options for this class. + public AzureCosmosDBNoSQLVectorStoreRecordCollection( + Database database, + string collectionName, + AzureCosmosDBNoSQLVectorStoreRecordCollectionOptions? options = default) + { + // Verify. + Verify.NotNull(database); + Verify.NotNullOrWhiteSpace(collectionName); + + // Assign. + this._database = database; + this.CollectionName = collectionName; + this._options = options ?? new(); + this._vectorStoreRecordDefinition = this._options.VectorStoreRecordDefinition ?? VectorStoreRecordPropertyReader.CreateVectorStoreRecordDefinitionFromType(typeof(TRecord), true); + var jsonSerializerOptions = this._options.JsonSerializerOptions ?? JsonSerializerOptions.Default; + + // Validate property types. + var properties = VectorStoreRecordPropertyReader.SplitDefinitionAndVerify(typeof(TRecord).Name, this._vectorStoreRecordDefinition, supportsMultipleVectors: true, requiresAtLeastOneVector: false); + VectorStoreRecordPropertyReader.VerifyPropertyTypes([properties.KeyProperty], s_supportedKeyTypes, "Key"); + VectorStoreRecordPropertyReader.VerifyPropertyTypes(properties.DataProperties, s_supportedDataTypes, "Data", supportEnumerable: true); + VectorStoreRecordPropertyReader.VerifyPropertyTypes(properties.VectorProperties, s_supportedVectorTypes, "Vector"); + + // Get storage names and store for later use. + this._keyProperty = properties.KeyProperty; + this._storagePropertyNames = VectorStoreRecordPropertyReader.BuildPropertyNameToJsonPropertyNameMap(properties, typeof(TRecord), jsonSerializerOptions); + + // Assign mapper. + this._mapper = this._options.JsonObjectCustomMapper ?? + new AzureCosmosDBNoSQLVectorStoreRecordMapper( + this._storagePropertyNames[this._keyProperty.DataModelPropertyName], + this._storagePropertyNames, + jsonSerializerOptions); + + // Use Azure CosmosDB NoSQL reserved key property name as storage key property name. + this._storagePropertyNames[this._keyProperty.DataModelPropertyName] = AzureCosmosDBNoSQLConstants.ReservedKeyPropertyName; + this._keyStoragePropertyName = AzureCosmosDBNoSQLConstants.ReservedKeyPropertyName; + + // If partition key is not provided, use key property as a partition key. + this._partitionKeyPropertyName = !string.IsNullOrWhiteSpace(this._options.PartitionKeyPropertyName) ? + this._options.PartitionKeyPropertyName! : + this._keyProperty.DataModelPropertyName; + + VerifyPartitionKeyProperty(this._partitionKeyPropertyName, this._vectorStoreRecordDefinition); + + this._partitionKeyStoragePropertyName = this._storagePropertyNames[this._partitionKeyPropertyName]; + + this._nonVectorStoragePropertyNames = properties.DataProperties + .Cast() + .Concat([properties.KeyProperty]) + .Select(x => this._storagePropertyNames[x.DataModelPropertyName]) + .ToList(); + } + + /// + public Task CollectionExistsAsync(CancellationToken cancellationToken = default) + { + return this.RunOperationAsync("GetContainerQueryIterator", async () => + { + const string Query = "SELECT VALUE(c.id) FROM c WHERE c.id = @collectionName"; + + var queryDefinition = new QueryDefinition(Query).WithParameter("@collectionName", this.CollectionName); + + using var feedIterator = this._database.GetContainerQueryIterator(queryDefinition); + + while (feedIterator.HasMoreResults) + { + var next = await feedIterator.ReadNextAsync(cancellationToken).ConfigureAwait(false); + + foreach (var containerName in next.Resource) + { + return true; + } + } + + return false; + }); + } + + /// + public Task CreateCollectionAsync(CancellationToken cancellationToken = default) + { + return this.RunOperationAsync("CreateContainer", () => + this._database.CreateContainerAsync(this.GetContainerProperties(), cancellationToken: cancellationToken)); + } + + /// + public async Task CreateCollectionIfNotExistsAsync(CancellationToken cancellationToken = default) + { + if (!await this.CollectionExistsAsync(cancellationToken).ConfigureAwait(false)) + { + await this.CreateCollectionAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + public Task DeleteCollectionAsync(CancellationToken cancellationToken = default) + { + return this.RunOperationAsync("DeleteContainer", () => + this._database + .GetContainer(this.CollectionName) + .DeleteContainerAsync(cancellationToken: cancellationToken)); + } + + #region Implementation of IVectorStoreRecordCollection + + /// + public Task DeleteAsync(string key, DeleteRecordOptions? options = null, CancellationToken cancellationToken = default) + { + // Use record key as partition key + var compositeKey = new AzureCosmosDBNoSQLCompositeKey(recordKey: key, partitionKey: key); + + return this.InternalDeleteAsync([compositeKey], cancellationToken); + } + + /// + public Task DeleteBatchAsync(IEnumerable keys, DeleteRecordOptions? options = null, CancellationToken cancellationToken = default) + { + // Use record keys as partition keys + var compositeKeys = keys.Select(key => new AzureCosmosDBNoSQLCompositeKey(recordKey: key, partitionKey: key)); + + return this.InternalDeleteAsync(compositeKeys, cancellationToken); + } + + /// + public async Task GetAsync(string key, GetRecordOptions? options = null, CancellationToken cancellationToken = default) + { + // Use record key as partition key + var compositeKey = new AzureCosmosDBNoSQLCompositeKey(recordKey: key, partitionKey: key); + + return await this.InternalGetAsync([compositeKey], options, cancellationToken) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + } + + /// + public async IAsyncEnumerable GetBatchAsync( + IEnumerable keys, + GetRecordOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Use record keys as partition keys + var compositeKeys = keys.Select(key => new AzureCosmosDBNoSQLCompositeKey(recordKey: key, partitionKey: key)); + + await foreach (var record in this.InternalGetAsync(compositeKeys, options, cancellationToken).ConfigureAwait(false)) + { + if (record is not null) + { + yield return record; + } + } + } + + /// + public async Task UpsertAsync(TRecord record, UpsertRecordOptions? options = null, CancellationToken cancellationToken = default) + { + var key = await this.InternalUpsertAsync(record, cancellationToken).ConfigureAwait(false); + + return key.RecordKey; + } + + /// + public async IAsyncEnumerable UpsertBatchAsync( + IEnumerable records, + UpsertRecordOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + Verify.NotNull(records); + + var tasks = records.Select(record => this.InternalUpsertAsync(record, cancellationToken)); + + var keys = await Task.WhenAll(tasks).ConfigureAwait(false); + + foreach (var key in keys) + { + if (key is not null) + { + yield return key.RecordKey; + } + } + } + + #endregion + + #region Implementation of IVectorStoreRecordCollection + + /// + public async Task GetAsync(AzureCosmosDBNoSQLCompositeKey key, GetRecordOptions? options = null, CancellationToken cancellationToken = default) + { + return await this.InternalGetAsync([key], options, cancellationToken) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + } + + /// + public async IAsyncEnumerable GetBatchAsync( + IEnumerable keys, + GetRecordOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var record in this.InternalGetAsync(keys, options, cancellationToken).ConfigureAwait(false)) + { + if (record is not null) + { + yield return record; + } + } + } + + /// + public Task DeleteAsync(AzureCosmosDBNoSQLCompositeKey key, DeleteRecordOptions? options = null, CancellationToken cancellationToken = default) + { + return this.InternalDeleteAsync([key], cancellationToken); + } + + /// + public Task DeleteBatchAsync(IEnumerable keys, DeleteRecordOptions? options = null, CancellationToken cancellationToken = default) + { + return this.InternalDeleteAsync(keys, cancellationToken); + } + + /// + Task IVectorStoreRecordCollection.UpsertAsync(TRecord record, UpsertRecordOptions? options, CancellationToken cancellationToken) + { + return this.InternalUpsertAsync(record, cancellationToken); + } + + /// + async IAsyncEnumerable IVectorStoreRecordCollection.UpsertBatchAsync( + IEnumerable records, + UpsertRecordOptions? options, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + Verify.NotNull(records); + + var tasks = records.Select(record => this.InternalUpsertAsync(record, cancellationToken)); + + var keys = await Task.WhenAll(tasks).ConfigureAwait(false); + + foreach (var key in keys) + { + if (key is not null) + { + yield return key; + } + } + } + + #endregion + + #region private + + private async Task RunOperationAsync(string operationName, Func> operation) + { + try + { + return await operation.Invoke().ConfigureAwait(false); + } + catch (Exception ex) + { + throw new VectorStoreOperationException("Call to vector store failed.", ex) + { + VectorStoreType = DatabaseName, + CollectionName = this.CollectionName, + OperationName = operationName + }; + } + } + + private static void VerifyPartitionKeyProperty(string partitionKeyPropertyName, VectorStoreRecordDefinition definition) + { + var partitionKeyProperty = definition.Properties + .FirstOrDefault(l => l.DataModelPropertyName.Equals(partitionKeyPropertyName, StringComparison.Ordinal)); + + if (partitionKeyProperty is null) + { + throw new ArgumentException("Partition key property must be part of record definition."); + } + + if (partitionKeyProperty.PropertyType != typeof(string)) + { + throw new ArgumentException("Partition key property must be string."); + } + } + + /// + /// Returns instance of with applied indexing policy. + /// More information here: . + /// + private ContainerProperties GetContainerProperties() + { + // Process Vector properties. + var embeddings = new Collection(); + var vectorIndexPaths = new Collection(); + + foreach (var property in this._vectorStoreRecordDefinition.Properties.OfType()) + { + var vectorPropertyName = this._storagePropertyNames[property.DataModelPropertyName]; + + if (property.Dimensions is not > 0) + { + throw new VectorStoreOperationException($"Property {nameof(property.Dimensions)} on {nameof(VectorStoreRecordVectorProperty)} '{property.DataModelPropertyName}' must be set to a positive integer to create a collection."); + } + + var path = $"/{vectorPropertyName}"; + + var embedding = new Embedding + { + DataType = GetDataType(property.PropertyType, vectorPropertyName), + Dimensions = (ulong)property.Dimensions, + DistanceFunction = GetDistanceFunction(property.DistanceFunction, vectorPropertyName), + Path = path + }; + + var vectorIndexPath = new VectorIndexPath + { + Type = GetIndexKind(property.IndexKind, vectorPropertyName), + Path = path + }; + + embeddings.Add(embedding); + vectorIndexPaths.Add(vectorIndexPath); + } + + var vectorEmbeddingPolicy = new VectorEmbeddingPolicy(embeddings); + var indexingPolicy = new IndexingPolicy + { + VectorIndexes = vectorIndexPaths, + IndexingMode = this._options.IndexingMode, + Automatic = this._options.Automatic + }; + + if (indexingPolicy.IndexingMode != IndexingMode.None) + { + // Process Data properties. + foreach (var property in this._vectorStoreRecordDefinition.Properties.OfType()) + { + if (property.IsFilterable || property.IsFullTextSearchable) + { + indexingPolicy.IncludedPaths.Add(new IncludedPath { Path = $"/{this._storagePropertyNames[property.DataModelPropertyName]}/?" }); + } + } + + // Adding special mandatory indexing path. + indexingPolicy.IncludedPaths.Add(new IncludedPath { Path = "/" }); + + // Exclude vector paths to ensure optimized performance. + foreach (var vectorIndexPath in vectorIndexPaths) + { + indexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = $"{vectorIndexPath.Path}/*" }); + } + } + + return new ContainerProperties(this.CollectionName, partitionKeyPath: $"/{this._partitionKeyStoragePropertyName}") + { + VectorEmbeddingPolicy = vectorEmbeddingPolicy, + IndexingPolicy = indexingPolicy + }; + } + + /// + /// More information about Azure CosmosDB NoSQL index kinds here: . + /// + private static VectorIndexType GetIndexKind(string? indexKind, string vectorPropertyName) + { + return indexKind switch + { + IndexKind.Flat => VectorIndexType.Flat, + IndexKind.QuantizedFlat => VectorIndexType.QuantizedFlat, + IndexKind.DiskAnn => VectorIndexType.DiskANN, + _ => throw new InvalidOperationException($"Index kind '{indexKind}' on {nameof(VectorStoreRecordVectorProperty)} '{vectorPropertyName}' is not supported by the Azure CosmosDB NoSQL VectorStore.") + }; + } + + /// + /// More information about Azure CosmosDB NoSQL distance functions here: . + /// + private static DistanceFunction GetDistanceFunction(string? distanceFunction, string vectorPropertyName) + { + return distanceFunction switch + { + SKDistanceFunction.CosineSimilarity => DistanceFunction.Cosine, + SKDistanceFunction.DotProductSimilarity => DistanceFunction.DotProduct, + SKDistanceFunction.EuclideanDistance => DistanceFunction.Euclidean, + _ => throw new InvalidOperationException($"Distance function '{distanceFunction}' for {nameof(VectorStoreRecordVectorProperty)} '{vectorPropertyName}' is not supported by the Azure CosmosDB NoSQL VectorStore.") + }; + } + + /// + /// Returns based on vector property type. + /// + private static VectorDataType GetDataType(Type vectorDataType, string vectorPropertyName) + { + return vectorDataType switch + { +#if NET5_0_OR_GREATER + Type type when type == typeof(ReadOnlyMemory) || type == typeof(ReadOnlyMemory?) => VectorDataType.Float16, +#endif + Type type when type == typeof(ReadOnlyMemory) || type == typeof(ReadOnlyMemory?) => VectorDataType.Float32, + Type type when type == typeof(ReadOnlyMemory) || type == typeof(ReadOnlyMemory?) => VectorDataType.Uint8, + Type type when type == typeof(ReadOnlyMemory) || type == typeof(ReadOnlyMemory?) => VectorDataType.Int8, + _ => throw new InvalidOperationException($"Data type '{vectorDataType}' for {nameof(VectorStoreRecordVectorProperty)} '{vectorPropertyName}' is not supported by the Azure CosmosDB NoSQL VectorStore.") + }; + } + + private async IAsyncEnumerable InternalGetAsync( + IEnumerable keys, + GetRecordOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + Verify.NotNull(keys); + + const string OperationName = "GetItemQueryIterator"; + + var includeVectors = options?.IncludeVectors ?? false; + var fields = new List(includeVectors ? this._storagePropertyNames.Values : this._nonVectorStoragePropertyNames); + var queryDefinition = this.GetSelectQuery(keys.ToList(), fields); + + await foreach (var jsonObject in this.GetItemsAsync(queryDefinition, cancellationToken).ConfigureAwait(false)) + { + yield return VectorStoreErrorHandler.RunModelConversion( + DatabaseName, + this.CollectionName, + OperationName, + () => this._mapper.MapFromStorageToDataModel(jsonObject, new() { IncludeVectors = includeVectors })); + } + } + + private async Task InternalUpsertAsync( + TRecord record, + CancellationToken cancellationToken) + { + Verify.NotNull(record); + + const string OperationName = "UpsertItem"; + + var jsonObject = VectorStoreErrorHandler.RunModelConversion( + DatabaseName, + this.CollectionName, + OperationName, + () => this._mapper.MapFromDataToStorageModel(record)); + + var keyValue = jsonObject.TryGetPropertyValue(this._keyStoragePropertyName, out var jsonKey) ? jsonKey?.ToString() : null; + var partitionKeyValue = jsonObject.TryGetPropertyValue(this._partitionKeyStoragePropertyName, out var jsonPartitionKey) ? jsonPartitionKey?.ToString() : null; + + if (string.IsNullOrWhiteSpace(keyValue)) + { + throw new VectorStoreOperationException($"Key property {this._keyProperty.DataModelPropertyName} is not initialized."); + } + + if (string.IsNullOrWhiteSpace(partitionKeyValue)) + { + throw new VectorStoreOperationException($"Partition key property {this._partitionKeyPropertyName} is not initialized."); + } + + await this.RunOperationAsync(OperationName, () => + this._database + .GetContainer(this.CollectionName) + .UpsertItemAsync(jsonObject, new PartitionKey(partitionKeyValue), cancellationToken: cancellationToken)) + .ConfigureAwait(false); + + return new AzureCosmosDBNoSQLCompositeKey(keyValue!, partitionKeyValue!); + } + + private async Task InternalDeleteAsync(IEnumerable keys, CancellationToken cancellationToken) + { + Verify.NotNull(keys); + + var tasks = keys.Select(key => + { + Verify.NotNullOrWhiteSpace(key.RecordKey); + Verify.NotNullOrWhiteSpace(key.PartitionKey); + + return this.RunOperationAsync("DeleteItem", () => + this._database + .GetContainer(this.CollectionName) + .DeleteItemAsync(key.RecordKey, new PartitionKey(key.PartitionKey), cancellationToken: cancellationToken)); + }); + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + private QueryDefinition GetSelectQuery(List keys, List fields) + { + Verify.True(keys.Count > 0, "At least one key should be provided.", nameof(keys)); + + const string WhereClauseDelimiter = " OR "; + const string SelectClauseDelimiter = ","; + + const string RecordKeyVariableName = "rk"; + const string PartitionKeyVariableName = "pk"; + + const string TableVariableName = "x"; + + var selectClauseArguments = string.Join(SelectClauseDelimiter, + fields.Select(field => $"{TableVariableName}.{field}")); + + var whereClauseArguments = string.Join(WhereClauseDelimiter, + keys.Select((key, index) => + $"({TableVariableName}.{this._keyStoragePropertyName} = @{RecordKeyVariableName}{index} AND " + + $"{TableVariableName}.{this._partitionKeyStoragePropertyName} = @{PartitionKeyVariableName}{index})")); + + var query = $"SELECT {selectClauseArguments} FROM {TableVariableName} WHERE {whereClauseArguments}"; + + var queryDefinition = new QueryDefinition(query); + + for (var i = 0; i < keys.Count; i++) + { + var recordKey = keys[i].RecordKey; + var partitionKey = keys[i].PartitionKey; + + Verify.NotNullOrWhiteSpace(recordKey); + Verify.NotNullOrWhiteSpace(partitionKey); + + queryDefinition.WithParameter($"@{RecordKeyVariableName}{i}", recordKey); + queryDefinition.WithParameter($"@{PartitionKeyVariableName}{i}", partitionKey); + } + + return queryDefinition; + } + + private async IAsyncEnumerable GetItemsAsync(QueryDefinition queryDefinition, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var iterator = this._database + .GetContainer(this.CollectionName) + .GetItemQueryIterator(queryDefinition); + + while (iterator.HasMoreResults) + { + var response = await iterator.ReadNextAsync(cancellationToken).ConfigureAwait(false); + + foreach (var record in response.Resource) + { + if (record is not null) + { + yield return record; + } + } + } + } + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollectionOptions.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollectionOptions.cs new file mode 100644 index 000000000000..a42d821a7d30 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollectionOptions.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Azure.Cosmos; +using Microsoft.SemanticKernel.Data; + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; + +/// +/// Options when creating a . +/// +public sealed class AzureCosmosDBNoSQLVectorStoreRecordCollectionOptions where TRecord : class +{ + /// + /// Gets or sets an optional custom mapper to use when converting between the data model and the Azure CosmosDB NoSQL record. + /// + /// + /// If not set, the default mapper that is provided by the Azure CosmosDB NoSQL client SDK will be used. + /// + public IVectorStoreRecordMapper? JsonObjectCustomMapper { get; init; } = null; + + /// + /// Gets or sets an optional record definition that defines the schema of the record type. + /// + /// + /// If not provided, the schema will be inferred from the record model class using reflection. + /// In this case, the record model properties must be annotated with the appropriate attributes to indicate their usage. + /// See , and . + /// + public VectorStoreRecordDefinition? VectorStoreRecordDefinition { get; init; } = null; + + /// + /// Gets or sets the JSON serializer options to use when converting between the data model and the Azure CosmosDB NoSQL record. + /// + public JsonSerializerOptions? JsonSerializerOptions { get; init; } = null; + + /// + /// The property name to use as partition key. + /// + public string? PartitionKeyPropertyName { get; init; } = null; + + /// + /// Specifies the indexing mode in the Azure Cosmos DB service. + /// More information here: . + /// + /// + /// Default is . + /// + public IndexingMode IndexingMode { get; init; } = IndexingMode.Consistent; + + /// + /// Gets or sets a value that indicates whether automatic indexing is enabled for a collection in the Azure Cosmos DB service. + /// + /// + /// Default is . + /// + public bool Automatic { get; init; } = true; +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordMapper.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordMapper.cs new file mode 100644 index 000000000000..ced34a5b2693 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordMapper.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.SemanticKernel.Data; + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; + +/// +/// Class for mapping between a json node stored in Azure CosmosDB NoSQL and the consumer data model. +/// +/// The consumer data model to map to or from. +internal sealed class AzureCosmosDBNoSQLVectorStoreRecordMapper : IVectorStoreRecordMapper + where TRecord : class +{ + /// The JSON serializer options to use when converting between the data model and the Azure CosmosDB NoSQL record. + private readonly JsonSerializerOptions _jsonSerializerOptions; + + /// The storage property name of the key field of consumer data model. + private readonly string _keyStoragePropertyName; + + /// A dictionary that maps from a property name to the storage name that should be used when serializing it to json for data and vector properties. + private readonly Dictionary _storagePropertyNames = []; + + public AzureCosmosDBNoSQLVectorStoreRecordMapper( + string keyStoragePropertyName, + Dictionary storagePropertyNames, + JsonSerializerOptions jsonSerializerOptions) + { + Verify.NotNull(jsonSerializerOptions); + + this._keyStoragePropertyName = keyStoragePropertyName; + this._storagePropertyNames = storagePropertyNames; + this._jsonSerializerOptions = jsonSerializerOptions; + } + + public JsonObject MapFromDataToStorageModel(TRecord dataModel) + { + var jsonObject = JsonSerializer.SerializeToNode(dataModel, this._jsonSerializerOptions)!.AsObject(); + + // Key property in Azure CosmosDB NoSQL has a reserved name. + RenameJsonProperty(jsonObject, this._keyStoragePropertyName, AzureCosmosDBNoSQLConstants.ReservedKeyPropertyName); + + return jsonObject; + } + + public TRecord MapFromStorageToDataModel(JsonObject storageModel, StorageToDataModelMapperOptions options) + { + // Rename key property for valid deserialization. + RenameJsonProperty(storageModel, AzureCosmosDBNoSQLConstants.ReservedKeyPropertyName, this._keyStoragePropertyName); + + return storageModel.Deserialize(this._jsonSerializerOptions)!; + } + + #region private + + private static void RenameJsonProperty(JsonObject jsonObject, string oldKey, string newKey) + { + if (jsonObject is not null && jsonObject.ContainsKey(oldKey)) + { + JsonNode? value = jsonObject[oldKey]; + + jsonObject.Remove(oldKey); + + jsonObject[newKey] = value; + } + } + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/Connectors.Memory.AzureCosmosDBNoSQL.csproj b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/Connectors.Memory.AzureCosmosDBNoSQL.csproj index 0ffb5b602e05..32fd24223c81 100644 --- a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/Connectors.Memory.AzureCosmosDBNoSQL.csproj +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/Connectors.Memory.AzureCosmosDBNoSQL.csproj @@ -27,4 +27,8 @@ + + + + diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/CosmosSystemTextJSonSerializer.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/CosmosSystemTextJSonSerializer.cs index 0737ce09c120..4fed0eab601d 100644 --- a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/CosmosSystemTextJSonSerializer.cs +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/CosmosSystemTextJSonSerializer.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Taken from https://github.com/Azure/azure-cosmos-dotnet-v3/pull/4332 +// TODO: Remove when https://github.com/Azure/azure-cosmos-dotnet-v3/pull/4589 will be released. using System; using System.Diagnostics.CodeAnalysis; diff --git a/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/IAzureCosmosDBNoSQLVectorStoreRecordCollectionFactory.cs b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/IAzureCosmosDBNoSQLVectorStoreRecordCollectionFactory.cs new file mode 100644 index 000000000000..26e33af811dc --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Memory.AzureCosmosDBNoSQL/IAzureCosmosDBNoSQLVectorStoreRecordCollectionFactory.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Azure.Cosmos; +using Microsoft.SemanticKernel.Data; + +namespace Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; + +/// +/// Interface for constructing Azure CosmosDB NoSQL instances when using to retrieve these. +/// +public interface IAzureCosmosDBNoSQLVectorStoreRecordCollectionFactory +{ + /// + /// Constructs a new instance of the . + /// + /// The data type of the record key. + /// The data model to use for adding, updating and retrieving data from storage. + /// that can be used to manage the collections in Azure CosmosDB NoSQL. + /// The name of the collection to connect to. + /// An optional record definition that defines the schema of the record type. If not present, attributes on will be used. + /// The new instance of . + IVectorStoreRecordCollection CreateVectorStoreRecordCollection( + Database database, + string name, + VectorStoreRecordDefinition? vectorStoreRecordDefinition) + where TKey : notnull + where TRecord : class; +} diff --git a/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeVectorStoreOptions.cs b/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeVectorStoreOptions.cs index 7a6fc9767f62..eb8caaa5a17d 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeVectorStoreOptions.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeVectorStoreOptions.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel.Connectors.Pinecone; public sealed class PineconeVectorStoreOptions { /// - /// An optional factory to use for constructing instances, if custom options are required. + /// An optional factory to use for constructing instances, if a custom record collection is required. /// public IPineconeVectorStoreRecordCollectionFactory? VectorStoreCollectionFactory { get; init; } } diff --git a/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStore.cs b/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStore.cs index ef9c9f1593f0..967060e56c33 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStore.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStore.cs @@ -65,9 +65,9 @@ public IVectorStoreRecordCollection GetCollection( return this._options.VectorStoreCollectionFactory.CreateVectorStoreRecordCollection(this._qdrantClient.QdrantClient, name, vectorStoreRecordDefinition); } - var directlyCreatedStore = new QdrantVectorStoreRecordCollection(this._qdrantClient, name, new QdrantVectorStoreRecordCollectionOptions() { VectorStoreRecordDefinition = vectorStoreRecordDefinition }); - var castCreatedStore = directlyCreatedStore as IVectorStoreRecordCollection; - return castCreatedStore!; + var recordCollection = new QdrantVectorStoreRecordCollection(this._qdrantClient, name, new QdrantVectorStoreRecordCollectionOptions() { VectorStoreRecordDefinition = vectorStoreRecordDefinition }); + var castRecordCollection = recordCollection as IVectorStoreRecordCollection; + return castRecordCollection!; } /// diff --git a/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStoreOptions.cs b/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStoreOptions.cs index c3ead1bdee2d..27790c731aed 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStoreOptions.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Qdrant/QdrantVectorStoreOptions.cs @@ -14,7 +14,7 @@ public sealed class QdrantVectorStoreOptions public bool HasNamedVectors { get; set; } = false; /// - /// An optional factory to use for constructing instances, if custom options are required. + /// An optional factory to use for constructing instances, if a custom record collection is required. /// public IQdrantVectorStoreRecordCollectionFactory? VectorStoreCollectionFactory { get; init; } } diff --git a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisHashSetVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisHashSetVectorStoreRecordCollection.cs index e68edb98870e..a2608a091edc 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisHashSetVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisHashSetVectorStoreRecordCollection.cs @@ -156,7 +156,7 @@ public async Task CollectionExistsAsync(CancellationToken cancellationToke public Task CreateCollectionAsync(CancellationToken cancellationToken = default) { // Map the record definition to a schema. - var schema = RedisVectorStoreCollectionCreateMapping.MapToSchema(this._vectorStoreRecordDefinition.Properties, this._storagePropertyNames); + var schema = RedisVectorStoreCollectionCreateMapping.MapToSchema(this._vectorStoreRecordDefinition.Properties, this._storagePropertyNames, useDollarPrefix: false); // Create the index creation params. // Add the collection name and colon as the index prefix, which means that any record where the key is prefixed with this text will be indexed by this index diff --git a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisJsonVectorStoreRecordCollection.cs b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisJsonVectorStoreRecordCollection.cs index 44a6bc41d195..acd2b540f913 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisJsonVectorStoreRecordCollection.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisJsonVectorStoreRecordCollection.cs @@ -144,7 +144,7 @@ public async Task CollectionExistsAsync(CancellationToken cancellationToke public Task CreateCollectionAsync(CancellationToken cancellationToken = default) { // Map the record definition to a schema. - var schema = RedisVectorStoreCollectionCreateMapping.MapToSchema(this._vectorStoreRecordDefinition.Properties, this._storagePropertyNames); + var schema = RedisVectorStoreCollectionCreateMapping.MapToSchema(this._vectorStoreRecordDefinition.Properties, this._storagePropertyNames, useDollarPrefix: true); // Create the index creation params. // Add the collection name and colon as the index prefix, which means that any record where the key is prefixed with this text will be indexed by this index diff --git a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStore.cs b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStore.cs index 51a933d36062..2f41fea7b160 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStore.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStore.cs @@ -57,13 +57,13 @@ public IVectorStoreRecordCollection GetCollection( if (this._options.StorageType == RedisStorageType.HashSet) { - var directlyCreatedStore = new RedisHashSetVectorStoreRecordCollection(this._database, name, new RedisHashSetVectorStoreRecordCollectionOptions() { VectorStoreRecordDefinition = vectorStoreRecordDefinition }) as IVectorStoreRecordCollection; - return directlyCreatedStore!; + var recordCollection = new RedisHashSetVectorStoreRecordCollection(this._database, name, new RedisHashSetVectorStoreRecordCollectionOptions() { VectorStoreRecordDefinition = vectorStoreRecordDefinition }) as IVectorStoreRecordCollection; + return recordCollection!; } else { - var directlyCreatedStore = new RedisJsonVectorStoreRecordCollection(this._database, name, new RedisJsonVectorStoreRecordCollectionOptions() { VectorStoreRecordDefinition = vectorStoreRecordDefinition }) as IVectorStoreRecordCollection; - return directlyCreatedStore!; + var recordCollection = new RedisJsonVectorStoreRecordCollection(this._database, name, new RedisJsonVectorStoreRecordCollectionOptions() { VectorStoreRecordDefinition = vectorStoreRecordDefinition }) as IVectorStoreRecordCollection; + return recordCollection!; } } diff --git a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStoreCollectionCreateMapping.cs b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStoreCollectionCreateMapping.cs index 2bdb6a67b5ef..cd8f3f516be6 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStoreCollectionCreateMapping.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStoreCollectionCreateMapping.cs @@ -48,11 +48,13 @@ internal static class RedisVectorStoreCollectionCreateMapping /// /// The property definitions to map from. /// A dictionary that maps from a property name to the storage name that should be used when serializing it to json for data and vector properties. + /// A value indicating whether to include $. prefix for field names as required in JSON mode. /// The mapped Redis . /// Thrown if there are missing required or unsupported configuration options set. - public static Schema MapToSchema(IEnumerable properties, Dictionary storagePropertyNames) + public static Schema MapToSchema(IEnumerable properties, Dictionary storagePropertyNames, bool useDollarPrefix) { var schema = new Schema(); + var fieldNamePrefix = useDollarPrefix ? "$." : string.Empty; // Loop through all properties and create the index fields. foreach (var property in properties) @@ -79,7 +81,7 @@ public static Schema MapToSchema(IEnumerable properti { if (dataProperty.PropertyType == typeof(string) || (typeof(IEnumerable).IsAssignableFrom(dataProperty.PropertyType) && GetEnumerableType(dataProperty.PropertyType) == typeof(string))) { - schema.AddTextField(new FieldName($"$.{storageName}", storageName)); + schema.AddTextField(new FieldName($"{fieldNamePrefix}{storageName}", storageName)); } else { @@ -92,15 +94,15 @@ public static Schema MapToSchema(IEnumerable properti { if (dataProperty.PropertyType == typeof(string)) { - schema.AddTagField(new FieldName($"$.{storageName}", storageName)); + schema.AddTagField(new FieldName($"{fieldNamePrefix}{storageName}", storageName)); } else if (typeof(IEnumerable).IsAssignableFrom(dataProperty.PropertyType) && GetEnumerableType(dataProperty.PropertyType) == typeof(string)) { - schema.AddTagField(new FieldName($"$.{storageName}.*", storageName)); + schema.AddTagField(new FieldName($"{fieldNamePrefix}{storageName}.*", storageName)); } else if (RedisVectorStoreCollectionCreateMapping.s_supportedFilterableNumericDataTypes.Contains(dataProperty.PropertyType)) { - schema.AddNumericField(new FieldName($"$.{storageName}", storageName)); + schema.AddNumericField(new FieldName($"{fieldNamePrefix}{storageName}", storageName)); } else { @@ -123,7 +125,7 @@ public static Schema MapToSchema(IEnumerable properti var indexKind = GetSDKIndexKind(vectorProperty); var distanceAlgorithm = GetSDKDistanceAlgorithm(vectorProperty); var dimensions = vectorProperty.Dimensions.Value.ToString(CultureInfo.InvariantCulture); - schema.AddVectorField(new FieldName($"$.{storageName}", storageName), indexKind, new Dictionary() + schema.AddVectorField(new FieldName($"{fieldNamePrefix}{storageName}", storageName), indexKind, new Dictionary() { ["TYPE"] = "FLOAT32", ["DIM"] = dimensions, diff --git a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStoreOptions.cs b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStoreOptions.cs index 0434b3c633ec..63eeda5a5e3e 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStoreOptions.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Redis/RedisVectorStoreOptions.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel.Connectors.Redis; public sealed class RedisVectorStoreOptions { /// - /// An optional factory to use for constructing instances, if custom options are required. + /// An optional factory to use for constructing instances, if a custom record collection is required. /// public IRedisVectorStoreRecordCollectionFactory? VectorStoreCollectionFactory { get; init; } diff --git a/dotnet/src/Connectors/Connectors.Memory.Sqlite/Database.cs b/dotnet/src/Connectors/Connectors.Memory.Sqlite/Database.cs index 581a21afc52a..cdd1171ec219 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Sqlite/Database.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Sqlite/Database.cs @@ -146,6 +146,41 @@ public async IAsyncEnumerable ReadAllAsync(SqliteConnection conn, return null; } + public async IAsyncEnumerable ReadBatchAsync(SqliteConnection conn, + string collectionName, + string[] keys, + bool withEmbeddings = false, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + using SqliteCommand cmd = conn.CreateCommand(); + var keyParameters = keys.Select((key, index) => $"@key{index}"); + var parameters = string.Join(", ", keyParameters); + + var selectFieldQuery = withEmbeddings ? "*" : "key, metadata, timestamp"; +#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities + cmd.CommandText = $@" + SELECT {selectFieldQuery} FROM {TableName} + WHERE collection=@collection + AND key IN ({parameters})"; +#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities + + cmd.Parameters.Add(new SqliteParameter("@collection", collectionName)); + for (int i = 0; i < keys.Length; i++) + { + cmd.Parameters.Add(new SqliteParameter($"@key{i}", keys[i])); + } + + using var dataReader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + while (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + string key = dataReader.GetString("key"); + string metadata = dataReader.GetString("metadata"); + string embedding = withEmbeddings ? dataReader.GetString("embedding") : string.Empty; + string timestamp = dataReader.GetString("timestamp"); + yield return new DatabaseEntry() { Key = key, MetadataString = metadata, EmbeddingString = embedding, Timestamp = timestamp }; + } + } + public Task DeleteCollectionAsync(SqliteConnection conn, string collectionName, CancellationToken cancellationToken = default) { using SqliteCommand cmd = conn.CreateCommand(); diff --git a/dotnet/src/Connectors/Connectors.Memory.Sqlite/SqliteMemoryStore.cs b/dotnet/src/Connectors/Connectors.Memory.Sqlite/SqliteMemoryStore.cs index 232b0e97b9dc..59c8591c0bf6 100644 --- a/dotnet/src/Connectors/Connectors.Memory.Sqlite/SqliteMemoryStore.cs +++ b/dotnet/src/Connectors/Connectors.Memory.Sqlite/SqliteMemoryStore.cs @@ -87,21 +87,9 @@ public async IAsyncEnumerable UpsertBatchAsync(string collectionName, IE } /// - public async IAsyncEnumerable GetBatchAsync(string collectionName, IEnumerable keys, bool withEmbeddings = false, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + public IAsyncEnumerable GetBatchAsync(string collectionName, IEnumerable keys, bool withEmbeddings = false, CancellationToken cancellationToken = default) { - foreach (var key in keys) - { - var result = await this.InternalGetAsync(this._dbConnection, collectionName, key, withEmbeddings, cancellationToken).ConfigureAwait(false); - if (result is not null) - { - yield return result; - } - else - { - yield break; - } - } + return this.InternalGetBatchAsync(this._dbConnection, collectionName, keys.ToArray(), withEmbeddings, cancellationToken); } /// @@ -283,5 +271,18 @@ await this._dbConnector.UpsertAsync( ParseTimestamp(entry.Value.Timestamp)); } + private async IAsyncEnumerable InternalGetBatchAsync( + SqliteConnection connection, + string collectionName, + string[] keys, bool withEmbedding, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (DatabaseEntry dbEntry in this._dbConnector.ReadBatchAsync(connection, collectionName, keys, withEmbedding, cancellationToken).ConfigureAwait(false)) + { + ReadOnlyMemory vector = withEmbedding ? JsonSerializer.Deserialize>(dbEntry.EmbeddingString, JsonOptionsCache.Default) : ReadOnlyMemory.Empty; + yield return MemoryRecord.FromJsonMetadata(dbEntry.MetadataString, vector, dbEntry.Key, ParseTimestamp(dbEntry.Timestamp)); ; + } + } + #endregion } diff --git a/dotnet/src/Connectors/Connectors.Oobabooga/README.md b/dotnet/src/Connectors/Connectors.Oobabooga/README.md deleted file mode 100644 index b1c45cd528cd..000000000000 --- a/dotnet/src/Connectors/Connectors.Oobabooga/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Semantic Kernel Oobabooga AI Connector - -This connector have moved, please go [here](https://github.com/MyIntelligenceAgency/semantic-fleet) for more information. diff --git a/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisHashSetVectorStoreRecordCollectionTests.cs b/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisHashSetVectorStoreRecordCollectionTests.cs index a95179e86346..2f3676059a1c 100644 --- a/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisHashSetVectorStoreRecordCollectionTests.cs +++ b/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisHashSetVectorStoreRecordCollectionTests.cs @@ -82,15 +82,15 @@ public async Task CanCreateCollectionAsync() 1, "testcollection:", "SCHEMA", - "$.OriginalNameData", + "OriginalNameData", "AS", "OriginalNameData", "TAG", - "$.data_storage_name", + "data_storage_name", "AS", "data_storage_name", "TAG", - "$.vector_storage_name", + "vector_storage_name", "AS", "vector_storage_name", "VECTOR", diff --git a/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisVectorStoreCollectionCreateMappingTests.cs b/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisVectorStoreCollectionCreateMappingTests.cs index c5bb3b12b2c5..b7c537103858 100644 --- a/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisVectorStoreCollectionCreateMappingTests.cs +++ b/dotnet/src/Connectors/Connectors.Redis.UnitTests/RedisVectorStoreCollectionCreateMappingTests.cs @@ -14,8 +14,10 @@ namespace Microsoft.SemanticKernel.Connectors.Redis.UnitTests; /// public class RedisVectorStoreCollectionCreateMappingTests { - [Fact] - public void MapToSchemaCreatesSchema() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MapToSchemaCreatesSchema(bool useDollarPrefix) { // Arrange. var properties = new VectorStoreRecordProperty[] @@ -50,7 +52,7 @@ public void MapToSchemaCreatesSchema() }; // Act. - var schema = RedisVectorStoreCollectionCreateMapping.MapToSchema(properties, storagePropertyNames); + var schema = RedisVectorStoreCollectionCreateMapping.MapToSchema(properties, storagePropertyNames, useDollarPrefix); // Assert. Assert.NotNull(schema); @@ -65,16 +67,32 @@ public void MapToSchemaCreatesSchema() Assert.IsType(schema.Fields[6]); Assert.IsType(schema.Fields[7]); - VerifyFieldName(schema.Fields[0].FieldName, new List { "$.FilterableString", "AS", "FilterableString" }); - VerifyFieldName(schema.Fields[1].FieldName, new List { "$.FullTextSearchableString", "AS", "FullTextSearchableString" }); - VerifyFieldName(schema.Fields[2].FieldName, new List { "$.FilterableStringEnumerable.*", "AS", "FilterableStringEnumerable" }); - VerifyFieldName(schema.Fields[3].FieldName, new List { "$.FullTextSearchableStringEnumerable", "AS", "FullTextSearchableStringEnumerable" }); + if (useDollarPrefix) + { + VerifyFieldName(schema.Fields[0].FieldName, new List { "$.FilterableString", "AS", "FilterableString" }); + VerifyFieldName(schema.Fields[1].FieldName, new List { "$.FullTextSearchableString", "AS", "FullTextSearchableString" }); + VerifyFieldName(schema.Fields[2].FieldName, new List { "$.FilterableStringEnumerable.*", "AS", "FilterableStringEnumerable" }); + VerifyFieldName(schema.Fields[3].FieldName, new List { "$.FullTextSearchableStringEnumerable", "AS", "FullTextSearchableStringEnumerable" }); + + VerifyFieldName(schema.Fields[4].FieldName, new List { "$.FilterableInt", "AS", "FilterableInt" }); + VerifyFieldName(schema.Fields[5].FieldName, new List { "$.FilterableNullableInt", "AS", "FilterableNullableInt" }); + + VerifyFieldName(schema.Fields[6].FieldName, new List { "$.VectorDefaultIndexingOptions", "AS", "VectorDefaultIndexingOptions" }); + VerifyFieldName(schema.Fields[7].FieldName, new List { "$.vector_specific_indexing_options", "AS", "vector_specific_indexing_options" }); + } + else + { + VerifyFieldName(schema.Fields[0].FieldName, new List { "FilterableString", "AS", "FilterableString" }); + VerifyFieldName(schema.Fields[1].FieldName, new List { "FullTextSearchableString", "AS", "FullTextSearchableString" }); + VerifyFieldName(schema.Fields[2].FieldName, new List { "FilterableStringEnumerable.*", "AS", "FilterableStringEnumerable" }); + VerifyFieldName(schema.Fields[3].FieldName, new List { "FullTextSearchableStringEnumerable", "AS", "FullTextSearchableStringEnumerable" }); - VerifyFieldName(schema.Fields[4].FieldName, new List { "$.FilterableInt", "AS", "FilterableInt" }); - VerifyFieldName(schema.Fields[5].FieldName, new List { "$.FilterableNullableInt", "AS", "FilterableNullableInt" }); + VerifyFieldName(schema.Fields[4].FieldName, new List { "FilterableInt", "AS", "FilterableInt" }); + VerifyFieldName(schema.Fields[5].FieldName, new List { "FilterableNullableInt", "AS", "FilterableNullableInt" }); - VerifyFieldName(schema.Fields[6].FieldName, new List { "$.VectorDefaultIndexingOptions", "AS", "VectorDefaultIndexingOptions" }); - VerifyFieldName(schema.Fields[7].FieldName, new List { "$.vector_specific_indexing_options", "AS", "vector_specific_indexing_options" }); + VerifyFieldName(schema.Fields[6].FieldName, new List { "VectorDefaultIndexingOptions", "AS", "VectorDefaultIndexingOptions" }); + VerifyFieldName(schema.Fields[7].FieldName, new List { "vector_specific_indexing_options", "AS", "vector_specific_indexing_options" }); + } Assert.Equal("10", ((VectorField)schema.Fields[6]).Attributes!["DIM"]); Assert.Equal("FLOAT32", ((VectorField)schema.Fields[6]).Attributes!["TYPE"]); @@ -95,7 +113,7 @@ public void MapToSchemaThrowsOnInvalidVectorDimensions(int? dimensions) var storagePropertyNames = new Dictionary() { { "VectorProperty", "VectorProperty" } }; // Act and assert. - Assert.Throws(() => RedisVectorStoreCollectionCreateMapping.MapToSchema(properties, storagePropertyNames)); + Assert.Throws(() => RedisVectorStoreCollectionCreateMapping.MapToSchema(properties, storagePropertyNames, true)); } [Fact] diff --git a/dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/ApiManifestKernelExtensions.cs b/dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/ApiManifestKernelExtensions.cs index 1009968fa0e7..7716eb7cac93 100644 --- a/dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/ApiManifestKernelExtensions.cs +++ b/dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/ApiManifestKernelExtensions.cs @@ -128,8 +128,6 @@ public static async Task CreatePluginFromApiManifestAsync( var predicate = OpenApiFilterService.CreatePredicate(null, null, requestUrls, openApiDocument); var filteredOpenApiDocument = OpenApiFilterService.CreateFilteredDocument(openApiDocument, predicate); - var serverUrl = filteredOpenApiDocument.Servers.FirstOrDefault()?.Url; - var openApiFunctionExecutionParameters = pluginParameters?.FunctionExecutionParameters?.ContainsKey(apiName) == true ? pluginParameters.FunctionExecutionParameters[apiName] : null; @@ -145,17 +143,18 @@ public static async Task CreatePluginFromApiManifestAsync( openApiFunctionExecutionParameters?.EnableDynamicPayload ?? true, openApiFunctionExecutionParameters?.EnablePayloadNamespacing ?? false); - if (serverUrl is not null) + var server = filteredOpenApiDocument.Servers.FirstOrDefault(); + if (server?.Url is not null) { foreach (var path in filteredOpenApiDocument.Paths) { - var operations = OpenApiDocumentParser.CreateRestApiOperations(serverUrl, path.Key, path.Value, null, logger); + var operations = OpenApiDocumentParser.CreateRestApiOperations(server, path.Key, path.Value, null, logger); foreach (RestApiOperation operation in operations) { try { logger.LogTrace("Registering Rest function {0}.{1}", pluginName, operation.Id); - functions.Add(OpenApiKernelPluginFactory.CreateRestApiFunction(pluginName, runner, operation, openApiFunctionExecutionParameters, new Uri(serverUrl), loggerFactory)); + functions.Add(OpenApiKernelPluginFactory.CreateRestApiFunction(pluginName, runner, operation, openApiFunctionExecutionParameters, new Uri(server.Url), loggerFactory)); } catch (Exception ex) when (!ex.IsCriticalException()) { diff --git a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs index af65b1c59825..77298a9c86af 100644 --- a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs +++ b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs @@ -50,9 +50,9 @@ public sealed class RestApiOperation public HttpMethod Method { get; } /// - /// The server URL. + /// The server. /// - public Uri? ServerUrl { get; } + public RestApiOperationServer Server { get; } /// /// The operation parameters. @@ -78,7 +78,7 @@ public sealed class RestApiOperation /// Creates an instance of a class. /// /// The operation identifier. - /// The server URL. + /// The server. /// The operation path. /// The operation method. /// The operation description. @@ -87,7 +87,7 @@ public sealed class RestApiOperation /// The operation responses. public RestApiOperation( string id, - Uri? serverUrl, + RestApiOperationServer server, string path, HttpMethod method, string description, @@ -96,7 +96,7 @@ public RestApiOperation( IDictionary? responses = null) { this.Id = id; - this.ServerUrl = serverUrl; + this.Server = server; this.Path = path; this.Method = method; this.Description = description; @@ -114,7 +114,7 @@ public RestApiOperation( /// The operation Url. public Uri BuildOperationUrl(IDictionary arguments, Uri? serverUrlOverride = null, Uri? apiHostUrl = null) { - var serverUrl = this.GetServerUrl(serverUrlOverride, apiHostUrl); + var serverUrl = this.GetServerUrl(serverUrlOverride, apiHostUrl, arguments); var path = this.BuildPath(this.Path, arguments); @@ -250,8 +250,9 @@ private string BuildPath(string pathTemplate, IDictionary argum /// /// Override for REST API operation server url. /// The URL of REST API host. + /// The operation arguments. /// The operation server url. - private Uri GetServerUrl(Uri? serverUrlOverride, Uri? apiHostUrl) + private Uri GetServerUrl(Uri? serverUrlOverride, Uri? apiHostUrl, IDictionary arguments) { string serverUrlString; @@ -259,10 +260,30 @@ private Uri GetServerUrl(Uri? serverUrlOverride, Uri? apiHostUrl) { serverUrlString = serverUrlOverride.AbsoluteUri; } + else if (this.Server.Url is not null) + { + serverUrlString = this.Server.Url; + foreach (var variable in this.Server.Variables) + { + arguments.TryGetValue(variable.Key, out object? value); + string? strValue = value as string; + if (strValue is not null && variable.Value.IsValid(strValue)) + { + serverUrlString = serverUrlString.Replace($"{{{variable.Key}}}", strValue); + } + else if (variable.Value.Default is not null) + { + serverUrlString = serverUrlString.Replace($"{{{variable.Key}}}", variable.Value.Default); + } + else + { + throw new KernelException($"No value provided for the '{variable.Key}' server variable of the operation - '{this.Id}'."); + } + } + } else { serverUrlString = - this.ServerUrl?.AbsoluteUri ?? apiHostUrl?.AbsoluteUri ?? throw new InvalidOperationException($"Server url is not defined for operation {this.Id}"); } diff --git a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationServer.cs b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationServer.cs new file mode 100644 index 000000000000..0936bdcdfb45 --- /dev/null +++ b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationServer.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel.Plugins.OpenApi; + +/// +/// REST API Operation Server. +/// +public sealed class RestApiOperationServer +{ + /// + /// A URL to the target host. This URL supports Server Variables and MAY be relative, + /// to indicate that the host location is relative to the location where the OpenAPI document is being served. + /// Variable substitutions will be made when a variable is named in {brackets}. + /// +#pragma warning disable CA1056 // URI-like properties should not be strings + public string? Url { get; } +#pragma warning restore CA1056 // URI-like properties should not be strings + + /// + /// A map between a variable name and its value. The value is used for substitution in the server's URL template. + /// + public IDictionary Variables { get; } + + /// + /// Construct a new object. + /// + /// URL to the target host + /// Substitution variables for the server's URL template +#pragma warning disable CA1054 // URI-like parameters should not be strings + public RestApiOperationServer(string? url = null, IDictionary? variables = null) +#pragma warning restore CA1054 // URI-like parameters should not be strings + { + this.Url = string.IsNullOrEmpty(url) ? null : url; + this.Variables = variables ?? new Dictionary(); + } +} diff --git a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationServerVariable.cs b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationServerVariable.cs new file mode 100644 index 000000000000..eba41dca754f --- /dev/null +++ b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationServerVariable.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel.Plugins.OpenApi; + +/// +/// REST API Operation Server Variable. +/// +public sealed class RestApiOperationServerVariable +{ + /// + /// An optional description for the server variable. CommonMark syntax MAY be used for rich text representation. + /// + public string? Description { get; } + + /// + /// REQUIRED. The default value to use for substitution, and to send, if an alternate value is not supplied. + /// Unlike the Schema Object's default, this value MUST be provided by the consumer. + /// + public string Default { get; } + + /// + /// An enumeration of string values to be used if the substitution options are from a limited set. + /// + public List? Enum { get; } + + /// + /// Construct a new object. + /// + /// The default value to use for substitution. + /// An optional description for the server variable. + /// An enumeration of string values to be used if the substitution options are from a limited set. + public RestApiOperationServerVariable(string defaultValue, string? description = null, List? enumValues = null) + { + this.Default = defaultValue; + this.Description = description; + this.Enum = enumValues; + } + + /// + /// Return true if the value is valid based on the enumeration of string values to be used. + /// + /// Value to be used as a substitution. + public bool IsValid(string? value) + { + return this.Enum?.Contains(value!) ?? true; + } +} diff --git a/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs b/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs index 2d6b856b4700..e0614f67e8fb 100644 --- a/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs +++ b/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs @@ -158,11 +158,11 @@ private static List ExtractRestApiOperations(OpenApiDocument d { var result = new List(); - var serverUrl = document.Servers.FirstOrDefault()?.Url; + var server = document.Servers.FirstOrDefault(); foreach (var pathPair in document.Paths) { - var operations = CreateRestApiOperations(serverUrl, pathPair.Key, pathPair.Value, operationsToExclude, logger); + var operations = CreateRestApiOperations(server, pathPair.Key, pathPair.Value, operationsToExclude, logger); result.AddRange(operations); } @@ -173,15 +173,16 @@ private static List ExtractRestApiOperations(OpenApiDocument d /// /// Creates REST API operation. /// - /// The server url. + /// Rest server. /// Rest resource path. /// Rest resource metadata. /// Optional list of operations not to import, e.g. in case they are not supported /// Used to perform logging. /// Rest operation. - internal static List CreateRestApiOperations(string? serverUrl, string path, OpenApiPathItem pathItem, IList? operationsToExclude, ILogger logger) + internal static List CreateRestApiOperations(OpenApiServer? server, string path, OpenApiPathItem pathItem, IList? operationsToExclude, ILogger logger) { var operations = new List(); + var operationServer = CreateRestApiOperationServer(server); foreach (var operationPair in pathItem.Operations) { @@ -196,7 +197,7 @@ internal static List CreateRestApiOperations(string? serverUrl var operation = new RestApiOperation( operationItem.OperationId, - string.IsNullOrEmpty(serverUrl) ? null : new Uri(serverUrl), + operationServer, path, new HttpMethod(method), string.IsNullOrEmpty(operationItem.Description) ? operationItem.Summary : operationItem.Description, @@ -214,6 +215,16 @@ internal static List CreateRestApiOperations(string? serverUrl return operations; } + /// + /// Build a object from the given object. + /// + /// Represents the server which hosts the REST API. + private static RestApiOperationServer CreateRestApiOperationServer(OpenApiServer? server) + { + var variables = server?.Variables.ToDictionary(item => item.Key, item => new RestApiOperationServerVariable(item.Value.Default, item.Value.Description, item.Value.Enum)); + return new(server?.Url, variables); + } + /// /// Build a dictionary of extension key value pairs from the given open api extension model, where the key is the extension name /// and the value is either the actual value in the case of primitive types like string, int, date, etc, or a json string in the diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/Extensions/RestApiOperationExtensionsTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/Extensions/RestApiOperationExtensionsTests.cs index 022a12d95719..2a79ee3bcc33 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/Extensions/RestApiOperationExtensionsTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/Extensions/RestApiOperationExtensionsTests.cs @@ -256,7 +256,7 @@ private static RestApiOperation CreateTestOperation(string method, RestApiOperat { return new RestApiOperation( id: "fake-id", - serverUrl: url, + server: new(url?.AbsoluteUri), path: "fake-path", method: new HttpMethod(method), description: "fake-description", diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs index 1e3109c0c1ff..74b16b14e096 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs @@ -103,7 +103,7 @@ public async Task ItCanParsePutOperationMetadataSuccessfullyAsync() var putOperation = restApi.Operations.Single(o => o.Id == "SetSecret"); Assert.NotNull(putOperation); Assert.Equal("Sets a secret in a specified key vault.", putOperation.Description); - Assert.Equal("https://my-key-vault.vault.azure.net/", putOperation.ServerUrl?.AbsoluteUri); + Assert.Equal("https://my-key-vault.vault.azure.net", putOperation.Server.Url); Assert.Equal(HttpMethod.Put, putOperation.Method); Assert.Equal("/secrets/{secret-name}", putOperation.Path); @@ -266,7 +266,7 @@ public async Task ItCanWorkWithDocumentsWithoutHostAndSchemaAttributesAsync() var restApi = await this._sut.ParseAsync(stream); //Assert - Assert.All(restApi.Operations, (op) => Assert.Null(op.ServerUrl)); + Assert.All(restApi.Operations, (op) => Assert.Null(op.Server.Url)); } [Fact] diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs index cb9eec5eb508..c2056ecf4606 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs @@ -104,7 +104,7 @@ public async Task ItCanParsePutOperationMetadataSuccessfullyAsync() var putOperation = restApi.Operations.Single(o => o.Id == "SetSecret"); Assert.NotNull(putOperation); Assert.Equal("Sets a secret in a specified key vault.", putOperation.Description); - Assert.Equal("https://my-key-vault.vault.azure.net/", putOperation.ServerUrl?.AbsoluteUri); + Assert.Equal("https://my-key-vault.vault.azure.net", putOperation.Server.Url); Assert.Equal(HttpMethod.Put, putOperation.Method); Assert.Equal("/secrets/{secret-name}", putOperation.Path); @@ -289,7 +289,7 @@ public async Task ItCanWorkWithDocumentsWithoutServersAttributeAsync() var restApi = await this._sut.ParseAsync(stream); //Assert - Assert.All(restApi.Operations, (op) => Assert.Null(op.ServerUrl)); + Assert.All(restApi.Operations, (op) => Assert.Null(op.Server.Url)); } [Fact] @@ -305,7 +305,7 @@ public async Task ItCanWorkWithDocumentsWithEmptyServersAttributeAsync() var restApi = await this._sut.ParseAsync(stream); //Assert - Assert.All(restApi.Operations, (op) => Assert.Null(op.ServerUrl)); + Assert.All(restApi.Operations, (op) => Assert.Null(op.Server.Url)); } [Theory] diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs index 60e182f1bfc6..54c1f1492f84 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs @@ -104,7 +104,7 @@ public async Task ItCanParsePutOperationMetadataSuccessfullyAsync() var putOperation = restApi.Operations.Single(o => o.Id == "SetSecret"); Assert.NotNull(putOperation); Assert.Equal("Sets a secret in a specified key vault.", putOperation.Description); - Assert.Equal("https://my-key-vault.vault.azure.net/", putOperation.ServerUrl?.AbsoluteUri); + Assert.Equal("https://my-key-vault.vault.azure.net", putOperation.Server.Url); Assert.Equal(HttpMethod.Put, putOperation.Method); Assert.Equal("/secrets/{secret-name}", putOperation.Path); @@ -266,7 +266,7 @@ public async Task ItCanWorkWithDocumentsWithoutServersAttributeAsync() var restApi = await this._sut.ParseAsync(stream); //Assert - Assert.All(restApi.Operations, (op) => Assert.Null(op.ServerUrl)); + Assert.All(restApi.Operations, (op) => Assert.Null(op.Server.Url)); } [Fact] @@ -282,7 +282,7 @@ public async Task ItCanWorkWithDocumentsWithEmptyServersAttributeAsync() var restApi = await this._sut.ParseAsync(stream); //Assert - Assert.All(restApi.Operations, (op) => Assert.Null(op.ServerUrl)); + Assert.All(restApi.Operations, (op) => Assert.Null(op.Server.Url)); } [Theory] diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs index 6d72c632e6d7..9bbd49a287d3 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationRunnerTests.cs @@ -66,7 +66,7 @@ public async Task ItCanRunCreateAndUpdateOperationsWithJsonPayloadSuccessfullyAs var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", httpMethod, "fake-description", @@ -144,7 +144,7 @@ public async Task ItCanRunCreateAndUpdateOperationsWithPlainTextPayloadSuccessfu var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", httpMethod, "fake-description", @@ -210,7 +210,7 @@ public async Task ItShouldAddHeadersToHttpRequestAsync() var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Get, "fake-description", @@ -275,7 +275,7 @@ public async Task ItShouldAddUserAgentHeaderToHttpRequestIfConfiguredAsync() var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Get, "fake-description", @@ -320,7 +320,7 @@ public async Task ItShouldBuildJsonPayloadDynamicallyAsync() var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -384,7 +384,7 @@ public async Task ItShouldBuildJsonPayloadDynamicallyUsingPayloadMetadataDataTyp var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -473,7 +473,7 @@ public async Task ItShouldBuildJsonPayloadDynamicallyResolvingArgumentsByFullNam var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -543,7 +543,7 @@ public async Task ItShouldThrowExceptionIfPayloadMetadataDoesNotHaveContentTypeA // Arrange var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -570,7 +570,7 @@ public async Task ItShouldThrowExceptionIfContentTypeArgumentIsNotProvidedAsync( // Arrange var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -601,7 +601,7 @@ public async Task ItShouldUsePayloadArgumentForPlainTextContentTypeWhenBuildingP var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -641,7 +641,7 @@ public async Task ItShouldUsePayloadAndContentTypeArgumentsIfDynamicPayloadBuild var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -687,7 +687,7 @@ public async Task ItShouldBuildJsonPayloadDynamicallyExcludingOptionalParameters var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -733,7 +733,7 @@ public async Task ItShouldBuildJsonPayloadDynamicallyIncludingOptionalParameters var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -788,7 +788,7 @@ public async Task ItShouldAddRequiredQueryStringParametersIfTheirArgumentsProvid var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Get, "fake-description", @@ -836,7 +836,7 @@ public async Task ItShouldAddNotRequiredQueryStringParametersIfTheirArgumentsPro var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Get, "fake-description", @@ -884,7 +884,7 @@ public async Task ItShouldSkipNotRequiredQueryStringParametersIfNoArgumentsProvi var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Get, "fake-description", @@ -923,7 +923,7 @@ public async Task ItShouldThrowExceptionIfNoArgumentProvidedForRequiredQueryStri var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Get, "fake-description", @@ -954,7 +954,7 @@ public async Task ItShouldReadContentAsStringSuccessfullyAsync(string contentTyp var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -996,7 +996,7 @@ public async Task ItShouldReadContentAsBytesSuccessfullyAsync(string contentType var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -1031,7 +1031,7 @@ public async Task ItShouldThrowExceptionForUnsupportedContentTypeAsync() var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -1074,7 +1074,7 @@ public async Task ItShouldReturnRequestUriAndContentAsync() var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -1122,7 +1122,7 @@ public async Task ItShouldHandleNoContentAsync() var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -1170,7 +1170,7 @@ public async Task ItShouldSetHttpRequestMessageOptionsAsync() var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -1214,7 +1214,7 @@ public async Task ItShouldIncludeRequestDataWhenOperationCanceledExceptionIsThro var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Post, "fake-description", @@ -1244,7 +1244,7 @@ public async Task ItShouldUseCustomHttpResponseContentReaderAsync() // Arrange var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Get, "fake-description", @@ -1278,7 +1278,7 @@ public async Task ItShouldUseDefaultHttpResponseContentReaderIfCustomDoesNotRetu var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Get, "fake-description", @@ -1310,7 +1310,7 @@ public async Task ItShouldDisposeContentStreamAndHttpResponseContentMessageAsync // Arrange var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Get, "fake-description", @@ -1397,7 +1397,7 @@ public async Task ItShouldReturnExpectedSchemaAsync(string expectedStatusCode, p { var operation = new RestApiOperation( "fake-id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path", HttpMethod.Get, "fake-description", diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationTests.cs index ea83585baa50..0a9099a34d8e 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationTests.cs @@ -24,7 +24,7 @@ public void ItShouldUseHostUrlIfNoOverrideProvided() // Arrange var sut = new RestApiOperation( "fake_id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "/", HttpMethod.Get, "fake_description", @@ -46,7 +46,7 @@ public void ItShouldUseHostUrlOverrideIfProvided() // Arrange var sut = new RestApiOperation( "fake_id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "/", HttpMethod.Get, "fake_description", @@ -87,7 +87,7 @@ public void ItShouldBuildOperationUrlWithPathParametersFromArguments() var sut = new RestApiOperation( "fake_id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "/{p1}/{p2}/other_fake_path_section", HttpMethod.Get, "fake_description", @@ -130,7 +130,7 @@ public void ItShouldBuildOperationUrlWithEncodedArguments() var sut = new RestApiOperation( "fake_id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "/{p1}/{p2}/other_fake_path_section", HttpMethod.Get, "fake_description", @@ -172,7 +172,7 @@ public void ShouldBuildResourceUrlWithoutQueryString() var sut = new RestApiOperation( "fake_id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "{fake-path}/", HttpMethod.Get, "fake_description", @@ -213,7 +213,7 @@ public void ItShouldBuildQueryString() var sut = new RestApiOperation( "fake_id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path/", HttpMethod.Get, "fake_description", @@ -247,7 +247,7 @@ public void ItShouldBuildQueryStringWithQuotes() var sut = new RestApiOperation( "fake_id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path/", HttpMethod.Get, "fake_description", @@ -280,7 +280,7 @@ public void ItShouldBuildQueryStringForArray() var sut = new RestApiOperation( "fake_id", - new Uri("https://fake-random-test-host"), + new RestApiOperationServer("https://fake-random-test-host"), "fake-path/", HttpMethod.Get, "fake_description", @@ -327,7 +327,7 @@ public void ItShouldRenderHeaderValuesFromArguments() { "fake_header_two", "fake_header_two_value" } }; - var sut = new RestApiOperation("fake_id", new Uri("http://fake_url"), "fake_path", HttpMethod.Get, "fake_description", parameters); + var sut = new RestApiOperation("fake_id", new RestApiOperationServer("http://fake_url"), "fake_path", HttpMethod.Get, "fake_description", parameters); // Act var headers = sut.BuildHeaders(arguments); @@ -352,7 +352,7 @@ public void ShouldThrowExceptionIfNoValueProvidedForRequiredHeader() new(name: "fake_header_two", type : "string", isRequired : false, expand : false, location : RestApiOperationParameterLocation.Header, style: RestApiOperationParameterStyle.Simple) }; - var sut = new RestApiOperation("fake_id", new Uri("http://fake_url"), "fake_path", HttpMethod.Get, "fake_description", metadata); + var sut = new RestApiOperation("fake_id", new RestApiOperationServer("http://fake_url"), "fake_path", HttpMethod.Get, "fake_description", metadata); // Act void Act() => sut.BuildHeaders(new Dictionary()); @@ -376,7 +376,7 @@ public void ItShouldSkipOptionalHeaderHavingNoValue() ["fake_header_one"] = "fake_header_one_value" }; - var sut = new RestApiOperation("fake_id", new Uri("http://fake_url"), "fake_path", HttpMethod.Get, "fake_description", metadata); + var sut = new RestApiOperation("fake_id", new RestApiOperationServer("http://fake_url"), "fake_path", HttpMethod.Get, "fake_description", metadata); // Act var headers = sut.BuildHeaders(arguments); @@ -404,7 +404,7 @@ public void ItShouldCreateHeaderWithCommaSeparatedValues() ["h2"] = "[1,2,3]" }; - var sut = new RestApiOperation("fake_id", new Uri("https://fake-random-test-host"), "fake_path", HttpMethod.Get, "fake_description", metadata); + var sut = new RestApiOperation("fake_id", new RestApiOperationServer("https://fake-random-test-host"), "fake_path", HttpMethod.Get, "fake_description", metadata); // Act var headers = sut.BuildHeaders(arguments); @@ -433,7 +433,7 @@ public void ItShouldCreateHeaderWithPrimitiveValue() ["h2"] = true }; - var sut = new RestApiOperation("fake_id", new Uri("https://fake-random-test-host"), "fake_path", HttpMethod.Get, "fake_description", metadata); + var sut = new RestApiOperation("fake_id", new RestApiOperationServer("https://fake-random-test-host"), "fake_path", HttpMethod.Get, "fake_description", metadata); // Act var headers = sut.BuildHeaders(arguments); @@ -462,7 +462,7 @@ public void ItShouldMixAndMatchHeadersOfDifferentValueTypes() ["h2"] = "false" }; - var sut = new RestApiOperation("fake_id", new Uri("https://fake-random-test-host"), "fake_path", HttpMethod.Get, "fake_description", metadata); + var sut = new RestApiOperation("fake_id", new RestApiOperationServer("https://fake-random-test-host"), "fake_path", HttpMethod.Get, "fake_description", metadata); // Act var headers = sut.BuildHeaders(arguments); @@ -702,4 +702,72 @@ public void ItAddsTheRightTypesInAddKernel() Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); } + + [Fact] + public void ItShouldUseDefaultServerVariableIfNoOverrideProvided() + { + // Arrange + var sut = new RestApiOperation( + "fake_id", + new RestApiOperationServer("https://example.com/{version}", new Dictionary { { "version", new RestApiOperationServerVariable("v2") } }), + "/items", + HttpMethod.Get, + "fake_description", + [] + ); + + var arguments = new Dictionary(); + + // Act + var url = sut.BuildOperationUrl(arguments); + + // Assert + Assert.Equal("https://example.com/v2/items", url.OriginalString); + } + + [Fact] + public void ItShouldUseDefaultServerVariableIfInvalidOverrideProvided() + { + // Arrange + var version = new RestApiOperationServerVariable("v2", null, ["v1", "v2"]); + var sut = new RestApiOperation( + "fake_id", + new RestApiOperationServer("https://example.com/{version}", new Dictionary { { "version", version } }), + "/items", + HttpMethod.Get, + "fake_description", + [] + ); + + var arguments = new Dictionary() { { "version", "v3" } }; + + // Act + var url = sut.BuildOperationUrl(arguments); + + // Assert + Assert.Equal("https://example.com/v2/items", url.OriginalString); + } + + [Fact] + public void ItShouldUseServerVariableOverrideIfProvided() + { + // Arrange + var version = new RestApiOperationServerVariable("v2", null, ["v1", "v2", "v3"]); + var sut = new RestApiOperation( + "fake_id", + new RestApiOperationServer("https://example.com/{version}", new Dictionary { { "version", version } }), + "/items", + HttpMethod.Get, + "fake_description", + [] + ); + + var arguments = new Dictionary() { { "version", "v3" } }; + + // Act + var url = sut.BuildOperationUrl(arguments); + + // Assert + Assert.Equal("https://example.com/v3/items", url.OriginalString); + } } diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBMemoryStoreTestsFixture.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBMemoryStoreTestsFixture.cs index 1b1255c46b68..6854e7e7fdf8 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBMemoryStoreTestsFixture.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBMemoryStoreTestsFixture.cs @@ -54,7 +54,7 @@ public async Task DisposeAsync() private static string GetSetting(IConfigurationRoot configuration, string settingName) { - var settingValue = configuration[$"AzureCosmosDB:{settingName}"]; + var settingValue = configuration[$"AzureCosmosDBMongoDB:{settingName}"]; if (string.IsNullOrWhiteSpace(settingValue)) { throw new ArgumentNullException($"{settingValue} string is not configured"); diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreCollectionFixture.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreCollectionFixture.cs new file mode 100644 index 000000000000..43f30eb5d520 --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreCollectionFixture.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.AzureCosmosDBMongoDB; + +[CollectionDefinition("AzureCosmosDBMongoDBVectorStoreCollection")] +public class AzureCosmosDBMongoDBVectorStoreCollectionFixture : ICollectionFixture +{ } diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreFixture.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreFixture.cs new file mode 100644 index 000000000000..3af1a3c66b1a --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreFixture.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel.Data; +using MongoDB.Driver; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.AzureCosmosDBMongoDB; + +public class AzureCosmosDBMongoDBVectorStoreFixture : IAsyncLifetime +{ + private readonly List _testCollections = ["sk-test-hotels", "sk-test-contacts", "sk-test-addresses"]; + + /// Main test collection for tests. + public string TestCollection => this._testCollections[0]; + + /// that can be used to manage the collections in Azure CosmosDB MongoDB. + public IMongoDatabase MongoDatabase { get; } + + /// Gets the manually created vector store record definition for Azure CosmosDB MongoDB test model. + public VectorStoreRecordDefinition HotelVectorStoreRecordDefinition { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + public AzureCosmosDBMongoDBVectorStoreFixture() + { + var configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile( + path: "testsettings.development.json", + optional: false, + reloadOnChange: true + ) + .AddEnvironmentVariables() + .Build(); + + var connectionString = GetConnectionString(configuration); + var client = new MongoClient(connectionString); + + this.MongoDatabase = client.GetDatabase("test"); + + this.HotelVectorStoreRecordDefinition = new() + { + Properties = + [ + new VectorStoreRecordKeyProperty("HotelId", typeof(string)), + new VectorStoreRecordDataProperty("HotelName", typeof(string)), + new VectorStoreRecordDataProperty("HotelCode", typeof(int)), + new VectorStoreRecordDataProperty("ParkingIncluded", typeof(bool)) { StoragePropertyName = "parking_is_included" }, + new VectorStoreRecordDataProperty("HotelRating", typeof(float)), + new VectorStoreRecordDataProperty("Tags", typeof(List)), + new VectorStoreRecordDataProperty("Description", typeof(string)), + new VectorStoreRecordVectorProperty("DescriptionEmbedding", typeof(ReadOnlyMemory?)) { Dimensions = 4, IndexKind = IndexKind.IvfFlat, DistanceFunction = DistanceFunction.CosineDistance } + ] + }; + } + + public async Task InitializeAsync() + { + foreach (var collection in this._testCollections) + { + await this.MongoDatabase.CreateCollectionAsync(collection); + } + } + + public async Task DisposeAsync() + { + foreach (var collection in this._testCollections) + { + await this.MongoDatabase.DropCollectionAsync(collection); + } + } + +#pragma warning disable CS8618 + public record AzureCosmosDBMongoDBHotel() + { + /// The key of the record. + [VectorStoreRecordKey] + public string HotelId { get; init; } + + /// A string metadata field. + [VectorStoreRecordData] + public string? HotelName { get; set; } + + /// An int metadata field. + [VectorStoreRecordData] + public int HotelCode { get; set; } + + /// A float metadata field. + [VectorStoreRecordData] + public float? HotelRating { get; set; } + + /// A bool metadata field. + [VectorStoreRecordData(StoragePropertyName = "parking_is_included")] + public bool ParkingIncluded { get; set; } + + /// An array metadata field. + [VectorStoreRecordData] + public List Tags { get; set; } = []; + + /// A data field. + [VectorStoreRecordData] + public string Description { get; set; } + + /// A vector field. + [VectorStoreRecordVector(Dimensions: 4, IndexKind: IndexKind.IvfFlat, DistanceFunction: DistanceFunction.CosineDistance)] + public ReadOnlyMemory? DescriptionEmbedding { get; set; } + } +#pragma warning restore CS8618 + + #region private + + private static string GetConnectionString(IConfigurationRoot configuration) + { + var settingValue = configuration["AzureCosmosDBMongoDB:ConnectionString"]; + if (string.IsNullOrWhiteSpace(settingValue)) + { + throw new ArgumentNullException($"{settingValue} string is not configured"); + } + + return settingValue; + } + + #endregion +} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollectionTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollectionTests.cs new file mode 100644 index 000000000000..f8a6053c78fd --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreRecordCollectionTests.cs @@ -0,0 +1,392 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; +using Microsoft.SemanticKernel.Data; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Xunit; +using static SemanticKernel.IntegrationTests.Connectors.AzureCosmosDBMongoDB.AzureCosmosDBMongoDBVectorStoreFixture; + +namespace SemanticKernel.IntegrationTests.Connectors.AzureCosmosDBMongoDB; + +[Collection("AzureCosmosDBMongoDBVectorStoreCollection")] +public class AzureCosmosDBMongoDBVectorStoreRecordCollectionTests(AzureCosmosDBMongoDBVectorStoreFixture fixture) +{ + private const string? SkipReason = "Azure CosmosDB MongoDB cluster is required"; + + [Theory(Skip = SkipReason)] + [InlineData("sk-test-hotels", true)] + [InlineData("nonexistentcollection", false)] + public async Task CollectionExistsReturnsCollectionStateAsync(string collectionName, bool expectedExists) + { + // Arrange + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection(fixture.MongoDatabase, collectionName); + + // Act + var actual = await sut.CollectionExistsAsync(); + + // Assert + Assert.Equal(expectedExists, actual); + } + + [Fact(Skip = SkipReason)] + public async Task ItCanCreateCollectionAsync() + { + // Arrange + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection(fixture.MongoDatabase, fixture.TestCollection); + + // Act + await sut.CreateCollectionAsync(); + + // Assert + Assert.True(await sut.CollectionExistsAsync()); + } + + [Theory(Skip = SkipReason)] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public async Task ItCanCreateCollectionUpsertAndGetAsync(bool includeVectors, bool useRecordDefinition) + { + // Arrange + const string HotelId = "55555555-5555-5555-5555-555555555555"; + + var collectionNamePostfix = useRecordDefinition ? "with-definition" : "with-type"; + var collectionName = $"collection-{collectionNamePostfix}"; + + var options = new AzureCosmosDBMongoDBVectorStoreRecordCollectionOptions + { + VectorStoreRecordDefinition = useRecordDefinition ? fixture.HotelVectorStoreRecordDefinition : null + }; + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection(fixture.MongoDatabase, collectionName); + + var record = this.CreateTestHotel(HotelId); + + // Act + await sut.CreateCollectionAsync(); + var upsertResult = await sut.UpsertAsync(record); + var getResult = await sut.GetAsync(HotelId, new() { IncludeVectors = includeVectors }); + + // Assert + Assert.True(await sut.CollectionExistsAsync()); + await sut.DeleteCollectionAsync(); + + Assert.Equal(HotelId, upsertResult); + Assert.NotNull(getResult); + + Assert.Equal(record.HotelId, getResult.HotelId); + Assert.Equal(record.HotelName, getResult.HotelName); + Assert.Equal(record.HotelCode, getResult.HotelCode); + Assert.Equal(record.HotelRating, getResult.HotelRating); + Assert.Equal(record.ParkingIncluded, getResult.ParkingIncluded); + Assert.Equal(record.Tags.ToArray(), getResult.Tags.ToArray()); + Assert.Equal(record.Description, getResult.Description); + + if (includeVectors) + { + Assert.NotNull(getResult.DescriptionEmbedding); + Assert.Equal(record.DescriptionEmbedding!.Value.ToArray(), getResult.DescriptionEmbedding.Value.ToArray()); + } + else + { + Assert.Null(getResult.DescriptionEmbedding); + } + } + + [Fact(Skip = SkipReason)] + public async Task ItCanDeleteCollectionAsync() + { + // Arrange + const string TempCollectionName = "temp-test"; + await fixture.MongoDatabase.CreateCollectionAsync(TempCollectionName); + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection(fixture.MongoDatabase, TempCollectionName); + + Assert.True(await sut.CollectionExistsAsync()); + + // Act + await sut.DeleteCollectionAsync(); + + // Assert + Assert.False(await sut.CollectionExistsAsync()); + } + + [Fact(Skip = SkipReason)] + public async Task ItCanGetAndDeleteRecordAsync() + { + // Arrange + const string HotelId = "55555555-5555-5555-5555-555555555555"; + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection(fixture.MongoDatabase, fixture.TestCollection); + + var record = this.CreateTestHotel(HotelId); + + var upsertResult = await sut.UpsertAsync(record); + var getResult = await sut.GetAsync(HotelId); + + Assert.Equal(HotelId, upsertResult); + Assert.NotNull(getResult); + + // Act + await sut.DeleteAsync(HotelId); + + getResult = await sut.GetAsync(HotelId); + + // Assert + Assert.Null(getResult); + } + + [Fact(Skip = SkipReason)] + public async Task ItCanGetAndDeleteBatchAsync() + { + // Arrange + const string HotelId1 = "11111111-1111-1111-1111-111111111111"; + const string HotelId2 = "22222222-2222-2222-2222-222222222222"; + const string HotelId3 = "33333333-3333-3333-3333-333333333333"; + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection(fixture.MongoDatabase, fixture.TestCollection); + + var record1 = this.CreateTestHotel(HotelId1); + var record2 = this.CreateTestHotel(HotelId2); + var record3 = this.CreateTestHotel(HotelId3); + + var upsertResults = await sut.UpsertBatchAsync([record1, record2, record3]).ToListAsync(); + var getResults = await sut.GetBatchAsync([HotelId1, HotelId2, HotelId3]).ToListAsync(); + + Assert.Equal([HotelId1, HotelId2, HotelId3], upsertResults); + + Assert.NotNull(getResults.First(l => l.HotelId == HotelId1)); + Assert.NotNull(getResults.First(l => l.HotelId == HotelId2)); + Assert.NotNull(getResults.First(l => l.HotelId == HotelId3)); + + // Act + await sut.DeleteBatchAsync([HotelId1, HotelId2, HotelId3]); + + getResults = await sut.GetBatchAsync([HotelId1, HotelId2, HotelId3]).ToListAsync(); + + // Assert + Assert.Empty(getResults); + } + + [Fact(Skip = SkipReason)] + public async Task ItCanUpsertRecordAsync() + { + // Arrange + const string HotelId = "55555555-5555-5555-5555-555555555555"; + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection(fixture.MongoDatabase, fixture.TestCollection); + + var record = this.CreateTestHotel(HotelId); + + var upsertResult = await sut.UpsertAsync(record); + var getResult = await sut.GetAsync(HotelId); + + Assert.Equal(HotelId, upsertResult); + Assert.NotNull(getResult); + + // Act + record.HotelName = "Updated name"; + record.HotelRating = 10; + + upsertResult = await sut.UpsertAsync(record); + getResult = await sut.GetAsync(HotelId); + + // Assert + Assert.NotNull(getResult); + Assert.Equal("Updated name", getResult.HotelName); + Assert.Equal(10, getResult.HotelRating); + } + + [Fact(Skip = SkipReason)] + public async Task UpsertWithModelWorksCorrectlyAsync() + { + // Arrange + var definition = new VectorStoreRecordDefinition + { + Properties = new List + { + new VectorStoreRecordKeyProperty("Id", typeof(string)), + new VectorStoreRecordDataProperty("HotelName", typeof(string)) + } + }; + + var model = new TestModel { Id = "key", HotelName = "Test Name" }; + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + fixture.MongoDatabase, + fixture.TestCollection, + new() { VectorStoreRecordDefinition = definition }); + + // Act + var upsertResult = await sut.UpsertAsync(model); + var getResult = await sut.GetAsync(model.Id); + + // Assert + Assert.Equal("key", upsertResult); + + Assert.NotNull(getResult); + Assert.Equal("key", getResult.Id); + Assert.Equal("Test Name", getResult.HotelName); + } + + [Fact(Skip = SkipReason)] + public async Task UpsertWithVectorStoreModelWorksCorrectlyAsync() + { + // Arrange + var model = new VectorStoreTestModel { Id = "key", HotelName = "Test Name" }; + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection(fixture.MongoDatabase, fixture.TestCollection); + + // Act + var upsertResult = await sut.UpsertAsync(model); + var getResult = await sut.GetAsync(model.Id); + + // Assert + Assert.Equal("key", upsertResult); + + Assert.NotNull(getResult); + Assert.Equal("key", getResult.Id); + Assert.Equal("Test Name", getResult.HotelName); + } + + [Fact(Skip = SkipReason)] + public async Task UpsertWithBsonModelWorksCorrectlyAsync() + { + // Arrange + var definition = new VectorStoreRecordDefinition + { + Properties = new List + { + new VectorStoreRecordKeyProperty("Id", typeof(string)), + new VectorStoreRecordDataProperty("HotelName", typeof(string)) + } + }; + + var model = new BsonTestModel { Id = "key", HotelName = "Test Name" }; + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection( + fixture.MongoDatabase, + fixture.TestCollection, + new() { VectorStoreRecordDefinition = definition }); + + // Act + var upsertResult = await sut.UpsertAsync(model); + var getResult = await sut.GetAsync(model.Id); + + // Assert + Assert.Equal("key", upsertResult); + + Assert.NotNull(getResult); + Assert.Equal("key", getResult.Id); + Assert.Equal("Test Name", getResult.HotelName); + } + + [Fact(Skip = SkipReason)] + public async Task UpsertWithBsonVectorStoreModelWorksCorrectlyAsync() + { + // Arrange + var model = new BsonVectorStoreTestModel { Id = "key", HotelName = "Test Name" }; + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection(fixture.MongoDatabase, fixture.TestCollection); + + // Act + var upsertResult = await sut.UpsertAsync(model); + var getResult = await sut.GetAsync(model.Id); + + // Assert + Assert.Equal("key", upsertResult); + + Assert.NotNull(getResult); + Assert.Equal("key", getResult.Id); + Assert.Equal("Test Name", getResult.HotelName); + } + + [Fact(Skip = SkipReason)] + public async Task UpsertWithBsonVectorStoreWithNameModelWorksCorrectlyAsync() + { + // Arrange + var model = new BsonVectorStoreWithNameTestModel { Id = "key", HotelName = "Test Name" }; + + var sut = new AzureCosmosDBMongoDBVectorStoreRecordCollection(fixture.MongoDatabase, fixture.TestCollection); + + // Act + var upsertResult = await sut.UpsertAsync(model); + var getResult = await sut.GetAsync(model.Id); + + // Assert + Assert.Equal("key", upsertResult); + + Assert.NotNull(getResult); + Assert.Equal("key", getResult.Id); + Assert.Equal("Test Name", getResult.HotelName); + } + + #region private + + private AzureCosmosDBMongoDBHotel CreateTestHotel(string hotelId) + { + return new AzureCosmosDBMongoDBHotel + { + HotelId = hotelId, + HotelName = $"My Hotel {hotelId}", + HotelCode = 42, + HotelRating = 4.5f, + ParkingIncluded = true, + Tags = { "t1", "t2" }, + Description = "This is a great hotel.", + DescriptionEmbedding = new[] { 30f, 31f, 32f, 33f }, + }; + } + + private sealed class TestModel + { + public string? Id { get; set; } + + public string? HotelName { get; set; } + } + + private sealed class VectorStoreTestModel + { + [VectorStoreRecordKey] + public string? Id { get; set; } + + [VectorStoreRecordData(StoragePropertyName = "hotel_name")] + public string? HotelName { get; set; } + } + + private sealed class BsonTestModel + { + [BsonId] + public string? Id { get; set; } + + [BsonElement("hotel_name")] + public string? HotelName { get; set; } + } + + private sealed class BsonVectorStoreTestModel + { + [BsonId] + [VectorStoreRecordKey] + public string? Id { get; set; } + + [BsonElement("hotel_name")] + [VectorStoreRecordData] + public string? HotelName { get; set; } + } + + private sealed class BsonVectorStoreWithNameTestModel + { + [BsonId] + [VectorStoreRecordKey] + public string? Id { get; set; } + + [BsonElement("bson_hotel_name")] + [VectorStoreRecordData(StoragePropertyName = "storage_hotel_name")] + public string? HotelName { get; set; } + } + + #endregion +} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreTests.cs new file mode 100644 index 000000000000..9be1378b7b86 --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBMongoDB/AzureCosmosDBMongoDBVectorStoreTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBMongoDB; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.AzureCosmosDBMongoDB; + +[Collection("AzureCosmosDBMongoDBVectorStoreCollection")] +public class AzureCosmosDBMongoDBVectorStoreTests(AzureCosmosDBMongoDBVectorStoreFixture fixture) +{ + private const string? SkipReason = "Azure CosmosDB MongoDB cluster is required"; + + [Fact(Skip = SkipReason)] + public async Task ItCanGetAListOfExistingCollectionNamesAsync() + { + // Arrange + var sut = new AzureCosmosDBMongoDBVectorStore(fixture.MongoDatabase); + + // Act + var collectionNames = await sut.ListCollectionNamesAsync().ToListAsync(); + + // Assert + Assert.Contains("sk-test-hotels", collectionNames); + Assert.Contains("sk-test-contacts", collectionNames); + Assert.Contains("sk-test-addresses", collectionNames); + } +} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLHotel.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLHotel.cs new file mode 100644 index 000000000000..859a453b2f81 --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLHotel.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.Data; + +namespace SemanticKernel.IntegrationTests.Connectors.Memory.AzureCosmosDBNoSQL; + +#pragma warning disable CS8618 + +public record AzureCosmosDBNoSQLHotel() +{ + /// The key of the record. + [VectorStoreRecordKey] + public string HotelId { get; init; } + + /// A string metadata field. + [VectorStoreRecordData(IsFilterable = true)] + public string? HotelName { get; set; } + + /// An int metadata field. + [VectorStoreRecordData(IsFullTextSearchable = true)] + public int HotelCode { get; set; } + + /// A float metadata field. + [VectorStoreRecordData] + public float? HotelRating { get; set; } + + /// A bool metadata field. + [JsonPropertyName("parking_is_included")] + [VectorStoreRecordData] + public bool ParkingIncluded { get; set; } + + /// An array metadata field. + [VectorStoreRecordData] + public List Tags { get; set; } = []; + + /// A data field. + [VectorStoreRecordData] + public string Description { get; set; } + + /// A vector field. + [VectorStoreRecordVector(Dimensions: 4, IndexKind: IndexKind.Flat, DistanceFunction: DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory? DescriptionEmbedding { get; set; } +} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLMemoryStoreTestsFixture.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLMemoryStoreTestsFixture.cs index 1df46166e63f..7e6f376a8684 100644 --- a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLMemoryStoreTestsFixture.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLMemoryStoreTestsFixture.cs @@ -47,7 +47,7 @@ public Task DisposeAsync() private static string GetSetting(IConfigurationRoot configuration, string settingName) { - var settingValue = configuration[$"AzureCosmosDB:{settingName}"]; + var settingValue = configuration[$"AzureCosmosDBNoSQL:{settingName}"]; if (string.IsNullOrWhiteSpace(settingValue)) { throw new ArgumentNullException($"{settingValue} string is not configured"); diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreCollectionFixture.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreCollectionFixture.cs new file mode 100644 index 000000000000..9702cdda490d --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreCollectionFixture.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.Memory.AzureCosmosDBNoSQL; + +[CollectionDefinition("AzureCosmosDBNoSQLVectorStoreCollection")] +public class AzureCosmosDBNoSQLVectorStoreCollectionFixture : ICollectionFixture +{ } diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreFixture.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreFixture.cs new file mode 100644 index 000000000000..74ac2f07ab48 --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreFixture.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.Memory.AzureCosmosDBNoSQL; + +public class AzureCosmosDBNoSQLVectorStoreFixture : IAsyncLifetime, IDisposable +{ + private const string DatabaseName = "testdb"; + + private readonly CosmosClient _cosmosClient; + + /// that can be used to manage the collections in Azure CosmosDB NoSQL. + public Database? Database { get; private set; } + + public AzureCosmosDBNoSQLVectorStoreFixture() + { + var configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile( + path: "testsettings.development.json", + optional: false, + reloadOnChange: true + ) + .AddEnvironmentVariables() + .Build(); + + var connectionString = GetConnectionString(configuration); + var options = new CosmosClientOptions { Serializer = new CosmosSystemTextJsonSerializer(JsonSerializerOptions.Default) }; + + this._cosmosClient = new CosmosClient(connectionString, options); + } + + public async Task InitializeAsync() + { + await this._cosmosClient.CreateDatabaseIfNotExistsAsync(DatabaseName); + + this.Database = this._cosmosClient.GetDatabase(DatabaseName); + } + + public async Task DisposeAsync() + { + await this.Database!.DeleteAsync(); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this._cosmosClient.Dispose(); + } + } + + #region private + + private static string GetConnectionString(IConfigurationRoot configuration) + { + var settingValue = configuration["AzureCosmosDBNoSQL:ConnectionString"]; + if (string.IsNullOrWhiteSpace(settingValue)) + { + throw new ArgumentNullException($"{settingValue} string is not configured"); + } + + return settingValue; + } + + #endregion +} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollectionTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollectionTests.cs new file mode 100644 index 000000000000..fe99094f1c1d --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreRecordCollectionTests.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Azure.Cosmos; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; +using Microsoft.SemanticKernel.Data; +using Xunit; +using DistanceFunction = Microsoft.SemanticKernel.Data.DistanceFunction; +using IndexKind = Microsoft.SemanticKernel.Data.IndexKind; + +namespace SemanticKernel.IntegrationTests.Connectors.Memory.AzureCosmosDBNoSQL; + +/// +/// Integration tests for class. +/// +[Collection("AzureCosmosDBNoSQLVectorStoreCollection")] +public sealed class AzureCosmosDBNoSQLVectorStoreRecordCollectionTests(AzureCosmosDBNoSQLVectorStoreFixture fixture) +{ + private const string? SkipReason = "Azure CosmosDB NoSQL cluster is required"; + + [Fact(Skip = SkipReason)] + public async Task ItCanCreateCollectionAsync() + { + // Arrange + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection(fixture.Database!, "test-create-collection"); + + // Act + await sut.CreateCollectionAsync(); + + // Assert + Assert.True(await sut.CollectionExistsAsync()); + } + + [Theory(Skip = SkipReason)] + [InlineData("sk-test-hotels", true)] + [InlineData("nonexistentcollection", false)] + public async Task CollectionExistsReturnsCollectionStateAsync(string collectionName, bool expectedExists) + { + // Arrange + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection(fixture.Database!, collectionName); + + if (expectedExists) + { + await fixture.Database!.CreateContainerIfNotExistsAsync(new ContainerProperties(collectionName, "/id")); + } + + // Act + var actual = await sut.CollectionExistsAsync(); + + // Assert + Assert.Equal(expectedExists, actual); + } + + [Theory(Skip = SkipReason)] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public async Task ItCanCreateCollectionUpsertAndGetAsync(bool includeVectors, bool useRecordDefinition) + { + // Arrange + const string HotelId = "55555555-5555-5555-5555-555555555555"; + + var collectionNamePostfix = useRecordDefinition ? "with-definition" : "with-type"; + collectionNamePostfix = includeVectors ? $"{collectionNamePostfix}-with-vectors" : $"{collectionNamePostfix}-without-vectors"; + var collectionName = $"collection-{collectionNamePostfix}"; + + var options = new AzureCosmosDBNoSQLVectorStoreRecordCollectionOptions + { + VectorStoreRecordDefinition = useRecordDefinition ? this.GetTestHotelRecordDefinition() : null + }; + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection(fixture.Database!, collectionName); + + var record = this.CreateTestHotel(HotelId); + + // Act + await sut.CreateCollectionAsync(); + var upsertResult = await sut.UpsertAsync(record); + var getResult = await sut.GetAsync(HotelId, new() { IncludeVectors = includeVectors }); + + // Assert + Assert.True(await sut.CollectionExistsAsync()); + await sut.DeleteCollectionAsync(); + + Assert.Equal(HotelId, upsertResult); + Assert.NotNull(getResult); + + Assert.Equal(record.HotelId, getResult.HotelId); + Assert.Equal(record.HotelName, getResult.HotelName); + Assert.Equal(record.HotelCode, getResult.HotelCode); + Assert.Equal(record.HotelRating, getResult.HotelRating); + Assert.Equal(record.ParkingIncluded, getResult.ParkingIncluded); + Assert.Equal(record.Tags.ToArray(), getResult.Tags.ToArray()); + Assert.Equal(record.Description, getResult.Description); + + if (includeVectors) + { + Assert.NotNull(getResult.DescriptionEmbedding); + Assert.Equal(record.DescriptionEmbedding!.Value.ToArray(), getResult.DescriptionEmbedding.Value.ToArray()); + } + else + { + Assert.Null(getResult.DescriptionEmbedding); + } + } + + [Fact(Skip = SkipReason)] + public async Task ItCanDeleteCollectionAsync() + { + // Arrange + const string TempCollectionName = "test-delete-collection"; + await fixture.Database!.CreateContainerAsync(new ContainerProperties(TempCollectionName, "/id")); + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection(fixture.Database!, TempCollectionName); + + Assert.True(await sut.CollectionExistsAsync()); + + // Act + await sut.DeleteCollectionAsync(); + + // Assert + Assert.False(await sut.CollectionExistsAsync()); + } + + [Theory(Skip = SkipReason)] + [InlineData("consistent-mode-collection", IndexingMode.Consistent)] + [InlineData("lazy-mode-collection", IndexingMode.Lazy)] + [InlineData("none-mode-collection", IndexingMode.None)] + public async Task ItCanGetAndDeleteRecordAsync(string collectionName, IndexingMode indexingMode) + { + // Arrange + const string HotelId = "55555555-5555-5555-5555-555555555555"; + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection( + fixture.Database!, + collectionName, + new() { IndexingMode = indexingMode, Automatic = indexingMode != IndexingMode.None }); + + await sut.CreateCollectionAsync(); + + var record = this.CreateTestHotel(HotelId); + + var upsertResult = await sut.UpsertAsync(record); + var getResult = await sut.GetAsync(HotelId); + + Assert.Equal(HotelId, upsertResult); + Assert.NotNull(getResult); + + // Act + await sut.DeleteAsync(HotelId); + + getResult = await sut.GetAsync(HotelId); + + // Assert + Assert.Null(getResult); + } + + [Fact(Skip = SkipReason)] + public async Task ItCanGetAndDeleteRecordWithPartitionKeyAsync() + { + // Arrange + const string HotelId = "55555555-5555-5555-5555-555555555555"; + const string HotelName = "Test Hotel Name"; + + IVectorStoreRecordCollection sut = + new AzureCosmosDBNoSQLVectorStoreRecordCollection( + fixture.Database!, + "delete-with-partition-key", + new() { PartitionKeyPropertyName = "HotelName" }); + + await sut.CreateCollectionAsync(); + + var record = this.CreateTestHotel(HotelId, HotelName); + + var upsertResult = await sut.UpsertAsync(record); + + var key = new AzureCosmosDBNoSQLCompositeKey(record.HotelId, record.HotelName!); + var getResult = await sut.GetAsync(key); + + Assert.Equal(HotelId, upsertResult.RecordKey); + Assert.Equal(HotelName, upsertResult.PartitionKey); + Assert.NotNull(getResult); + + // Act + await sut.DeleteAsync(key); + + getResult = await sut.GetAsync(key); + + // Assert + Assert.Null(getResult); + } + + [Fact(Skip = SkipReason)] + public async Task ItCanGetAndDeleteBatchAsync() + { + // Arrange + const string HotelId1 = "11111111-1111-1111-1111-111111111111"; + const string HotelId2 = "22222222-2222-2222-2222-222222222222"; + const string HotelId3 = "33333333-3333-3333-3333-333333333333"; + + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection(fixture.Database!, "get-and-delete-batch"); + + await sut.CreateCollectionAsync(); + + var record1 = this.CreateTestHotel(HotelId1); + var record2 = this.CreateTestHotel(HotelId2); + var record3 = this.CreateTestHotel(HotelId3); + + var upsertResults = await sut.UpsertBatchAsync([record1, record2, record3]).ToListAsync(); + var getResults = await sut.GetBatchAsync([HotelId1, HotelId2, HotelId3]).ToListAsync(); + + Assert.Equal([HotelId1, HotelId2, HotelId3], upsertResults); + + Assert.NotNull(getResults.First(l => l.HotelId == HotelId1)); + Assert.NotNull(getResults.First(l => l.HotelId == HotelId2)); + Assert.NotNull(getResults.First(l => l.HotelId == HotelId3)); + + // Act + await sut.DeleteBatchAsync([HotelId1, HotelId2, HotelId3]); + + getResults = await sut.GetBatchAsync([HotelId1, HotelId2, HotelId3]).ToListAsync(); + + // Assert + Assert.Empty(getResults); + } + + [Fact(Skip = SkipReason)] + public async Task ItCanUpsertRecordAsync() + { + // Arrange + const string HotelId = "55555555-5555-5555-5555-555555555555"; + var sut = new AzureCosmosDBNoSQLVectorStoreRecordCollection(fixture.Database!, "upsert-record"); + + await sut.CreateCollectionAsync(); + + var record = this.CreateTestHotel(HotelId); + + var upsertResult = await sut.UpsertAsync(record); + var getResult = await sut.GetAsync(HotelId); + + Assert.Equal(HotelId, upsertResult); + Assert.NotNull(getResult); + + // Act + record.HotelName = "Updated name"; + record.HotelRating = 10; + + upsertResult = await sut.UpsertAsync(record); + getResult = await sut.GetAsync(HotelId); + + // Assert + Assert.NotNull(getResult); + Assert.Equal("Updated name", getResult.HotelName); + Assert.Equal(10, getResult.HotelRating); + } + + #region private + + private AzureCosmosDBNoSQLHotel CreateTestHotel(string hotelId, string? hotelName = null) + { + return new AzureCosmosDBNoSQLHotel + { + HotelId = hotelId, + HotelName = hotelName ?? $"My Hotel {hotelId}", + HotelCode = 42, + HotelRating = 4.5f, + ParkingIncluded = true, + Tags = { "t1", "t2" }, + Description = "This is a great hotel.", + DescriptionEmbedding = new[] { 30f, 31f, 32f, 33f }, + }; + } + + private VectorStoreRecordDefinition GetTestHotelRecordDefinition() + { + return new() + { + Properties = + [ + new VectorStoreRecordKeyProperty("HotelId", typeof(string)), + new VectorStoreRecordDataProperty("HotelName", typeof(string)), + new VectorStoreRecordDataProperty("HotelCode", typeof(int)), + new VectorStoreRecordDataProperty("parking_is_included", typeof(bool)), + new VectorStoreRecordDataProperty("HotelRating", typeof(float)), + new VectorStoreRecordDataProperty("Tags", typeof(List)), + new VectorStoreRecordDataProperty("Description", typeof(string)), + new VectorStoreRecordVectorProperty("DescriptionEmbedding", typeof(ReadOnlyMemory?)) { Dimensions = 4, IndexKind = IndexKind.Flat, DistanceFunction = DistanceFunction.CosineDistance } + ] + }; + } + + #endregion +} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreTests.cs new file mode 100644 index 000000000000..938fe5c14caf --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/AzureCosmosDBNoSQLVectorStoreTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Azure.Cosmos; +using Microsoft.SemanticKernel.Connectors.AzureCosmosDBNoSQL; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.Memory.AzureCosmosDBNoSQL; + +/// +/// Integration tests for . +/// +[Collection("AzureCosmosDBNoSQLVectorStoreCollection")] +public sealed class AzureCosmosDBNoSQLVectorStoreTests(AzureCosmosDBNoSQLVectorStoreFixture fixture) +{ + private const string? SkipReason = "Azure CosmosDB NoSQL cluster is required"; + + [Fact(Skip = SkipReason)] + public async Task ItCanGetAListOfExistingCollectionNamesAsync() + { + // Arrange + var sut = new AzureCosmosDBNoSQLVectorStore(fixture.Database!); + + await fixture.Database!.CreateContainerIfNotExistsAsync(new ContainerProperties("list-names-1", "/id")); + await fixture.Database!.CreateContainerIfNotExistsAsync(new ContainerProperties("list-names-2", "/id")); + + // Act + var collectionNames = await sut.ListCollectionNamesAsync().ToListAsync(); + + // Assert + Assert.Contains("list-names-1", collectionNames); + Assert.Contains("list-names-2", collectionNames); + } +} diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/CosmosSystemTextJsonSerializer.cs b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/CosmosSystemTextJsonSerializer.cs new file mode 100644 index 000000000000..4fed0eab601d --- /dev/null +++ b/dotnet/src/IntegrationTests/Connectors/Memory/AzureCosmosDBNoSQL/CosmosSystemTextJsonSerializer.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Taken from https://github.com/Azure/azure-cosmos-dotnet-v3/pull/4332 +// TODO: Remove when https://github.com/Azure/azure-cosmos-dotnet-v3/pull/4589 will be released. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.Azure.Cosmos; + +/// +/// This class provides a default implementation of System.Text.Json Cosmos Linq Serializer. +/// +internal sealed class CosmosSystemTextJsonSerializer : CosmosLinqSerializer +{ + /// + /// A read-only instance of . + /// + private readonly JsonSerializerOptions _jsonSerializerOptions; + + /// + /// Creates an instance of + /// with the default values for the Cosmos SDK + /// + /// An instance of containing the json serialization options. + public CosmosSystemTextJsonSerializer( + JsonSerializerOptions jsonSerializerOptions) + { + this._jsonSerializerOptions = jsonSerializerOptions; + } + + /// + [return: MaybeNull] + public override T FromStream(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (stream.CanSeek && stream.Length == 0) + { + return default; + } + + if (typeof(Stream).IsAssignableFrom(typeof(T))) + { + return (T)(object)stream; + } + + using (stream) + { + return JsonSerializer.Deserialize(stream, this._jsonSerializerOptions); + } + } + + /// + public override Stream ToStream(T input) + { + MemoryStream streamPayload = new(); + JsonSerializer.Serialize( + utf8Json: streamPayload, + value: input, + options: this._jsonSerializerOptions); + + streamPayload.Position = 0; + return streamPayload; + } + + /// + /// Convert a MemberInfo to a string for use in LINQ query translation. + /// + /// Any MemberInfo used in the query. + /// A serialized representation of the member. + /// + /// Note that this is just a default implementation which handles the basic scenarios. Any passed in + /// here are not going to be reflected in SerializeMemberName(). For example, if customers passed in a JsonSerializerOption such as below + /// + /// + /// + /// This would not be honored by SerializeMemberName() unless it included special handling for this, for example. + /// + /// (true); + /// if (jsonExtensionDataAttribute != null) + /// { + /// return null; + /// } + /// JsonPropertyNameAttribute jsonPropertyNameAttribute = memberInfo.GetCustomAttribute(true); + /// if (!string.IsNullOrEmpty(jsonPropertyNameAttribute?.Name)) + /// { + /// return jsonPropertyNameAttribute.Name; + /// } + /// return System.Text.Json.JsonNamingPolicy.CamelCase.ConvertName(memberInfo.Name); + /// } + /// ]]> + /// + /// To handle such scenarios, please create a custom serializer which inherits from the and overrides the + /// SerializeMemberName to add any special handling. + /// + public override string? SerializeMemberName(MemberInfo memberInfo) + { + JsonExtensionDataAttribute? jsonExtensionDataAttribute = + memberInfo.GetCustomAttribute(true); + + if (jsonExtensionDataAttribute != null) + { + return null; + } + + JsonPropertyNameAttribute? jsonPropertyNameAttribute = memberInfo.GetCustomAttribute(true); + if (jsonPropertyNameAttribute is { } && !string.IsNullOrEmpty(jsonPropertyNameAttribute.Name)) + { + return jsonPropertyNameAttribute.Name; + } + + return memberInfo.Name; + } +} diff --git a/dotnet/src/IntegrationTests/Connectors/Weaviate/WeaviateMemoryStoreTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateMemoryStoreTests.cs similarity index 96% rename from dotnet/src/IntegrationTests/Connectors/Weaviate/WeaviateMemoryStoreTests.cs rename to dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateMemoryStoreTests.cs index b8cad556d3f7..b88795e9a3d6 100644 --- a/dotnet/src/IntegrationTests/Connectors/Weaviate/WeaviateMemoryStoreTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/WeaviateMemoryStoreTests.cs @@ -9,7 +9,7 @@ using Microsoft.SemanticKernel.Memory; using Xunit; -namespace SemanticKernel.IntegrationTests.Connectors.Weaviate; +namespace SemanticKernel.IntegrationTests.Connectors.Memory.Weaviate; /// /// Tests for collection and upsert operations. @@ -96,7 +96,7 @@ public async Task ItListsCollectionsAsync() await this._weaviateMemoryStore.CreateCollectionAsync(collectionName); Assert.True(await this._weaviateMemoryStore.DoesCollectionExistAsync(collectionName)); - Assert.Single((await this._weaviateMemoryStore.GetCollectionsAsync().ToListAsync())); + Assert.Single(await this._weaviateMemoryStore.GetCollectionsAsync().ToListAsync()); var collectionName2 = "SK" + Guid.NewGuid(); await this._weaviateMemoryStore.CreateCollectionAsync(collectionName2); @@ -110,17 +110,17 @@ public async Task ItDeletesCollectionAsync() { await this.DeleteAllClassesAsync(); - Assert.Empty((await this._weaviateMemoryStore.GetCollectionsAsync().ToListAsync())); + Assert.Empty(await this._weaviateMemoryStore.GetCollectionsAsync().ToListAsync()); var collectionName = "SK" + Guid.NewGuid(); await this._weaviateMemoryStore.CreateCollectionAsync(collectionName); Assert.True(await this._weaviateMemoryStore.DoesCollectionExistAsync(collectionName)); - Assert.Single((await this._weaviateMemoryStore.GetCollectionsAsync().ToListAsync())); + Assert.Single(await this._weaviateMemoryStore.GetCollectionsAsync().ToListAsync()); await this._weaviateMemoryStore.DeleteCollectionAsync(collectionName); Assert.False(await this._weaviateMemoryStore.DoesCollectionExistAsync(collectionName)); - Assert.Empty((await this._weaviateMemoryStore.GetCollectionsAsync().ToListAsync())); + Assert.Empty(await this._weaviateMemoryStore.GetCollectionsAsync().ToListAsync()); } [Fact(Skip = SkipReason)] diff --git a/dotnet/src/IntegrationTests/Connectors/Weaviate/docker-compose.yml b/dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/docker-compose.yml similarity index 100% rename from dotnet/src/IntegrationTests/Connectors/Weaviate/docker-compose.yml rename to dotnet/src/IntegrationTests/Connectors/Memory/Weaviate/docker-compose.yml diff --git a/dotnet/src/IntegrationTests/Plugins/OpenApi/repair-service.json b/dotnet/src/IntegrationTests/Plugins/OpenApi/repair-service.json index 1d5cc22bcbd3..ebf9f5e22c3f 100644 --- a/dotnet/src/IntegrationTests/Plugins/OpenApi/repair-service.json +++ b/dotnet/src/IntegrationTests/Plugins/OpenApi/repair-service.json @@ -6,9 +6,9 @@ "version": "1.0.0" }, "servers": [ - { - "url": "https://piercerepairsapi.azurewebsites.net/" - } + { + "url": "https://piercerepairsapi.azurewebsites.net" + } ], "paths": { "/repairs": { diff --git a/dotnet/src/IntegrationTests/README.md b/dotnet/src/IntegrationTests/README.md index bc2234acda64..1c646a824251 100644 --- a/dotnet/src/IntegrationTests/README.md +++ b/dotnet/src/IntegrationTests/README.md @@ -8,9 +8,8 @@ 3. **HuggingFace API key**: see https://huggingface.co/docs/huggingface_hub/guides/inference for details. 4. **Azure Bing Web Search API**: go to [Bing Web Search API](https://www.microsoft.com/en-us/bing/apis/bing-web-search-api) and select `Try Now` to get started. -5. **Oobabooga Text generation web UI**: Follow the [installation instructions](https://github.com/oobabooga/text-generation-webui#installation) to get a local Oobabooga instance running. Follow the [download instructions](https://github.com/oobabooga/text-generation-webui#downloading-models) to install a test model e.g. `python download-model.py gpt2`. Follow the [starting instructions](https://github.com/oobabooga/text-generation-webui#starting-the-web-ui) to start your local instance, enabling API, e.g. `python server.py --model gpt2 --listen --api --api-blocking-port "5000" --api-streaming-port "5005"`. Note that `--model` parameter is optional and models can be downloaded and hot swapped using exclusively the web UI, making it easy to test various models. -6. **Postgres**: start a postgres with the [pgvector](https://github.com/pgvector/pgvector) extension installed. You can easily do it using the docker image [ankane/pgvector](https://hub.docker.com/r/ankane/pgvector). -7. **Weaviate**: go to `IntegrationTests/Connectors/Weaviate` where `docker-compose.yml` is located and run `docker-compose up --build`. +5. **Postgres**: start a postgres with the [pgvector](https://github.com/pgvector/pgvector) extension installed. You can easily do it using the docker image [ankane/pgvector](https://hub.docker.com/r/ankane/pgvector). +6. **Weaviate**: go to `IntegrationTests/Connectors/Weaviate` where `docker-compose.yml` is located and run `docker-compose up --build`. ## Setup diff --git a/dotnet/src/IntegrationTests/testsettings.json b/dotnet/src/IntegrationTests/testsettings.json index 66df73f8b7a5..fd551e3b5d84 100644 --- a/dotnet/src/IntegrationTests/testsettings.json +++ b/dotnet/src/IntegrationTests/testsettings.json @@ -75,7 +75,10 @@ "ConnectionString": "", "VectorSearchCollection": "dotnetMSKNearestTest.nearestSearch" }, - "AzureCosmosDB": { + "AzureCosmosDBNoSQL": { + "ConnectionString": "" + }, + "AzureCosmosDBMongoDB": { "ConnectionString": "" }, "SqlServer": { diff --git a/dotnet/src/SemanticKernel.Abstractions/Data/RecordDefinition/IndexKind.cs b/dotnet/src/SemanticKernel.Abstractions/Data/RecordDefinition/IndexKind.cs index 364baaa8e727..fc6a7ced0b89 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Data/RecordDefinition/IndexKind.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Data/RecordDefinition/IndexKind.cs @@ -33,4 +33,21 @@ public static class IndexKind /// Better with smaller datasets. /// public const string Flat = nameof(Flat); + + /// + /// Inverted File with Flat Compression. Designed to enhance search efficiency by narrowing the search area through the use of neighbor partitions or clusters. + /// Also referred to as approximate nearest neighbor (ANN) search. + /// + public const string IvfFlat = nameof(IvfFlat); + + /// + /// Disk-based Approximate Nearest Neighbor algorithm designed for efficiently searching for approximate nearest neighbors (ANN) in high-dimensional spaces. + /// The primary focus of DiskANN is to handle large-scale datasets that cannot fit entirely into memory, leveraging disk storage to store the data while maintaining fast search times. + /// + public const string DiskAnn = nameof(DiskAnn); + + /// + /// Index that compresses vectors using DiskANN-based quantization methods for better efficiency in the kNN search. + /// + public const string QuantizedFlat = nameof(QuantizedFlat); } diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs index b838d7b30261..149dbf108ece 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/KernelFunction.cs @@ -37,7 +37,7 @@ public abstract class KernelFunction private static readonly Histogram s_invocationDuration = s_meter.CreateHistogram( name: "semantic_kernel.function.invocation.duration", unit: "s", - description: "Measures the duration of a function’s execution"); + description: "Measures the duration of a function's execution"); /// to record function streaming duration. /// @@ -47,7 +47,7 @@ public abstract class KernelFunction private static readonly Histogram s_streamingDuration = s_meter.CreateHistogram( name: "semantic_kernel.function.streaming.duration", unit: "s", - description: "Measures the duration of a function’s streaming execution"); + description: "Measures the duration of a function's streaming execution"); /// /// Gets the name of the function. diff --git a/python/.conf/packages_list.json b/python/.conf/packages_list.json deleted file mode 100644 index d4011687b48c..000000000000 --- a/python/.conf/packages_list.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "black": { - "repo": "https://github.com/psf/black", - "rev": "${rev}" - }, - "mypy": { - "repo": "https://github.com/pre-commit/mirrors-mypy", - "rev": "v${rev}" - }, - "ruff": { - "repo": "https://github.com/astral-sh/ruff-pre-commit", - "rev": "v${rev}" - } -} \ No newline at end of file diff --git a/python/.cspell.json b/python/.cspell.json index 79602e7ac6ac..804c4ebfa4c6 100644 --- a/python/.cspell.json +++ b/python/.cspell.json @@ -54,6 +54,7 @@ "qdrant", "huggingface", "pytestmark", - "contoso" + "contoso", + "opentelemetry" ] } \ No newline at end of file diff --git a/python/.env.example b/python/.env.example index d63e29eb17c3..4d5ac35c51f9 100644 --- a/python/.env.example +++ b/python/.env.example @@ -8,30 +8,11 @@ AZURE_OPENAI_TEXT_DEPLOYMENT_NAME="" AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME="" AZURE_OPENAI_ENDPOINT="" AZURE_OPENAI_API_KEY="" -AZURE_OPENAI_API_VERSION="2024-02-15-preview" -AZURE_OPENAI_TEMPERATURE=0 -AZURE_OPENAI_MAX_TOKENS=1000 -AZURE_OPENAI_TOP_P=1.0 -AZURE_OPENAI_STREAM=true +AZURE_OPENAI_API_VERSION="" AZURE_AISEARCH_URL="" AZURE_AISEARCH_SERVICE="" AZURE_AISEARCH_API_KEY="" AZURE_AISEARCH_INDEX_NAME="" -AZURE_AISEARCH_EMBEDDING_DEPLOYMENT_NAME="" -AZURE_AISEARCH_USE_SEMANTIC_SEARCH=false -AZURE_AISEARCH_SEMANTIC_SEARCH_CONFIG=default -AZURE_AISEARCH_INDEX_IS_PRECHUNKED=false -AZURE_AISEARCH_TOP_K=5 -AZURE_AISEARCH_ENABLE_IN_DOMAIN=true -AZURE_AISEARCH_CONTENT_COLUMNS=content -AZURE_AISEARCH_FILEPATH_COLUMN=filepath -AZURE_AISEARCH_TITLE_COLUMN=title -AZURE_AISEARCH_URL_COLUMN=url -AZURE_AISEARCH_VECTOR_COLUMNS=contentVector -AZURE_AISEARCH_QUERY_TYPE=simple -AZURE_AISEARCH_PERMITTED_GROUPS_COLUMN= -AZURE_AISEARCH_STRICTNESS=3 -AZURE_AISEARCH_FILTER="" MONGODB_ATLAS_CONNECTION_STRING="" PINECONE_API_KEY="" PINECONE_ENVIRONMENT="" @@ -40,11 +21,10 @@ WEAVIATE_URL="" WEAVIATE_API_KEY="" GOOGLE_SEARCH_ENGINE_ID="" REDIS_CONNECTION_STRING="" -AZCOSMOS_API = "" // should be mongo-vcore for now, as CosmosDB only supports vector search in mongo-vcore for now. -AZCOSMOS_CONNSTR = "" -AZCOSMOS_DATABASE_NAME = "" -AZCOSMOS_CONTAINER_NAME = "" -# Starts with AstraCS: +AZCOSMOS_API="" +AZCOSMOS_CONNSTR="" +AZCOSMOS_DATABASE_NAME="" +AZCOSMOS_CONTAINER_NAME="" ASTRADB_APP_TOKEN="" ASTRADB_ID="" ASTRADB_REGION="" diff --git a/.pre-commit-config.yaml b/python/.pre-commit-config.yaml similarity index 79% rename from .pre-commit-config.yaml rename to python/.pre-commit-config.yaml index 5fd6aa7d9377..e964cebe7613 100644 --- a/.pre-commit-config.yaml +++ b/python/.pre-commit-config.yaml @@ -1,11 +1,6 @@ files: ^python/ fail_fast: true repos: - - repo: https://github.com/floatingpurr/sync_with_poetry - rev: 1.1.0 - hooks: - - id: sync_with_poetry - args: [--config=.pre-commit-config.yaml, --db=python/.conf/packages_list.json, python/poetry.lock] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: @@ -32,21 +27,28 @@ repos: name: Check Valid Python Notebooks types: ["jupyter"] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.17.0 hooks: - id: pyupgrade args: [--py310-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.7 + rev: v0.6.1 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] + - id: ruff-format + - repo: https://github.com/astral-sh/uv-pre-commit + # uv version. + rev: 0.3.3 + hooks: + # Update the uv lockfile + - id: uv-lock - repo: local hooks: - id: mypy files: ^python/semantic_kernel/ name: mypy - entry: poetry -C python/ run python -m mypy -p semantic_kernel --config-file=python/mypy.ini + entry: cd python && uv run mypy -p semantic_kernel --config-file mypy.ini language: system types: [python] pass_filenames: false diff --git a/python/.vscode/tasks.json b/python/.vscode/tasks.json index fa9736a6fd5e..9a6a7ecbfd69 100644 --- a/python/.vscode/tasks.json +++ b/python/.vscode/tasks.json @@ -6,7 +6,7 @@ { "label": "Python: Run Checks", "type": "shell", - "command": "poetry", + "command": "uv", "args": [ "run", "pre-commit", @@ -34,7 +34,7 @@ { "label": "Python: Run Checks - Staged", "type": "shell", - "command": "poetry", + "command": "uv", "args": [ "run", "pre-commit", @@ -61,7 +61,7 @@ { "label": "Python: Run Mypy", "type": "shell", - "command": "poetry", + "command": "uv", "args": [ "run", "pre-commit", @@ -90,21 +90,17 @@ { "label": "Python: Install", "type": "shell", - "command": "poetry", - "args": [ - "install", - "--all-extras" - ], + "command": "make install PYTHON_VERSION=${input:py_version}", "presentation": { - "reveal": "silent", - "panel": "shared" + "reveal": "always", + "panel": "new" }, "problemMatcher": [] }, { "label": "Python: Tests - Unit", "type": "shell", - "command": "poetry", + "command": "uv", "args": [ "run", "pytest", @@ -120,7 +116,7 @@ { "label": "Python: Tests - Unit - Failed Only", "type": "shell", - "command": "poetry", + "command": "uv", "args": [ "run", "pytest", @@ -138,7 +134,7 @@ { "label": "Python: Tests - Code Coverage", "type": "shell", - "command": "poetry run pytest --cov=semantic_kernel --cov-report=term-missing:skip-covered tests/unit/", + "command": "uv run pytest --cov=semantic_kernel --cov-report=term-missing:skip-covered tests/unit/", "group": "test", "presentation": { "reveal": "always", @@ -149,7 +145,7 @@ { "label": "Python: Tests - All", "type": "shell", - "command": "poetry", + "command": "uv", "args": [ "run", "pytest", @@ -168,5 +164,18 @@ }, "problemMatcher": [] } + ], + "inputs": [ + { + "type": "pickString", + "options": [ + "3.10", + "3.11", + "3.12" + ], + "id": "py_version", + "description": "Python version", + "default": "3.10" + } ] } \ No newline at end of file diff --git a/python/DEV_SETUP.md b/python/DEV_SETUP.md index e9a938259cae..d81c64be6bc1 100644 --- a/python/DEV_SETUP.md +++ b/python/DEV_SETUP.md @@ -1,164 +1,126 @@ # Dev Setup -This document describes how to setup your environment with Python and Poetry, +This document describes how to setup your environment with Python and uv, if you're working on new features or a bug fix for Semantic Kernel, or simply want to run the tests included. -## LLM setup - -Make sure you have an -[OpenAI API Key](https://platform.openai.com) or -[Azure OpenAI service key](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart?pivots=rest-api) - -There are two methods to manage keys, secrets, and endpoints: - -1. Store them in environment variables. SK Python leverages pydantic settings to load keys, secrets, and endpoints. This means that there is a first attempt to load them from environment variables. The `.env` file naming applies to how the names should be stored as environment variables. - -2. If you'd like to use the `.env` file, you will need to configure the `.env` file with the following keys into a `.env` file (see the `.env.example` file): - -``` -OPENAI_API_KEY="" -OPENAI_ORG_ID="" -AZURE_OPENAI_CHAT_DEPLOYMENT_NAME="" -AZURE_OPENAI_TEXT_DEPLOYMENT_NAME="" -AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME="" -AZURE_OPENAI_ENDPOINT="" -AZURE_OPENAI_API_KEY="" -``` +## System setup -You will then configure the Text/ChatCompletion class with the keyword argument `env_file_path`: +## If you're on WSL -```python -chat_completion = OpenAIChatCompletion(service_id="test", env_file_path=) -``` +Check that you've cloned the repository to `~/workspace` or a similar folder. +Avoid `/mnt/c/` and prefer using your WSL user's home directory. -This optional `env_file_path` parameter will allow pydantic settings to use the `.env` file as a fallback to read the settings. +Ensure you have the WSL extension for VSCode installed. -If using the second method, we suggest adding a copy of the `.env` file under these folders: +## Using uv -- [./tests](./tests) -- [./samples/getting_started](./samples/getting_started). +uv allows us to use SK from the local files, without worrying about paths, as +if you had SK pip package installed. -## System setup +To install SK and all the required tools in your system, first, navigate to the directory containing +this DEV_SETUP using your chosen shell. -To get started, you'll need VSCode and a local installation of Python 3.8+. +### For windows (non-WSL) -You can run: +Check the [uv documentation](https://docs.astral.sh/uv/getting-started/installation/) for the installation instructions. At the time of writing this is the command to install uv: -```python - python3 --version ; pip3 --version ; code -v +```powershell +powershell -c "irm https://astral.sh/uv/install.ps1 | iex" ``` -to verify that you have the required dependencies. +You can then run the following commands manually: + +```powershell +# Install Python 3.10, 3.11, and 3.12 +uv python install 3.10 3.11 3.12 +# Create a virtual environment with Python 3.10 (you can change this to 3.11 or 3.12) +$PYTHON_VERSION = "3.10" +uv venv --python $PYTHON_VERSION +# Install SK and all dependencies +uv sync --all-extras --dev +# Install pre-commit hooks +uv run pre-commit install -c python/.pre-commit-config.yaml +``` -## If you're on WSL +Or you can then either install [`make`](https://gnuwin32.sourceforge.net/packages/make.htm) and then follow the guide for Mac and Linux, or run the following commands, the commands are shown as bash but should work in powershell as well. -Check that you've cloned the repository to `~/workspace` or a similar folder. -Avoid `/mnt/c/` and prefer using your WSL user's home directory. +### For Mac and Linux (both native and WSL) -Ensure you have the WSL extension for VSCode installed (and the Python extension -for VSCode installed). - -You'll also need `pip3` installed. If you don't yet have a `python3` install in WSL, -you can run: +It is super simple to get started, run the following commands: ```bash -sudo apt-get update && sudo apt-get install python3 python3-pip +make install ``` -ℹ️ **Note**: if you don't have your PATH setup to find executables installed by `pip3`, -you may need to run `~/.local/bin/poetry install` and `~/.local/bin/poetry shell` -instead. You can fix this by adding `export PATH="$HOME/.local/bin:$PATH"` to -your `~/.bashrc` and closing/re-opening the terminal.\_ - -## Using Poetry - -Poetry allows to use SK from the local files, without worrying about paths, as -if you had SK pip package installed. - -To install Poetry in your system, first, navigate to the directory containing -this README using your chosen shell. You will need to have Python 3.10, 3.11, or 3.12 -installed. +This will install uv, python, Semantic Kernel and all dependencies and the pre-commit config. It uses python 3.10 by default, if you want to change that set the `PYTHON_VERSION` environment variable to the desired version (currently supported are 3.10, 3.11, 3.12). For instance for 3.12" + +```bash +make install PYTHON_VERSION=3.12 +``` -Install the Poetry package manager and create a project virtual environment. -Note: SK requires at least Poetry 1.2.0. +If you want to change python version (without installing uv, python and pre-commit), you can use the same parameter, but do: -### Note for MacOS Users +```bash +make install-sk PYTHON_VERSION=3.12 +``` -It is best to install Poetry using their -[official installer](https://python-poetry.org/docs/#installing-with-the-official-installer). +ℹ️ **Note**: Running the install or install-sk command will wipe away your existing virtual environment and create a new one. -On MacOS, you might find that `python` commands are not recognized by default, -and you can only use `python3`. To make it easier to run `python ...` commands -(which Poetry requires), you can create an alias in your shell configuration file. +Alternatively you can run the VSCode task `Python: Install` to run the same command. -Follow these steps: +## VSCode Setup -1. **Open your shell configuration file**: - - For **Bash**: `nano ~/.bash_profile` or `nano ~/.bashrc` - - For **Zsh** (default on macOS Catalina and later): `nano ~/.zshrc` +Open the workspace in [VSCode](https://code.visualstudio.com/docs/editor/workspaces). +> The workspace for python should be rooted in the `./python` folder. -2. **Add the alias**: - ```sh - alias python='python3' - ``` +Open any of the `.py` files in the project and run the `Python: Select Interpreter` +command from the command palette. Make sure the virtual env (default path is `.venv`) created by +`uv` is selected. -3. **Save the file and exit**: - - In `nano`, press `CTRL + X`, then `Y`, and hit `Enter`. +If prompted, install `ruff`. (It should have been installed as part of `uv sync --dev`). -4. **Apply the changes**: - - For **Bash**: `source ~/.bash_profile` or `source ~/.bashrc` - - For **Zsh**: `source ~/.zshrc` +You also need to install the `ruff` extension in VSCode so that auto-formatting uses the `ruff` formatter on save. +Read more about the extension [here](https://github.com/astral-sh/ruff-vscode). -After these steps, you should be able to use `python` in your terminal to run -Python 3 commands. +## LLM setup -### Poetry Installation +Make sure you have an +[OpenAI API Key](https://platform.openai.com) or +[Azure OpenAI service key](https://learn.microsoft.com/azure/cognitive-services/openai/quickstart?pivots=rest-api) -```bash -# Install poetry package if not choosing to install via their official installer -pip3 install poetry +There are two methods to manage keys, secrets, and endpoints: -# optionally, define which python version you want to use -poetry env use python3.11 +1. Store them in environment variables. SK Python leverages pydantic settings to load keys, secrets, and endpoints from the environment. + > When you are using VSCode and have the python extension setup, it automatically loads environment variables from a `.env` file, so you don't have to manually set them in the terminal. + > During runtime on different platforms, environment settings set as part of the deployments should be used. -# Use poetry to install base project dependencies -poetry install +2. Store them in a separate `.env` file, like `dev.env`, you can then pass that name into the constructor for most services, to the `env_file_path` parameter, see below. + > Do not store `*.env` files in your repository, and make sure to add them to your `.gitignore` file. -# If you want to get all dependencies for tests installed, use -# poetry install --with tests -# example: poetry install --with hugging_face +There are a lot of settings, for a more extensive list of settings, see [ALL_SETTINGS.md](./samples/concepts/setup/ALL_SETTINGS.md). -# Use poetry to activate project venv -poetry shell +### Example for file-based setup with OpenAI Chat Completions +To configure a `.env` file with just the keys needed for OpenAI Chat Completions, you can create a `openai.env` (this name is just as an example, a single `.env` with all required keys is more common) file in the root of the `python` folder with the following content: -# Optionally, you can install the pre-commit hooks -poetry run pre-commit install -# this will run linters and mypy checks on all the changed code. +Content of `openai.env`: +```env +OPENAI_API_KEY="" +OPENAI_CHAT_MODEL_ID="gpt-4o-mini" ``` -## VSCode Setup - -Open the [workspace](https://code.visualstudio.com/docs/editor/workspaces) in VSCode. -> The Python workspace is the `./python` folder if you are at the root of the repository. +You will then configure the ChatCompletion class with the keyword argument `env_file_path`: -Open any of the `.py` files in the project and run the `Python: Select Interpreter` -command from the command palette. Make sure the virtual env (venv) created by -`poetry` is selected. -The python you're looking for should be under `~/.cache/pypoetry/virtualenvs/semantic-kernel-.../bin/python`. - -If prompted, install `ruff`. (It should have been installed as part of `poetry install`). - -You also need to install the `ruff` extension in VSCode so that auto-formatting uses the `ruff` formatter on save. -Read more about the extension here: https://github.com/astral-sh/ruff-vscode +```python +chat_completion = OpenAIChatCompletion(service_id="test", env_file_path="openai.env") +``` ## Tests You can run the unit tests under the [tests/unit](tests/unit/) folder. ```bash - poetry install --with unit-tests - poetry run pytest tests/unit + uv run pytest tests/unit ``` Alternatively, you can run them using VSCode Tasks. Open the command palette @@ -167,21 +129,18 @@ Alternatively, you can run them using VSCode Tasks. Open the command palette You can run the integration tests under the [tests/integration](tests/integration/) folder. ```bash - poetry install --with tests - poetry run pytest tests/integration + uv run pytest tests/integration ``` You can also run all the tests together under the [tests](tests/) folder. ```bash - poetry install - poetry run pytest tests + uv run pytest tests ``` Alternatively, you can run them using VSCode Tasks. Open the command palette (`Ctrl+Shift+P`) and type `Tasks: Run Task`. Select `Python: Tests - All` from the list. -## Tools and scripts ## Implementation Decisions @@ -203,9 +162,9 @@ They should contain: - If necessary to further explain the logic a newline follows the first line and then the explanation is given. - The following three sections are optional, and if used should be separated by a single empty line. - Arguments are then specified after a header called `Args:`, with each argument being specified in the following format: - - `arg_name` (`arg_type`): Explanation of the argument, arg_type is optional, as long as you are consistent. + - `arg_name`: Explanation of the argument. - if a longer explanation is needed for a argument, it should be placed on the next line, indented by 4 spaces. - - Default values do not have to be specified, they will be pulled from the definition. + - Type and default values do not have to be specified, they will be pulled from the definition. - Returns are specified after a header called `Returns:` or `Yields:`, with the return type and explanation of the return value. - Finally, a header for exceptions can be added, called `Raises:`, with each exception being specified in the following format: - `ExceptionType`: Explanation of the exception. @@ -227,12 +186,12 @@ def equal(arg1: str, arg2: str) -> bool: Here is extra explanation of the logic involved. Args: - arg1 (str): The first string to compare. - arg2 (str): The second string to compare. + arg1: The first string to compare. + arg2: The second string to compare. This string requires extra explanation. Returns: - bool: True if the strings are the same, False otherwise. + True if the strings are the same, False otherwise. Raises: ValueError: If one of the strings is empty. @@ -244,11 +203,8 @@ If in doubt, use the link above to read much more considerations of what to do a ## Pydantic and Serialization -[Pydantic Documentation](https://docs.pydantic.dev/1.10/) - -### Overview - This section describes how one can enable serialization for their class using Pydantic. +For more info you can refer to the [Pydantic Documentation](https://docs.pydantic.dev/latest/). ### Upgrading existing classes to use Pydantic @@ -263,7 +219,7 @@ class A: self.d = d ``` -You would convert this to a Pydantic class by subclassing from the `KernelBaseModel` class. +You would convert this to a Pydantic class by sub-classing from the `KernelBaseModel` class. ```python from pydantic import Field @@ -298,10 +254,13 @@ class A: You can use the `KernelBaseModel` to convert these to pydantic serializable classes. ```python -from typing import Generic +from typing import Generic, TypeVar from semantic_kernel.kernel_pydantic import KernelBaseModel +T1 = TypeVar("T1") +T2 = TypeVar("T2", bound=) + class A(KernelBaseModel, Generic[T1, T2]): # T1 and T2 must be specified in the Generic argument otherwise, pydantic will # NOT be able to serialize this class @@ -310,32 +269,31 @@ class A(KernelBaseModel, Generic[T1, T2]): c: T2 ``` -## Pipeline checks +## Code quality checks -To run the same checks that run during the GitHub Action build, you can use -this command, from the [python](../python) folder: +To run the same checks that run during a commit and the GitHub Action `Python Code Quality Checks`, you can use this command, from the [python](../python) folder: ```bash - poetry run pre-commit run -a + uv run pre-commit run -a ``` or use the following task (using `Ctrl+Shift+P`): - `Python - Run Checks` to run the checks on the whole project. - `Python - Run Checks - Staged` to run the checks on the currently staged files only. -Ideally you should run these checks before committing any changes, use `poetry run pre-commit install` to set that up. +Ideally you should run these checks before committing any changes, when you install using the instructions above the pre-commit hooks should be installed already. ## Code Coverage We try to maintain a high code coverage for the project. To run the code coverage on the unit tests, you can use the following command: ```bash - poetry run pytest --cov=semantic_kernel --cov-report=term-missing:skip-covered tests/unit/ + uv run pytest --cov=semantic_kernel --cov-report=term-missing:skip-covered tests/unit/ ``` or use the following task (using `Ctrl+Shift+P`): - `Python: Tests - Code Coverage` to run the code coverage on the whole project. -This will show you which files are not covered by the tests, including the specific lines not covered. +This will show you which files are not covered by the tests, including the specific lines not covered. Make sure to consider the untested lines from the code you are working on, but feel free to add other tests as well, that is always welcome! ## Catching up with the latest changes There are many people committing to Semantic Kernel, so it is important to keep your local repository up to date. To do this, you can run the following commands: diff --git a/python/Makefile b/python/Makefile index 8fdeec500379..a4eb077db5d7 100644 --- a/python/Makefile +++ b/python/Makefile @@ -1,54 +1,70 @@ -SHELL = bash +SHELL = /bin/bash -.PHONY: help install recreate-env pre-commit +.PHONY: help install clean +.SILENT: +all: install -help: - @echo -e "\033[1mUSAGE:\033[0m" - @echo " make [target]" - @echo "" - @echo -e "\033[1mTARGETS:\033[0m" - @echo " install - install Poetry and project dependencies" - @echo " install-pre-commit - install and configure pre-commit hooks" - @echo " pre-commit - run pre-commit hooks on all files" - @echo " recreate-env - destroy and recreate Poetry's virtualenv" +ifeq ($(PYTHON_VERSION),) + PYTHON_VERSION="3.10" +endif .ONESHELL: +help: + echo -e "\033[1mUSAGE:\033[0m" + echo " make [target]" + echo "" + echo -e "\033[1mTARGETS:\033[0m" + echo " help - show this help message" + echo " install - install uv, python, Semantic Kernel and all dependencies" + echo " This is the default and will use Python 3.10." + echo " install-uv - install uv" + echo " install-python - install python distributions" + echo " install-sk - install Semantic Kernel and all dependencies" + echo " install-pre-commit - install pre-commit hooks" + echo " clean - remove the virtualenvs" + echo "" + echo -e "\033[1mVARIABLES:\033[0m" + echo " PYTHON_VERSION - Python version to use. Default is 3.10" + echo " By default, 3.10, 3.11 and 3.12 are installed as well." + install: - @# Check to make sure Python is installed - @if ! command -v python3 &> /dev/null - then - echo "Python could not be found" - echo "Please install Python" - exit 1 - fi - - @# Check if Poetry is installed - @if ! command -v poetry &> /dev/null - then - echo "Poetry could not be found" - echo "Installing Poetry" - curl -sSL https://install.python-poetry.org | python3 - - fi - - # Install the dependencies - poetry install + make install-uv + make install-python + make install-sk + make install-pre-commit + +UV_VERSION = $(shell uv --version 2> /dev/null) +install-uv: +# Check if uv is installed +ifdef UV_VERSION + echo "uv found $(UV_VERSION)" + echo "running uv update" + uv self update +else + echo "uv could not be found" + echo "Installing uv" + curl -LsSf https://astral.sh/uv/install.sh | sh +endif .ONESHELL: -recreate-env: - # Stop the current virtualenv if active or alternative use - # `exit` to exit from a Poetry shell session - (deactivate || exit 0) +install-python: + echo "Installing python 3.10, 3.11, 3.12" + uv python install 3.10 3.11 3.12 - # Remove all the files of the current environment of the folder we are in - export POETRY_LOCATION=$$(poetry env info -p) - echo "Poetry is $${POETRY_LOCATION}" - rm -rf "$${POETRY_LOCATION}" +.ONESHELL: +install-pre-commit: + echo "Installing pre-commit hooks" + uv run pre-commit install -c python/.pre-commit-config.yaml -pre-commit: - poetry run pre-commit run --all-files -c .conf/.pre-commit-config.yaml .ONESHELL: -install-pre-commit: - poetry run pre-commit install - # Edit the pre-commit config file to change the config path - sed -i 's|\.pre-commit-config\.yaml|\.conf/\.pre-commit-config\.yaml|g' .git/hooks/pre-commit +install-sk: + echo "Creating and activating venv for python $(PYTHON_VERSION)" + uv venv --python $(PYTHON_VERSION) + echo "Installing Semantic Kernel and all dependencies" + uv sync --all-extras --dev + +.ONESHELL: +clean: + # Remove the virtualenv + rm -rf .venv \ No newline at end of file diff --git a/python/README.md b/python/README.md index db821e29dde8..0624f34a032f 100644 --- a/python/README.md +++ b/python/README.md @@ -1,17 +1,18 @@ # Get Started with Semantic Kernel ⚡ Install the latest package: - - python -m pip install --upgrade semantic-kernel - +```bash +python -m pip install --upgrade semantic-kernel +``` If you want to use some of the optional dependencies (OpenAI is installed by default), you can install them with: - - python -m pip install --upgrade semantic-kernel[hugging_face] +```bash +python -m pip install --upgrade semantic-kernel[hugging_face] +``` or all of them: - - python -m pip install --upgrade semantic-kernel[all] - +```bash +python -m pip install --upgrade semantic-kernel[all] +``` # AI Services ## OpenAI / Azure OpenAI API keys @@ -26,7 +27,7 @@ There are two methods to manage keys, secrets, and endpoints: 2. If you'd like to use the `.env` file, you will need to configure the `.env` file with the following keys in the file (see the `.env.example` file): -``` +```bash OPENAI_API_KEY="" OPENAI_ORG_ID="" AZURE_OPENAI_CHAT_DEPLOYMENT_NAME="" diff --git a/python/log.txt b/python/log.txt deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/python/mypy.ini b/python/mypy.ini index 9f392f90a3ab..fae1fd597ab6 100644 --- a/python/mypy.ini +++ b/python/mypy.ini @@ -1,6 +1,5 @@ [mypy] -python_version = 3.11 plugins = pydantic.mypy ignore_missing_imports = true diff --git a/python/poetry.lock b/python/poetry.lock deleted file mode 100644 index 3781e85f22e4..000000000000 --- a/python/poetry.lock +++ /dev/null @@ -1,7715 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. - -[[package]] -name = "accelerate" -version = "0.33.0" -description = "Accelerate" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "accelerate-0.33.0-py3-none-any.whl", hash = "sha256:0a7f33d60ba09afabd028d4f0856dd19c5a734b7a596d637d9dd6e3d0eadbaf3"}, - {file = "accelerate-0.33.0.tar.gz", hash = "sha256:11ba481ed6ea09191775df55ce464aeeba67a024bd0261a44b77b30fb439e26a"}, -] - -[package.dependencies] -huggingface-hub = ">=0.21.0" -numpy = ">=1.17,<2.0.0" -packaging = ">=20.0" -psutil = "*" -pyyaml = "*" -safetensors = ">=0.3.1" -torch = ">=1.10.0" - -[package.extras] -deepspeed = ["deepspeed (<=0.14.0)"] -dev = ["bitsandbytes", "black (>=23.1,<24.0)", "datasets", "diffusers", "evaluate", "hf-doc-builder (>=0.3.0)", "parameterized", "pytest (>=7.2.0,<=8.0.0)", "pytest-subtests", "pytest-xdist", "rich", "ruff (>=0.2.1,<0.3.0)", "scikit-learn", "scipy", "timm", "torchpippy (>=0.2.0)", "tqdm", "transformers"] -quality = ["black (>=23.1,<24.0)", "hf-doc-builder (>=0.3.0)", "ruff (>=0.2.1,<0.3.0)"] -rich = ["rich"] -sagemaker = ["sagemaker"] -test-dev = ["bitsandbytes", "datasets", "diffusers", "evaluate", "scikit-learn", "scipy", "timm", "torchpippy (>=0.2.0)", "tqdm", "transformers"] -test-prod = ["parameterized", "pytest (>=7.2.0,<=8.0.0)", "pytest-subtests", "pytest-xdist"] -test-trackers = ["comet-ml", "dvclive", "tensorboard", "wandb"] -testing = ["bitsandbytes", "datasets", "diffusers", "evaluate", "parameterized", "pytest (>=7.2.0,<=8.0.0)", "pytest-subtests", "pytest-xdist", "scikit-learn", "scipy", "timm", "torchpippy (>=0.2.0)", "tqdm", "transformers"] - -[[package]] -name = "aiohappyeyeballs" -version = "2.3.5" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohappyeyeballs-2.3.5-py3-none-any.whl", hash = "sha256:4d6dea59215537dbc746e93e779caea8178c866856a721c9c660d7a5a7b8be03"}, - {file = "aiohappyeyeballs-2.3.5.tar.gz", hash = "sha256:6fa48b9f1317254f122a07a131a86b71ca6946ca989ce6326fff54a99a920105"}, -] - -[[package]] -name = "aiohttp" -version = "3.10.5" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3"}, - {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6"}, - {file = "aiohttp-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683"}, - {file = "aiohttp-3.10.5-cp310-cp310-win32.whl", hash = "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef"}, - {file = "aiohttp-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088"}, - {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2"}, - {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf"}, - {file = "aiohttp-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058"}, - {file = "aiohttp-3.10.5-cp311-cp311-win32.whl", hash = "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072"}, - {file = "aiohttp-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff"}, - {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487"}, - {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a"}, - {file = "aiohttp-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6"}, - {file = "aiohttp-3.10.5-cp312-cp312-win32.whl", hash = "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12"}, - {file = "aiohttp-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc"}, - {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092"}, - {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77"}, - {file = "aiohttp-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987"}, - {file = "aiohttp-3.10.5-cp313-cp313-win32.whl", hash = "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04"}, - {file = "aiohttp-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022"}, - {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f6f18898ace4bcd2d41a122916475344a87f1dfdec626ecde9ee802a711bc569"}, - {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5ede29d91a40ba22ac1b922ef510aab871652f6c88ef60b9dcdf773c6d32ad7a"}, - {file = "aiohttp-3.10.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:673f988370f5954df96cc31fd99c7312a3af0a97f09e407399f61583f30da9bc"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58718e181c56a3c02d25b09d4115eb02aafe1a732ce5714ab70326d9776457c3"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b38b1570242fbab8d86a84128fb5b5234a2f70c2e32f3070143a6d94bc854cf"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:074d1bff0163e107e97bd48cad9f928fa5a3eb4b9d33366137ffce08a63e37fe"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd31f176429cecbc1ba499d4aba31aaccfea488f418d60376b911269d3b883c5"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7384d0b87d4635ec38db9263e6a3f1eb609e2e06087f0aa7f63b76833737b471"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8989f46f3d7ef79585e98fa991e6ded55d2f48ae56d2c9fa5e491a6e4effb589"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:c83f7a107abb89a227d6c454c613e7606c12a42b9a4ca9c5d7dad25d47c776ae"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cde98f323d6bf161041e7627a5fd763f9fd829bcfcd089804a5fdce7bb6e1b7d"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:676f94c5480d8eefd97c0c7e3953315e4d8c2b71f3b49539beb2aa676c58272f"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2d21ac12dc943c68135ff858c3a989f2194a709e6e10b4c8977d7fcd67dfd511"}, - {file = "aiohttp-3.10.5-cp38-cp38-win32.whl", hash = "sha256:17e997105bd1a260850272bfb50e2a328e029c941c2708170d9d978d5a30ad9a"}, - {file = "aiohttp-3.10.5-cp38-cp38-win_amd64.whl", hash = "sha256:1c19de68896747a2aa6257ae4cf6ef59d73917a36a35ee9d0a6f48cff0f94db8"}, - {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7e2fe37ac654032db1f3499fe56e77190282534810e2a8e833141a021faaab0e"}, - {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5bf3ead3cb66ab990ee2561373b009db5bc0e857549b6c9ba84b20bc462e172"}, - {file = "aiohttp-3.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b2c16a919d936ca87a3c5f0e43af12a89a3ce7ccbce59a2d6784caba945b68b"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad146dae5977c4dd435eb31373b3fe9b0b1bf26858c6fc452bf6af394067e10b"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c5c6fa16412b35999320f5c9690c0f554392dc222c04e559217e0f9ae244b92"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95c4dc6f61d610bc0ee1edc6f29d993f10febfe5b76bb470b486d90bbece6b22"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da452c2c322e9ce0cfef392e469a26d63d42860f829026a63374fde6b5c5876f"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:898715cf566ec2869d5cb4d5fb4be408964704c46c96b4be267442d265390f32"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:391cc3a9c1527e424c6865e087897e766a917f15dddb360174a70467572ac6ce"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:380f926b51b92d02a34119d072f178d80bbda334d1a7e10fa22d467a66e494db"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce91db90dbf37bb6fa0997f26574107e1b9d5ff939315247b7e615baa8ec313b"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9093a81e18c45227eebe4c16124ebf3e0d893830c6aca7cc310bfca8fe59d857"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ee40b40aa753d844162dcc80d0fe256b87cba48ca0054f64e68000453caead11"}, - {file = "aiohttp-3.10.5-cp39-cp39-win32.whl", hash = "sha256:03f2645adbe17f274444953bdea69f8327e9d278d961d85657cb0d06864814c1"}, - {file = "aiohttp-3.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:d17920f18e6ee090bdd3d0bfffd769d9f2cb4c8ffde3eb203777a3895c128862"}, - {file = "aiohttp-3.10.5.tar.gz", hash = "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.3.0" -aiosignal = ">=1.1.2" -async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] - -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anthropic" -version = "0.32.0" -description = "The official Python library for the anthropic API" -optional = false -python-versions = ">=3.7" -files = [ - {file = "anthropic-0.32.0-py3-none-any.whl", hash = "sha256:302c7c652b05a26c418f70697b585d7b47daac36210d097a0daa45ecda89f258"}, - {file = "anthropic-0.32.0.tar.gz", hash = "sha256:1027bddeb7c3cbcb5e16d5e3b4d4a8d17b6258ca2fb4298bf91cc69adb148452"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -jiter = ">=0.4.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -tokenizers = ">=0.13.0" -typing-extensions = ">=4.7,<5" - -[package.extras] -bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] -vertex = ["google-auth (>=2,<3)"] - -[[package]] -name = "anyio" -version = "4.4.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] - -[[package]] -name = "appnope" -version = "0.1.4" -description = "Disable App Nap on macOS >= 10.9" -optional = false -python-versions = ">=3.6" -files = [ - {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, - {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, -] - -[[package]] -name = "asgiref" -version = "3.8.1" -description = "ASGI specs, helper code, and adapters" -optional = false -python-versions = ">=3.8" -files = [ - {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, - {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} - -[package.extras] -tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] - -[[package]] -name = "asttokens" -version = "2.4.1" -description = "Annotate AST trees with source code positions" -optional = false -python-versions = "*" -files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, -] - -[package.dependencies] -six = ">=1.12.0" - -[package.extras] -astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] -test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] - -[[package]] -name = "async-timeout" -version = "4.0.3" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.7" -files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, -] - -[[package]] -name = "attrs" -version = "24.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, -] - -[package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] - -[[package]] -name = "authlib" -version = "1.3.1" -description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." -optional = false -python-versions = ">=3.8" -files = [ - {file = "Authlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377"}, - {file = "authlib-1.3.1.tar.gz", hash = "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917"}, -] - -[package.dependencies] -cryptography = "*" - -[[package]] -name = "azure-ai-inference" -version = "1.0.0b3" -description = "Microsoft Azure Ai Inference Client Library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "azure-ai-inference-1.0.0b3.tar.gz", hash = "sha256:1e99dc74c3b335a457500311bbbadb348f54dc4c12252a93cb8ab78d6d217ff0"}, - {file = "azure_ai_inference-1.0.0b3-py3-none-any.whl", hash = "sha256:6734ca7334c809a170beb767f1f1455724ab3f006cb60045e42a833c0e764403"}, -] - -[package.dependencies] -azure-core = ">=1.30.0" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-common" -version = "1.1.28" -description = "Microsoft Azure Client Library for Python (Common)" -optional = false -python-versions = "*" -files = [ - {file = "azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3"}, - {file = "azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad"}, -] - -[[package]] -name = "azure-core" -version = "1.30.2" -description = "Microsoft Azure Core Library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "azure-core-1.30.2.tar.gz", hash = "sha256:a14dc210efcd608821aa472d9fb8e8d035d29b68993819147bc290a8ac224472"}, - {file = "azure_core-1.30.2-py3-none-any.whl", hash = "sha256:cf019c1ca832e96274ae85abd3d9f752397194d9fea3b41487290562ac8abe4a"}, -] - -[package.dependencies] -requests = ">=2.21.0" -six = ">=1.11.0" -typing-extensions = ">=4.6.0" - -[package.extras] -aio = ["aiohttp (>=3.0)"] - -[[package]] -name = "azure-cosmos" -version = "4.7.0" -description = "Microsoft Azure Cosmos Client Library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "azure-cosmos-4.7.0.tar.gz", hash = "sha256:72d714033134656302a2e8957c4b93590673bd288b0ca60cb123e348ae99a241"}, - {file = "azure_cosmos-4.7.0-py3-none-any.whl", hash = "sha256:03d8c7740ddc2906fb16e07b136acc0fe6a6a02656db46c5dd6f1b127b58cc96"}, -] - -[package.dependencies] -azure-core = ">=1.25.1" -typing-extensions = ">=4.6.0" - -[[package]] -name = "azure-identity" -version = "1.17.1" -description = "Microsoft Azure Identity Library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "azure-identity-1.17.1.tar.gz", hash = "sha256:32ecc67cc73f4bd0595e4f64b1ca65cd05186f4fe6f98ed2ae9f1aa32646efea"}, - {file = "azure_identity-1.17.1-py3-none-any.whl", hash = "sha256:db8d59c183b680e763722bfe8ebc45930e6c57df510620985939f7f3191e0382"}, -] - -[package.dependencies] -azure-core = ">=1.23.0" -cryptography = ">=2.5" -msal = ">=1.24.0" -msal-extensions = ">=0.3.0" -typing-extensions = ">=4.0.0" - -[[package]] -name = "azure-search-documents" -version = "11.6.0b4" -description = "Microsoft Azure Cognitive Search Client Library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "azure-search-documents-11.6.0b4.tar.gz", hash = "sha256:b09fc3fa2813e83e7177874b352c84462fb86934d9f4299775361e1dfccc3f8f"}, - {file = "azure_search_documents-11.6.0b4-py3-none-any.whl", hash = "sha256:9590392464f882762ce6bad03613c822d4423f09f311c275b833de25398c00c1"}, -] - -[package.dependencies] -azure-common = ">=1.1" -azure-core = ">=1.28.0" -isodate = ">=0.6.0" - -[[package]] -name = "backoff" -version = "2.2.1" -description = "Function decoration for backoff and retry" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, - {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, -] - -[[package]] -name = "bcrypt" -version = "4.2.0" -description = "Modern password hashing for your software and your servers" -optional = false -python-versions = ">=3.7" -files = [ - {file = "bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb"}, - {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00"}, - {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d"}, - {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291"}, - {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328"}, - {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7"}, - {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399"}, - {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060"}, - {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7"}, - {file = "bcrypt-4.2.0-cp37-abi3-win32.whl", hash = "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458"}, - {file = "bcrypt-4.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5"}, - {file = "bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841"}, - {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68"}, - {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe"}, - {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2"}, - {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c"}, - {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae"}, - {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d"}, - {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e"}, - {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8"}, - {file = "bcrypt-4.2.0-cp39-abi3-win32.whl", hash = "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34"}, - {file = "bcrypt-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9"}, - {file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a"}, - {file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db"}, - {file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170"}, - {file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184"}, - {file = "bcrypt-4.2.0.tar.gz", hash = "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221"}, -] - -[package.extras] -tests = ["pytest (>=3.2.1,!=3.3.0)"] -typecheck = ["mypy"] - -[[package]] -name = "beautifulsoup4" -version = "4.12.3" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "bleach" -version = "6.1.0" -description = "An easy safelist-based HTML-sanitizing tool." -optional = false -python-versions = ">=3.8" -files = [ - {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, - {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, -] - -[package.dependencies] -six = ">=1.9.0" -webencodings = "*" - -[package.extras] -css = ["tinycss2 (>=1.1.0,<1.3)"] - -[[package]] -name = "build" -version = "1.2.1" -description = "A simple, correct Python build frontend" -optional = false -python-versions = ">=3.8" -files = [ - {file = "build-1.2.1-py3-none-any.whl", hash = "sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4"}, - {file = "build-1.2.1.tar.gz", hash = "sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "os_name == \"nt\""} -importlib-metadata = {version = ">=4.6", markers = "python_full_version < \"3.10.2\""} -packaging = ">=19.1" -pyproject_hooks = "*" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] -test = ["build[uv,virtualenv]", "filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"] -typing = ["build[uv]", "importlib-metadata (>=5.1)", "mypy (>=1.9.0,<1.10.0)", "tomli", "typing-extensions (>=3.7.4.3)"] -uv = ["uv (>=0.1.18)"] -virtualenv = ["virtualenv (>=20.0.35)"] - -[[package]] -name = "cachetools" -version = "5.4.0" -description = "Extensible memoizing collections and decorators" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, - {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, -] - -[[package]] -name = "certifi" -version = "2024.7.4" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, -] - -[[package]] -name = "cffi" -version = "1.17.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, - {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, - {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, - {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, - {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, - {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, - {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, - {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, - {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, - {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, - {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, - {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, - {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, - {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, - {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, - {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "chardet" -version = "5.2.0" -description = "Universal encoding detector for Python 3" -optional = false -python-versions = ">=3.7" -files = [ - {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, - {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - -[[package]] -name = "cheap-repr" -version = "0.5.2" -description = "Better version of repr/reprlib for short, cheap string representations." -optional = false -python-versions = "*" -files = [ - {file = "cheap_repr-0.5.2-py2.py3-none-any.whl", hash = "sha256:537ec1991bfee885c13c6d473afd110a408e039cde26882e95bf92761556ab6e"}, - {file = "cheap_repr-0.5.2.tar.gz", hash = "sha256:001a5cf8adb0305c7ad3152c5f776040ac2a559d97f85770cebcb28c6ca5a30f"}, -] - -[package.extras] -tests = ["Django", "numpy (>=1.16.3)", "pandas (>=0.24.2)", "pytest"] - -[[package]] -name = "chroma-hnswlib" -version = "0.7.6" -description = "Chromas fork of hnswlib" -optional = false -python-versions = "*" -files = [ - {file = "chroma_hnswlib-0.7.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f35192fbbeadc8c0633f0a69c3d3e9f1a4eab3a46b65458bbcbcabdd9e895c36"}, - {file = "chroma_hnswlib-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f007b608c96362b8f0c8b6b2ac94f67f83fcbabd857c378ae82007ec92f4d82"}, - {file = "chroma_hnswlib-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:456fd88fa0d14e6b385358515aef69fc89b3c2191706fd9aee62087b62aad09c"}, - {file = "chroma_hnswlib-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dfaae825499c2beaa3b75a12d7ec713b64226df72a5c4097203e3ed532680da"}, - {file = "chroma_hnswlib-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:2487201982241fb1581be26524145092c95902cb09fc2646ccfbc407de3328ec"}, - {file = "chroma_hnswlib-0.7.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81181d54a2b1e4727369486a631f977ffc53c5533d26e3d366dda243fb0998ca"}, - {file = "chroma_hnswlib-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4b4ab4e11f1083dd0a11ee4f0e0b183ca9f0f2ed63ededba1935b13ce2b3606f"}, - {file = "chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53db45cd9173d95b4b0bdccb4dbff4c54a42b51420599c32267f3abbeb795170"}, - {file = "chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c093f07a010b499c00a15bc9376036ee4800d335360570b14f7fe92badcdcf9"}, - {file = "chroma_hnswlib-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:0540b0ac96e47d0aa39e88ea4714358ae05d64bbe6bf33c52f316c664190a6a3"}, - {file = "chroma_hnswlib-0.7.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e87e9b616c281bfbe748d01705817c71211613c3b063021f7ed5e47173556cb7"}, - {file = "chroma_hnswlib-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec5ca25bc7b66d2ecbf14502b5729cde25f70945d22f2aaf523c2d747ea68912"}, - {file = "chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305ae491de9d5f3c51e8bd52d84fdf2545a4a2bc7af49765cda286b7bb30b1d4"}, - {file = "chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:822ede968d25a2c88823ca078a58f92c9b5c4142e38c7c8b4c48178894a0a3c5"}, - {file = "chroma_hnswlib-0.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2fe6ea949047beed19a94b33f41fe882a691e58b70c55fdaa90274ae78be046f"}, - {file = "chroma_hnswlib-0.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feceff971e2a2728c9ddd862a9dd6eb9f638377ad98438876c9aeac96c9482f5"}, - {file = "chroma_hnswlib-0.7.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb0633b60e00a2b92314d0bf5bbc0da3d3320be72c7e3f4a9b19f4609dc2b2ab"}, - {file = "chroma_hnswlib-0.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:a566abe32fab42291f766d667bdbfa234a7f457dcbd2ba19948b7a978c8ca624"}, - {file = "chroma_hnswlib-0.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6be47853d9a58dedcfa90fc846af202b071f028bbafe1d8711bf64fe5a7f6111"}, - {file = "chroma_hnswlib-0.7.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a7af35bdd39a88bffa49f9bb4bf4f9040b684514a024435a1ef5cdff980579d"}, - {file = "chroma_hnswlib-0.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a53b1f1551f2b5ad94eb610207bde1bb476245fc5097a2bec2b476c653c58bde"}, - {file = "chroma_hnswlib-0.7.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3085402958dbdc9ff5626ae58d696948e715aef88c86d1e3f9285a88f1afd3bc"}, - {file = "chroma_hnswlib-0.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:77326f658a15adfb806a16543f7db7c45f06fd787d699e643642d6bde8ed49c4"}, - {file = "chroma_hnswlib-0.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93b056ab4e25adab861dfef21e1d2a2756b18be5bc9c292aa252fa12bb44e6ae"}, - {file = "chroma_hnswlib-0.7.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fe91f018b30452c16c811fd6c8ede01f84e5a9f3c23e0758775e57f1c3778871"}, - {file = "chroma_hnswlib-0.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6c0e627476f0f4d9e153420d36042dd9c6c3671cfd1fe511c0253e38c2a1039"}, - {file = "chroma_hnswlib-0.7.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e9796a4536b7de6c6d76a792ba03e08f5aaa53e97e052709568e50b4d20c04f"}, - {file = "chroma_hnswlib-0.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:d30e2db08e7ffdcc415bd072883a322de5995eb6ec28a8f8c054103bbd3ec1e0"}, - {file = "chroma_hnswlib-0.7.6.tar.gz", hash = "sha256:4dce282543039681160259d29fcde6151cc9106c6461e0485f57cdccd83059b7"}, -] - -[package.dependencies] -numpy = "*" - -[[package]] -name = "chromadb" -version = "0.5.5" -description = "Chroma." -optional = false -python-versions = ">=3.8" -files = [ - {file = "chromadb-0.5.5-py3-none-any.whl", hash = "sha256:2a5a4b84cb0fc32b380e193be68cdbadf3d9f77dbbf141649be9886e42910ddd"}, - {file = "chromadb-0.5.5.tar.gz", hash = "sha256:84f4bfee320fb4912cbeb4d738f01690891e9894f0ba81f39ee02867102a1c4d"}, -] - -[package.dependencies] -bcrypt = ">=4.0.1" -build = ">=1.0.3" -chroma-hnswlib = "0.7.6" -fastapi = ">=0.95.2" -grpcio = ">=1.58.0" -httpx = ">=0.27.0" -importlib-resources = "*" -kubernetes = ">=28.1.0" -mmh3 = ">=4.0.1" -numpy = ">=1.22.5,<2.0.0" -onnxruntime = ">=1.14.1" -opentelemetry-api = ">=1.2.0" -opentelemetry-exporter-otlp-proto-grpc = ">=1.2.0" -opentelemetry-instrumentation-fastapi = ">=0.41b0" -opentelemetry-sdk = ">=1.2.0" -orjson = ">=3.9.12" -overrides = ">=7.3.1" -posthog = ">=2.4.0" -pydantic = ">=1.9" -pypika = ">=0.48.9" -PyYAML = ">=6.0.0" -tenacity = ">=8.2.3" -tokenizers = ">=0.13.2" -tqdm = ">=4.65.0" -typer = ">=0.9.0" -typing-extensions = ">=4.5.0" -uvicorn = {version = ">=0.18.3", extras = ["standard"]} - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coloredlogs" -version = "15.0.1" -description = "Colored terminal output for Python's logging module" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, - {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, -] - -[package.dependencies] -humanfriendly = ">=9.1" - -[package.extras] -cron = ["capturer (>=2.4)"] - -[[package]] -name = "comm" -version = "0.2.2" -description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." -optional = false -python-versions = ">=3.8" -files = [ - {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, - {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, -] - -[package.dependencies] -traitlets = ">=4" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "coverage" -version = "7.6.1" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "cryptography" -version = "43.0.0" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = ">=3.7" -files = [ - {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, - {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, - {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, - {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, - {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, - {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, - {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, - {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, - {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, - {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, - {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, -] - -[package.dependencies] -cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "debugpy" -version = "1.8.5" -description = "An implementation of the Debug Adapter Protocol for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7"}, - {file = "debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a"}, - {file = "debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed"}, - {file = "debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e"}, - {file = "debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a"}, - {file = "debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b"}, - {file = "debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408"}, - {file = "debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3"}, - {file = "debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156"}, - {file = "debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb"}, - {file = "debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7"}, - {file = "debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c"}, - {file = "debugpy-1.8.5-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:3df6692351172a42af7558daa5019651f898fc67450bf091335aa8a18fbf6f3a"}, - {file = "debugpy-1.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd04a73eb2769eb0bfe43f5bfde1215c5923d6924b9b90f94d15f207a402226"}, - {file = "debugpy-1.8.5-cp38-cp38-win32.whl", hash = "sha256:8f913ee8e9fcf9d38a751f56e6de12a297ae7832749d35de26d960f14280750a"}, - {file = "debugpy-1.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:a697beca97dad3780b89a7fb525d5e79f33821a8bc0c06faf1f1289e549743cf"}, - {file = "debugpy-1.8.5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c"}, - {file = "debugpy-1.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406"}, - {file = "debugpy-1.8.5-cp39-cp39-win32.whl", hash = "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34"}, - {file = "debugpy-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c"}, - {file = "debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44"}, - {file = "debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0"}, -] - -[[package]] -name = "decorator" -version = "5.1.1" -description = "Decorators for Humans" -optional = false -python-versions = ">=3.5" -files = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] - -[[package]] -name = "defusedxml" -version = "0.7.1" -description = "XML bomb protection for Python stdlib modules" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, -] - -[[package]] -name = "deprecated" -version = "1.2.14" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, - {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] - -[[package]] -name = "distlib" -version = "0.3.8" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, -] - -[[package]] -name = "distro" -version = "1.9.0" -description = "Distro - an OS platform information API" -optional = false -python-versions = ">=3.6" -files = [ - {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, - {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, -] - -[[package]] -name = "dnspython" -version = "2.6.1" -description = "DNS toolkit" -optional = false -python-versions = ">=3.8" -files = [ - {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, - {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, -] - -[package.extras] -dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] -dnssec = ["cryptography (>=41)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] -doq = ["aioquic (>=0.9.25)"] -idna = ["idna (>=3.6)"] -trio = ["trio (>=0.23)"] -wmi = ["wmi (>=1.5.1)"] - -[[package]] -name = "docstring-parser" -version = "0.16" -description = "Parse Python docstrings in reST, Google and Numpydoc format" -optional = false -python-versions = ">=3.6,<4.0" -files = [ - {file = "docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637"}, - {file = "docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e"}, -] - -[[package]] -name = "environs" -version = "9.5.0" -description = "simplified environment variable parsing" -optional = false -python-versions = ">=3.6" -files = [ - {file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"}, - {file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"}, -] - -[package.dependencies] -marshmallow = ">=3.0.0" -python-dotenv = "*" - -[package.extras] -dev = ["dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)", "pytest", "tox"] -django = ["dj-database-url", "dj-email-url", "django-cache-url"] -lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"] -tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "execnet" -version = "2.1.1" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.8" -files = [ - {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, - {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, -] - -[package.extras] -testing = ["hatch", "pre-commit", "pytest", "tox"] - -[[package]] -name = "executing" -version = "2.0.1" -description = "Get the currently executing AST node of a frame, and other information" -optional = false -python-versions = ">=3.5" -files = [ - {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, - {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, -] - -[package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] - -[[package]] -name = "fastapi" -version = "0.112.0" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fastapi-0.112.0-py3-none-any.whl", hash = "sha256:3487ded9778006a45834b8c816ec4a48d522e2631ca9e75ec5a774f1b052f821"}, - {file = "fastapi-0.112.0.tar.gz", hash = "sha256:d262bc56b7d101d1f4e8fc0ad2ac75bb9935fec504d2b7117686cec50710cf05"}, -] - -[package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.37.2,<0.38.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "fastjsonschema" -version = "2.20.0" -description = "Fastest Python implementation of JSON schema" -optional = false -python-versions = "*" -files = [ - {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"}, - {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"}, -] - -[package.extras] -devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] - -[[package]] -name = "filelock" -version = "3.15.4" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, - {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] -typing = ["typing-extensions (>=4.8)"] - -[[package]] -name = "flatbuffers" -version = "24.3.25" -description = "The FlatBuffers serialization format for Python" -optional = false -python-versions = "*" -files = [ - {file = "flatbuffers-24.3.25-py2.py3-none-any.whl", hash = "sha256:8dbdec58f935f3765e4f7f3cf635ac3a77f83568138d6a2311f524ec96364812"}, - {file = "flatbuffers-24.3.25.tar.gz", hash = "sha256:de2ec5b203f21441716617f38443e0a8ebf3d25bf0d9c0bb0ce68fa00ad546a4"}, -] - -[[package]] -name = "frozenlist" -version = "1.4.1" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.8" -files = [ - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, - {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, - {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, - {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, - {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, - {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, - {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, - {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, - {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, - {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, - {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, - {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, - {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, -] - -[[package]] -name = "fsspec" -version = "2024.6.1" -description = "File-system specification" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fsspec-2024.6.1-py3-none-any.whl", hash = "sha256:3cb443f8bcd2efb31295a5b9fdb02aee81d8452c80d28f97a6d0959e6cee101e"}, - {file = "fsspec-2024.6.1.tar.gz", hash = "sha256:fad7d7e209dd4c1208e3bbfda706620e0da5142bebbd9c384afb95b07e798e49"}, -] - -[package.extras] -abfs = ["adlfs"] -adl = ["adlfs"] -arrow = ["pyarrow (>=1)"] -dask = ["dask", "distributed"] -dev = ["pre-commit", "ruff"] -doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] -dropbox = ["dropbox", "dropboxdrivefs", "requests"] -full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] -fuse = ["fusepy"] -gcs = ["gcsfs"] -git = ["pygit2"] -github = ["requests"] -gs = ["gcsfs"] -gui = ["panel"] -hdfs = ["pyarrow (>=1)"] -http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] -libarchive = ["libarchive-c"] -oci = ["ocifs"] -s3 = ["s3fs"] -sftp = ["paramiko"] -smb = ["smbprotocol"] -ssh = ["paramiko"] -test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] -test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] -test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] -tqdm = ["tqdm"] - -[[package]] -name = "google-ai-generativelanguage" -version = "0.6.6" -description = "Google Ai Generativelanguage API client library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google-ai-generativelanguage-0.6.6.tar.gz", hash = "sha256:1739f035caeeeca5c28f887405eec8690f3372daf79fecf26454a97a4f1733a8"}, - {file = "google_ai_generativelanguage-0.6.6-py3-none-any.whl", hash = "sha256:59297737931f073d55ce1268dcc6d95111ee62850349d2b6cde942b16a4fca5c"}, -] - -[package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" -proto-plus = ">=1.22.3,<2.0.0dev" -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" - -[[package]] -name = "google-api-core" -version = "2.19.1" -description = "Google API client core library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google-api-core-2.19.1.tar.gz", hash = "sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd"}, - {file = "google_api_core-2.19.1-py3-none-any.whl", hash = "sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125"}, -] - -[package.dependencies] -google-auth = ">=2.14.1,<3.0.dev0" -googleapis-common-protos = ">=1.56.2,<2.0.dev0" -grpcio = [ - {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, - {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, -] -grpcio-status = [ - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, - {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, -] -proto-plus = ">=1.22.3,<2.0.0dev" -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" -requests = ">=2.18.0,<3.0.0.dev0" - -[package.extras] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] -grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] -grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] - -[[package]] -name = "google-api-python-client" -version = "2.140.0" -description = "Google API Client Library for Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google_api_python_client-2.140.0-py2.py3-none-any.whl", hash = "sha256:aeb4bb99e9fdd241473da5ff35464a0658fea0db76fe89c0f8c77ecfc3813404"}, - {file = "google_api_python_client-2.140.0.tar.gz", hash = "sha256:0bb973adccbe66a3d0a70abe4e49b3f2f004d849416bfec38d22b75649d389d8"}, -] - -[package.dependencies] -google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" -google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" -google-auth-httplib2 = ">=0.2.0,<1.0.0" -httplib2 = ">=0.19.0,<1.dev0" -uritemplate = ">=3.0.1,<5" - -[[package]] -name = "google-auth" -version = "2.33.0" -description = "Google Authentication Library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google_auth-2.33.0-py2.py3-none-any.whl", hash = "sha256:8eff47d0d4a34ab6265c50a106a3362de6a9975bb08998700e389f857e4d39df"}, - {file = "google_auth-2.33.0.tar.gz", hash = "sha256:d6a52342160d7290e334b4d47ba390767e4438ad0d45b7630774533e82655b95"}, -] - -[package.dependencies] -cachetools = ">=2.0.0,<6.0" -pyasn1-modules = ">=0.2.1" -rsa = ">=3.1.4,<5" - -[package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] -enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] -pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] -reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0.dev0)"] - -[[package]] -name = "google-auth-httplib2" -version = "0.2.0" -description = "Google Authentication Library: httplib2 transport" -optional = false -python-versions = "*" -files = [ - {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, - {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, -] - -[package.dependencies] -google-auth = "*" -httplib2 = ">=0.19.0" - -[[package]] -name = "google-cloud-aiplatform" -version = "1.62.0" -description = "Vertex AI API client library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "google-cloud-aiplatform-1.62.0.tar.gz", hash = "sha256:e15d5b2a99e30d4a16f4c51cfb8129962e6da41a9027d2ea696abe0e2f006fe8"}, - {file = "google_cloud_aiplatform-1.62.0-py2.py3-none-any.whl", hash = "sha256:d7738e0fd4494a54ae08a51755a2143d58937cba2db826189771f45566c9ee3c"}, -] - -[package.dependencies] -docstring-parser = "<1" -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.8.dev0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<3.0.0dev" -google-cloud-bigquery = ">=1.15.0,<3.20.0 || >3.20.0,<4.0.0dev" -google-cloud-resource-manager = ">=1.3.3,<3.0.0dev" -google-cloud-storage = ">=1.32.0,<3.0.0dev" -packaging = ">=14.3" -proto-plus = ">=1.22.3,<2.0.0dev" -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" -pydantic = "<3" -shapely = "<3.0.0dev" - -[package.extras] -autologging = ["mlflow (>=1.27.0,<=2.1.1)"] -cloud-profiler = ["tensorboard-plugin-profile (>=2.4.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "werkzeug (>=2.0.0,<2.1.0dev)"] -datasets = ["pyarrow (>=10.0.1)", "pyarrow (>=14.0.0)", "pyarrow (>=3.0.0,<8.0dev)"] -endpoint = ["requests (>=2.28.1)"] -full = ["cloudpickle (<3.0)", "docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0)", "fastapi (>=0.71.0,<=0.109.1)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-cloud-logging (<4.0)", "google-vizier (>=0.1.6)", "httpx (>=0.23.0,<0.25.0)", "immutabledict", "lit-nlp (==0.4.0)", "mlflow (>=1.27.0,<=2.1.1)", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "pandas (>=1.0.0,<2.2.0)", "pyarrow (>=10.0.1)", "pyarrow (>=14.0.0)", "pyarrow (>=3.0.0,<8.0dev)", "pyarrow (>=6.0.1)", "pydantic (<2)", "pyyaml (>=5.3.1,<7)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<=2.9.3)", "ray[default] (>=2.5,<=2.9.3)", "requests (>=2.28.1)", "setuptools (<70.0.0)", "starlette (>=0.17.1)", "tensorboard-plugin-profile (>=2.4.0,<3.0.0dev)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "tqdm (>=4.23.0)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)", "werkzeug (>=2.0.0,<2.1.0dev)"] -langchain = ["langchain (>=0.1.16,<0.3)", "langchain-core (<0.3)", "langchain-google-vertexai (<2)", "openinference-instrumentation-langchain (>=0.1.19,<0.2)", "orjson (<=3.10.6)", "tenacity (<=8.3)"] -langchain-testing = ["absl-py", "cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "langchain (>=0.1.16,<0.3)", "langchain-core (<0.3)", "langchain-google-vertexai (<2)", "openinference-instrumentation-langchain (>=0.1.19,<0.2)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-sdk (<2)", "orjson (<=3.10.6)", "pydantic (>=2.6.3,<3)", "pytest-xdist", "tenacity (<=8.3)"] -lit = ["explainable-ai-sdk (>=1.0.0)", "lit-nlp (==0.4.0)", "pandas (>=1.0.0)", "tensorflow (>=2.3.0,<3.0.0dev)"] -metadata = ["numpy (>=1.15.0)", "pandas (>=1.0.0)"] -pipelines = ["pyyaml (>=5.3.1,<7)"] -prediction = ["docker (>=5.0.3)", "fastapi (>=0.71.0,<=0.109.1)", "httpx (>=0.23.0,<0.25.0)", "starlette (>=0.17.1)", "uvicorn[standard] (>=0.16.0)"] -preview = ["cloudpickle (<3.0)", "google-cloud-logging (<4.0)"] -private-endpoints = ["requests (>=2.28.1)", "urllib3 (>=1.21.1,<1.27)"] -rapid-evaluation = ["pandas (>=1.0.0,<2.2.0)", "tqdm (>=4.23.0)"] -ray = ["google-cloud-bigquery", "google-cloud-bigquery-storage", "immutabledict", "pandas (>=1.0.0,<2.2.0)", "pyarrow (>=6.0.1)", "pydantic (<2)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<=2.9.3)", "ray[default] (>=2.5,<=2.9.3)", "setuptools (<70.0.0)"] -ray-testing = ["google-cloud-bigquery", "google-cloud-bigquery-storage", "immutabledict", "pandas (>=1.0.0,<2.2.0)", "pyarrow (>=6.0.1)", "pydantic (<2)", "pytest-xdist", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<=2.9.3)", "ray[default] (>=2.5,<=2.9.3)", "ray[train] (==2.9.3)", "scikit-learn", "setuptools (<70.0.0)", "tensorflow", "torch (>=2.0.0,<2.1.0)", "xgboost", "xgboost-ray"] -reasoningengine = ["cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.6.3,<3)"] -tensorboard = ["tensorboard-plugin-profile (>=2.4.0,<3.0.0dev)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "werkzeug (>=2.0.0,<2.1.0dev)"] -testing = ["bigframes", "cloudpickle (<3.0)", "docker (>=5.0.3)", "explainable-ai-sdk (>=1.0.0)", "fastapi (>=0.71.0,<=0.109.1)", "google-api-core (>=2.11,<3.0.0)", "google-cloud-bigquery", "google-cloud-bigquery-storage", "google-cloud-logging (<4.0)", "google-vizier (>=0.1.6)", "grpcio-testing", "httpx (>=0.23.0,<0.25.0)", "immutabledict", "ipython", "kfp (>=2.6.0,<3.0.0)", "lit-nlp (==0.4.0)", "mlflow (>=1.27.0,<=2.1.1)", "nltk", "numpy (>=1.15.0)", "pandas (>=1.0.0)", "pandas (>=1.0.0,<2.2.0)", "pyarrow (>=10.0.1)", "pyarrow (>=14.0.0)", "pyarrow (>=3.0.0,<8.0dev)", "pyarrow (>=6.0.1)", "pydantic (<2)", "pyfakefs", "pytest-asyncio", "pytest-xdist", "pyyaml (>=5.3.1,<7)", "ray[default] (>=2.4,<2.5.dev0 || >2.9.0,!=2.9.1,!=2.9.2,<=2.9.3)", "ray[default] (>=2.5,<=2.9.3)", "requests (>=2.28.1)", "requests-toolbelt (<1.0.0)", "scikit-learn", "sentencepiece (>=0.2.0)", "setuptools (<70.0.0)", "starlette (>=0.17.1)", "tensorboard-plugin-profile (>=2.4.0,<3.0.0dev)", "tensorflow (==2.13.0)", "tensorflow (==2.16.1)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.3.0,<3.0.0dev)", "tensorflow (>=2.4.0,<3.0.0dev)", "torch (>=2.0.0,<2.1.0)", "torch (>=2.2.0)", "tqdm (>=4.23.0)", "urllib3 (>=1.21.1,<1.27)", "uvicorn[standard] (>=0.16.0)", "werkzeug (>=2.0.0,<2.1.0dev)", "xgboost"] -tokenization = ["sentencepiece (>=0.2.0)"] -vizier = ["google-vizier (>=0.1.6)"] -xai = ["tensorflow (>=2.3.0,<3.0.0dev)"] - -[[package]] -name = "google-cloud-bigquery" -version = "3.25.0" -description = "Google BigQuery API client library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google-cloud-bigquery-3.25.0.tar.gz", hash = "sha256:5b2aff3205a854481117436836ae1403f11f2594e6810a98886afd57eda28509"}, - {file = "google_cloud_bigquery-3.25.0-py2.py3-none-any.whl", hash = "sha256:7f0c371bc74d2a7fb74dacbc00ac0f90c8c2bec2289b51dd6685a275873b1ce9"}, -] - -[package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<3.0.0dev" -google-cloud-core = ">=1.6.0,<3.0.0dev" -google-resumable-media = ">=0.6.0,<3.0dev" -packaging = ">=20.0.0" -python-dateutil = ">=2.7.2,<3.0dev" -requests = ">=2.21.0,<3.0.0dev" - -[package.extras] -all = ["Shapely (>=1.8.4,<3.0.0dev)", "db-dtypes (>=0.3.0,<2.0.0dev)", "geopandas (>=0.9.0,<1.0dev)", "google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "importlib-metadata (>=1.0.0)", "ipykernel (>=6.0.0)", "ipython (>=7.23.1,!=8.1.0)", "ipywidgets (>=7.7.0)", "opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)", "pandas (>=1.1.0)", "proto-plus (>=1.15.0,<2.0.0dev)", "protobuf (>=3.19.5,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0dev)", "pyarrow (>=3.0.0)", "tqdm (>=4.7.4,<5.0.0dev)"] -bigquery-v2 = ["proto-plus (>=1.15.0,<2.0.0dev)", "protobuf (>=3.19.5,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0dev)"] -bqstorage = ["google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "pyarrow (>=3.0.0)"] -geopandas = ["Shapely (>=1.8.4,<3.0.0dev)", "geopandas (>=0.9.0,<1.0dev)"] -ipython = ["ipykernel (>=6.0.0)", "ipython (>=7.23.1,!=8.1.0)"] -ipywidgets = ["ipykernel (>=6.0.0)", "ipywidgets (>=7.7.0)"] -opentelemetry = ["opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)"] -pandas = ["db-dtypes (>=0.3.0,<2.0.0dev)", "importlib-metadata (>=1.0.0)", "pandas (>=1.1.0)", "pyarrow (>=3.0.0)"] -tqdm = ["tqdm (>=4.7.4,<5.0.0dev)"] - -[[package]] -name = "google-cloud-core" -version = "2.4.1" -description = "Google Cloud API client core library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073"}, - {file = "google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61"}, -] - -[package.dependencies] -google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" -google-auth = ">=1.25.0,<3.0dev" - -[package.extras] -grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] - -[[package]] -name = "google-cloud-resource-manager" -version = "1.12.5" -description = "Google Cloud Resource Manager API client library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google_cloud_resource_manager-1.12.5-py2.py3-none-any.whl", hash = "sha256:2708a718b45c79464b7b21559c701b5c92e6b0b1ab2146d0a256277a623dc175"}, - {file = "google_cloud_resource_manager-1.12.5.tar.gz", hash = "sha256:b7af4254401ed4efa3aba3a929cb3ddb803fa6baf91a78485e45583597de5891"}, -] - -[package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" -grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev" -proto-plus = ">=1.22.3,<2.0.0dev" -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" - -[[package]] -name = "google-cloud-storage" -version = "2.18.2" -description = "Google Cloud Storage API client library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google_cloud_storage-2.18.2-py2.py3-none-any.whl", hash = "sha256:97a4d45c368b7d401ed48c4fdfe86e1e1cb96401c9e199e419d289e2c0370166"}, - {file = "google_cloud_storage-2.18.2.tar.gz", hash = "sha256:aaf7acd70cdad9f274d29332673fcab98708d0e1f4dceb5a5356aaef06af4d99"}, -] - -[package.dependencies] -google-api-core = ">=2.15.0,<3.0.0dev" -google-auth = ">=2.26.1,<3.0dev" -google-cloud-core = ">=2.3.0,<3.0dev" -google-crc32c = ">=1.0,<2.0dev" -google-resumable-media = ">=2.7.2" -requests = ">=2.18.0,<3.0.0dev" - -[package.extras] -protobuf = ["protobuf (<6.0.0dev)"] -tracing = ["opentelemetry-api (>=1.1.0)"] - -[[package]] -name = "google-crc32c" -version = "1.5.0" -description = "A python wrapper of the C library 'Google CRC32C'" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google-crc32c-1.5.0.tar.gz", hash = "sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7"}, - {file = "google_crc32c-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13"}, - {file = "google_crc32c-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346"}, - {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65"}, - {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b"}, - {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02"}, - {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4"}, - {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e"}, - {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c"}, - {file = "google_crc32c-1.5.0-cp310-cp310-win32.whl", hash = "sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee"}, - {file = "google_crc32c-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289"}, - {file = "google_crc32c-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273"}, - {file = "google_crc32c-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298"}, - {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57"}, - {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438"}, - {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906"}, - {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183"}, - {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd"}, - {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c"}, - {file = "google_crc32c-1.5.0-cp311-cp311-win32.whl", hash = "sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709"}, - {file = "google_crc32c-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-win32.whl", hash = "sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740"}, - {file = "google_crc32c-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8"}, - {file = "google_crc32c-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37"}, - {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894"}, - {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-win32.whl", hash = "sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4"}, - {file = "google_crc32c-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c"}, - {file = "google_crc32c-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7"}, - {file = "google_crc32c-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210"}, - {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd"}, - {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96"}, - {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61"}, - {file = "google_crc32c-1.5.0-cp39-cp39-win32.whl", hash = "sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c"}, - {file = "google_crc32c-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93"}, -] - -[package.extras] -testing = ["pytest"] - -[[package]] -name = "google-generativeai" -version = "0.7.2" -description = "Google Generative AI High level API client library and tools." -optional = false -python-versions = ">=3.9" -files = [ - {file = "google_generativeai-0.7.2-py3-none-any.whl", hash = "sha256:3117d1ebc92ee77710d4bc25ab4763492fddce9b6332eb25d124cf5d8b78b339"}, -] - -[package.dependencies] -google-ai-generativelanguage = "0.6.6" -google-api-core = "*" -google-api-python-client = "*" -google-auth = ">=2.15.0" -protobuf = "*" -pydantic = "*" -tqdm = "*" -typing-extensions = "*" - -[package.extras] -dev = ["Pillow", "absl-py", "black", "ipython", "nose2", "pandas", "pytype", "pyyaml"] - -[[package]] -name = "google-resumable-media" -version = "2.7.2" -description = "Utilities for Google Media Downloads and Resumable Uploads" -optional = false -python-versions = ">=3.7" -files = [ - {file = "google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa"}, - {file = "google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0"}, -] - -[package.dependencies] -google-crc32c = ">=1.0,<2.0dev" - -[package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "google-auth (>=1.22.0,<2.0dev)"] -requests = ["requests (>=2.18.0,<3.0.0dev)"] - -[[package]] -name = "googleapis-common-protos" -version = "1.63.2" -description = "Common protobufs used in Google APIs" -optional = false -python-versions = ">=3.7" -files = [ - {file = "googleapis-common-protos-1.63.2.tar.gz", hash = "sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87"}, - {file = "googleapis_common_protos-1.63.2-py2.py3-none-any.whl", hash = "sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945"}, -] - -[package.dependencies] -grpcio = {version = ">=1.44.0,<2.0.0.dev0", optional = true, markers = "extra == \"grpc\""} -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" - -[package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] - -[[package]] -name = "grpc-google-iam-v1" -version = "0.13.1" -description = "IAM API client library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "grpc-google-iam-v1-0.13.1.tar.gz", hash = "sha256:3ff4b2fd9d990965e410965253c0da6f66205d5a8291c4c31c6ebecca18a9001"}, - {file = "grpc_google_iam_v1-0.13.1-py2.py3-none-any.whl", hash = "sha256:c3e86151a981811f30d5e7330f271cee53e73bb87755e88cc3b6f0c7b5fe374e"}, -] - -[package.dependencies] -googleapis-common-protos = {version = ">=1.56.0,<2.0.0dev", extras = ["grpc"]} -grpcio = ">=1.44.0,<2.0.0dev" -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" - -[[package]] -name = "grpcio" -version = "1.63.0" -description = "HTTP/2-based RPC framework" -optional = false -python-versions = ">=3.8" -files = [ - {file = "grpcio-1.63.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:2e93aca840c29d4ab5db93f94ed0a0ca899e241f2e8aec6334ab3575dc46125c"}, - {file = "grpcio-1.63.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:91b73d3f1340fefa1e1716c8c1ec9930c676d6b10a3513ab6c26004cb02d8b3f"}, - {file = "grpcio-1.63.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:b3afbd9d6827fa6f475a4f91db55e441113f6d3eb9b7ebb8fb806e5bb6d6bd0d"}, - {file = "grpcio-1.63.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f3f6883ce54a7a5f47db43289a0a4c776487912de1a0e2cc83fdaec9685cc9f"}, - {file = "grpcio-1.63.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf8dae9cc0412cb86c8de5a8f3be395c5119a370f3ce2e69c8b7d46bb9872c8d"}, - {file = "grpcio-1.63.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:08e1559fd3b3b4468486b26b0af64a3904a8dbc78d8d936af9c1cf9636eb3e8b"}, - {file = "grpcio-1.63.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5c039ef01516039fa39da8a8a43a95b64e288f79f42a17e6c2904a02a319b357"}, - {file = "grpcio-1.63.0-cp310-cp310-win32.whl", hash = "sha256:ad2ac8903b2eae071055a927ef74121ed52d69468e91d9bcbd028bd0e554be6d"}, - {file = "grpcio-1.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:b2e44f59316716532a993ca2966636df6fbe7be4ab6f099de6815570ebe4383a"}, - {file = "grpcio-1.63.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:f28f8b2db7b86c77916829d64ab21ff49a9d8289ea1564a2b2a3a8ed9ffcccd3"}, - {file = "grpcio-1.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:65bf975639a1f93bee63ca60d2e4951f1b543f498d581869922910a476ead2f5"}, - {file = "grpcio-1.63.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:b5194775fec7dc3dbd6a935102bb156cd2c35efe1685b0a46c67b927c74f0cfb"}, - {file = "grpcio-1.63.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4cbb2100ee46d024c45920d16e888ee5d3cf47c66e316210bc236d5bebc42b3"}, - {file = "grpcio-1.63.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ff737cf29b5b801619f10e59b581869e32f400159e8b12d7a97e7e3bdeee6a2"}, - {file = "grpcio-1.63.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd1e68776262dd44dedd7381b1a0ad09d9930ffb405f737d64f505eb7f77d6c7"}, - {file = "grpcio-1.63.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:93f45f27f516548e23e4ec3fbab21b060416007dbe768a111fc4611464cc773f"}, - {file = "grpcio-1.63.0-cp311-cp311-win32.whl", hash = "sha256:878b1d88d0137df60e6b09b74cdb73db123f9579232c8456f53e9abc4f62eb3c"}, - {file = "grpcio-1.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:756fed02dacd24e8f488f295a913f250b56b98fb793f41d5b2de6c44fb762434"}, - {file = "grpcio-1.63.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:93a46794cc96c3a674cdfb59ef9ce84d46185fe9421baf2268ccb556f8f81f57"}, - {file = "grpcio-1.63.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a7b19dfc74d0be7032ca1eda0ed545e582ee46cd65c162f9e9fc6b26ef827dc6"}, - {file = "grpcio-1.63.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:8064d986d3a64ba21e498b9a376cbc5d6ab2e8ab0e288d39f266f0fca169b90d"}, - {file = "grpcio-1.63.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:219bb1848cd2c90348c79ed0a6b0ea51866bc7e72fa6e205e459fedab5770172"}, - {file = "grpcio-1.63.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2d60cd1d58817bc5985fae6168d8b5655c4981d448d0f5b6194bbcc038090d2"}, - {file = "grpcio-1.63.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e350cb096e5c67832e9b6e018cf8a0d2a53b2a958f6251615173165269a91b0"}, - {file = "grpcio-1.63.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:56cdf96ff82e3cc90dbe8bac260352993f23e8e256e063c327b6cf9c88daf7a9"}, - {file = "grpcio-1.63.0-cp312-cp312-win32.whl", hash = "sha256:3a6d1f9ea965e750db7b4ee6f9fdef5fdf135abe8a249e75d84b0a3e0c668a1b"}, - {file = "grpcio-1.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:d2497769895bb03efe3187fb1888fc20e98a5f18b3d14b606167dacda5789434"}, - {file = "grpcio-1.63.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:fdf348ae69c6ff484402cfdb14e18c1b0054ac2420079d575c53a60b9b2853ae"}, - {file = "grpcio-1.63.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a3abfe0b0f6798dedd2e9e92e881d9acd0fdb62ae27dcbbfa7654a57e24060c0"}, - {file = "grpcio-1.63.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:6ef0ad92873672a2a3767cb827b64741c363ebaa27e7f21659e4e31f4d750280"}, - {file = "grpcio-1.63.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b416252ac5588d9dfb8a30a191451adbf534e9ce5f56bb02cd193f12d8845b7f"}, - {file = "grpcio-1.63.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b77eaefc74d7eb861d3ffbdf91b50a1bb1639514ebe764c47773b833fa2d91"}, - {file = "grpcio-1.63.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b005292369d9c1f80bf70c1db1c17c6c342da7576f1c689e8eee4fb0c256af85"}, - {file = "grpcio-1.63.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cdcda1156dcc41e042d1e899ba1f5c2e9f3cd7625b3d6ebfa619806a4c1aadda"}, - {file = "grpcio-1.63.0-cp38-cp38-win32.whl", hash = "sha256:01799e8649f9e94ba7db1aeb3452188048b0019dc37696b0f5ce212c87c560c3"}, - {file = "grpcio-1.63.0-cp38-cp38-win_amd64.whl", hash = "sha256:6a1a3642d76f887aa4009d92f71eb37809abceb3b7b5a1eec9c554a246f20e3a"}, - {file = "grpcio-1.63.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:75f701ff645858a2b16bc8c9fc68af215a8bb2d5a9b647448129de6e85d52bce"}, - {file = "grpcio-1.63.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cacdef0348a08e475a721967f48206a2254a1b26ee7637638d9e081761a5ba86"}, - {file = "grpcio-1.63.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:0697563d1d84d6985e40ec5ec596ff41b52abb3fd91ec240e8cb44a63b895094"}, - {file = "grpcio-1.63.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6426e1fb92d006e47476d42b8f240c1d916a6d4423c5258ccc5b105e43438f61"}, - {file = "grpcio-1.63.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48cee31bc5f5a31fb2f3b573764bd563aaa5472342860edcc7039525b53e46a"}, - {file = "grpcio-1.63.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:50344663068041b34a992c19c600236e7abb42d6ec32567916b87b4c8b8833b3"}, - {file = "grpcio-1.63.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:259e11932230d70ef24a21b9fb5bb947eb4703f57865a404054400ee92f42f5d"}, - {file = "grpcio-1.63.0-cp39-cp39-win32.whl", hash = "sha256:a44624aad77bf8ca198c55af811fd28f2b3eaf0a50ec5b57b06c034416ef2d0a"}, - {file = "grpcio-1.63.0-cp39-cp39-win_amd64.whl", hash = "sha256:166e5c460e5d7d4656ff9e63b13e1f6029b122104c1633d5f37eaea348d7356d"}, - {file = "grpcio-1.63.0.tar.gz", hash = "sha256:f3023e14805c61bc439fb40ca545ac3d5740ce66120a678a3c6c2c55b70343d1"}, -] - -[package.extras] -protobuf = ["grpcio-tools (>=1.63.0)"] - -[[package]] -name = "grpcio-health-checking" -version = "1.62.3" -description = "Standard Health Checking Service for gRPC" -optional = false -python-versions = ">=3.6" -files = [ - {file = "grpcio-health-checking-1.62.3.tar.gz", hash = "sha256:5074ba0ce8f0dcfe328408ec5c7551b2a835720ffd9b69dade7fa3e0dc1c7a93"}, - {file = "grpcio_health_checking-1.62.3-py3-none-any.whl", hash = "sha256:f29da7dd144d73b4465fe48f011a91453e9ff6c8af0d449254cf80021cab3e0d"}, -] - -[package.dependencies] -grpcio = ">=1.62.3" -protobuf = ">=4.21.6" - -[[package]] -name = "grpcio-status" -version = "1.62.3" -description = "Status proto mapping for gRPC" -optional = false -python-versions = ">=3.6" -files = [ - {file = "grpcio-status-1.62.3.tar.gz", hash = "sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485"}, - {file = "grpcio_status-1.62.3-py3-none-any.whl", hash = "sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8"}, -] - -[package.dependencies] -googleapis-common-protos = ">=1.5.5" -grpcio = ">=1.62.3" -protobuf = ">=4.21.6" - -[[package]] -name = "grpcio-tools" -version = "1.62.3" -description = "Protobuf code generator for gRPC" -optional = false -python-versions = ">=3.7" -files = [ - {file = "grpcio-tools-1.62.3.tar.gz", hash = "sha256:7c7136015c3d62c3eef493efabaf9e3380e3e66d24ee8e94c01cb71377f57833"}, - {file = "grpcio_tools-1.62.3-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2f968b049c2849540751ec2100ab05e8086c24bead769ca734fdab58698408c1"}, - {file = "grpcio_tools-1.62.3-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:0a8c0c4724ae9c2181b7dbc9b186df46e4f62cb18dc184e46d06c0ebeccf569e"}, - {file = "grpcio_tools-1.62.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5782883a27d3fae8c425b29a9d3dcf5f47d992848a1b76970da3b5a28d424b26"}, - {file = "grpcio_tools-1.62.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d812daffd0c2d2794756bd45a353f89e55dc8f91eb2fc840c51b9f6be62667"}, - {file = "grpcio_tools-1.62.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b47d0dda1bdb0a0ba7a9a6de88e5a1ed61f07fad613964879954961e36d49193"}, - {file = "grpcio_tools-1.62.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ca246dffeca0498be9b4e1ee169b62e64694b0f92e6d0be2573e65522f39eea9"}, - {file = "grpcio_tools-1.62.3-cp310-cp310-win32.whl", hash = "sha256:6a56d344b0bab30bf342a67e33d386b0b3c4e65868ffe93c341c51e1a8853ca5"}, - {file = "grpcio_tools-1.62.3-cp310-cp310-win_amd64.whl", hash = "sha256:710fecf6a171dcbfa263a0a3e7070e0df65ba73158d4c539cec50978f11dad5d"}, - {file = "grpcio_tools-1.62.3-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:703f46e0012af83a36082b5f30341113474ed0d91e36640da713355cd0ea5d23"}, - {file = "grpcio_tools-1.62.3-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:7cc83023acd8bc72cf74c2edbe85b52098501d5b74d8377bfa06f3e929803492"}, - {file = "grpcio_tools-1.62.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ff7d58a45b75df67d25f8f144936a3e44aabd91afec833ee06826bd02b7fbe7"}, - {file = "grpcio_tools-1.62.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f2483ea232bd72d98a6dc6d7aefd97e5bc80b15cd909b9e356d6f3e326b6e43"}, - {file = "grpcio_tools-1.62.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:962c84b4da0f3b14b3cdb10bc3837ebc5f136b67d919aea8d7bb3fd3df39528a"}, - {file = "grpcio_tools-1.62.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8ad0473af5544f89fc5a1ece8676dd03bdf160fb3230f967e05d0f4bf89620e3"}, - {file = "grpcio_tools-1.62.3-cp311-cp311-win32.whl", hash = "sha256:db3bc9fa39afc5e4e2767da4459df82b095ef0cab2f257707be06c44a1c2c3e5"}, - {file = "grpcio_tools-1.62.3-cp311-cp311-win_amd64.whl", hash = "sha256:e0898d412a434e768a0c7e365acabe13ff1558b767e400936e26b5b6ed1ee51f"}, - {file = "grpcio_tools-1.62.3-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:d102b9b21c4e1e40af9a2ab3c6d41afba6bd29c0aa50ca013bf85c99cdc44ac5"}, - {file = "grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:0a52cc9444df978438b8d2332c0ca99000521895229934a59f94f37ed896b133"}, - {file = "grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141d028bf5762d4a97f981c501da873589df3f7e02f4c1260e1921e565b376fa"}, - {file = "grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47a5c093ab256dec5714a7a345f8cc89315cb57c298b276fa244f37a0ba507f0"}, - {file = "grpcio_tools-1.62.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f6831fdec2b853c9daa3358535c55eed3694325889aa714070528cf8f92d7d6d"}, - {file = "grpcio_tools-1.62.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e02d7c1a02e3814c94ba0cfe43d93e872c758bd8fd5c2797f894d0c49b4a1dfc"}, - {file = "grpcio_tools-1.62.3-cp312-cp312-win32.whl", hash = "sha256:b881fd9505a84457e9f7e99362eeedd86497b659030cf57c6f0070df6d9c2b9b"}, - {file = "grpcio_tools-1.62.3-cp312-cp312-win_amd64.whl", hash = "sha256:11c625eebefd1fd40a228fc8bae385e448c7e32a6ae134e43cf13bbc23f902b7"}, - {file = "grpcio_tools-1.62.3-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:ec6fbded0c61afe6f84e3c2a43e6d656791d95747d6d28b73eff1af64108c434"}, - {file = "grpcio_tools-1.62.3-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:bfda6ee8990997a9df95c5606f3096dae65f09af7ca03a1e9ca28f088caca5cf"}, - {file = "grpcio_tools-1.62.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b77f9f9cee87cd798f0fe26b7024344d1b03a7cd2d2cba7035f8433b13986325"}, - {file = "grpcio_tools-1.62.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e02d3b96f2d0e4bab9ceaa30f37d4f75571e40c6272e95364bff3125a64d184"}, - {file = "grpcio_tools-1.62.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1da38070738da53556a4b35ab67c1b9884a5dd48fa2f243db35dc14079ea3d0c"}, - {file = "grpcio_tools-1.62.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ace43b26d88a58dcff16c20d23ff72b04d0a415f64d2820f4ff06b1166f50557"}, - {file = "grpcio_tools-1.62.3-cp37-cp37m-win_amd64.whl", hash = "sha256:350a80485e302daaa95d335a931f97b693e170e02d43767ab06552c708808950"}, - {file = "grpcio_tools-1.62.3-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:c3a1ac9d394f8e229eb28eec2e04b9a6f5433fa19c9d32f1cb6066e3c5114a1d"}, - {file = "grpcio_tools-1.62.3-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:11f363570dea661dde99e04a51bd108a5807b5df32a6f8bdf4860e34e94a4dbf"}, - {file = "grpcio_tools-1.62.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9ad9950119d8ae27634e68b7663cc8d340ae535a0f80d85a55e56a6973ab1f"}, - {file = "grpcio_tools-1.62.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c5d22b252dcef11dd1e0fbbe5bbfb9b4ae048e8880d33338215e8ccbdb03edc"}, - {file = "grpcio_tools-1.62.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:27cd9ef5c5d68d5ed104b6dcb96fe9c66b82050e546c9e255716903c3d8f0373"}, - {file = "grpcio_tools-1.62.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f4b1615adf67bd8bb71f3464146a6f9949972d06d21a4f5e87e73f6464d97f57"}, - {file = "grpcio_tools-1.62.3-cp38-cp38-win32.whl", hash = "sha256:e18e15287c31baf574fcdf8251fb7f997d64e96c6ecf467906e576da0a079af6"}, - {file = "grpcio_tools-1.62.3-cp38-cp38-win_amd64.whl", hash = "sha256:6c3064610826f50bd69410c63101954676edc703e03f9e8f978a135f1aaf97c1"}, - {file = "grpcio_tools-1.62.3-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:8e62cc7164b0b7c5128e637e394eb2ef3db0e61fc798e80c301de3b2379203ed"}, - {file = "grpcio_tools-1.62.3-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:c8ad5cce554e2fcaf8842dee5d9462583b601a3a78f8b76a153c38c963f58c10"}, - {file = "grpcio_tools-1.62.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec279dcf3518201fc592c65002754f58a6b542798cd7f3ecd4af086422f33f29"}, - {file = "grpcio_tools-1.62.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c989246c2aebc13253f08be32538a4039a64e12d9c18f6d662d7aee641dc8b5"}, - {file = "grpcio_tools-1.62.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ca4f5eeadbb57cf03317d6a2857823239a63a59cc935f5bd6cf6e8b7af7a7ecc"}, - {file = "grpcio_tools-1.62.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0cb3a3436ac119cbd37a7d3331d9bdf85dad21a6ac233a3411dff716dcbf401e"}, - {file = "grpcio_tools-1.62.3-cp39-cp39-win32.whl", hash = "sha256:3eae6ea76d62fcac091e1f15c2dcedf1dc3f114f8df1a972a8a0745e89f4cf61"}, - {file = "grpcio_tools-1.62.3-cp39-cp39-win_amd64.whl", hash = "sha256:eec73a005443061f4759b71a056f745e3b000dc0dc125c9f20560232dfbcbd14"}, -] - -[package.dependencies] -grpcio = ">=1.62.3" -protobuf = ">=4.21.6,<5.0dev" -setuptools = "*" - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "h2" -version = "4.1.0" -description = "HTTP/2 State-Machine based protocol implementation" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, - {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, -] - -[package.dependencies] -hpack = ">=4.0,<5" -hyperframe = ">=6.0,<7" - -[[package]] -name = "hiredis" -version = "3.0.0" -description = "Python wrapper for hiredis" -optional = false -python-versions = ">=3.8" -files = [ - {file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:4b182791c41c5eb1d9ed736f0ff81694b06937ca14b0d4dadde5dadba7ff6dae"}, - {file = "hiredis-3.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:13c275b483a052dd645eb2cb60d6380f1f5215e4c22d6207e17b86be6dd87ffa"}, - {file = "hiredis-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1018cc7f12824506f165027eabb302735b49e63af73eb4d5450c66c88f47026"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83a29cc7b21b746cb6a480189e49f49b2072812c445e66a9e38d2004d496b81c"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e241fab6332e8fb5f14af00a4a9c6aefa22f19a336c069b7ddbf28ef8341e8d6"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fb8de899f0145d6c4d5d4bd0ee88a78eb980a7ffabd51e9889251b8f58f1785"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b23291951959141173eec10f8573538e9349fa27f47a0c34323d1970bf891ee5"}, - {file = "hiredis-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e421ac9e4b5efc11705a0d5149e641d4defdc07077f748667f359e60dc904420"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77c8006c12154c37691b24ff293c077300c22944018c3ff70094a33e10c1d795"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:41afc0d3c18b59eb50970479a9c0e5544fb4b95e3a79cf2fbaece6ddefb926fe"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:04ccae6dcd9647eae6025425ab64edb4d79fde8b9e6e115ebfabc6830170e3b2"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fe91d62b0594db5ea7d23fc2192182b1a7b6973f628a9b8b2e0a42a2be721ac6"}, - {file = "hiredis-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99516d99316062824a24d145d694f5b0d030c80da693ea6f8c4ecf71a251d8bb"}, - {file = "hiredis-3.0.0-cp310-cp310-win32.whl", hash = "sha256:562eaf820de045eb487afaa37e6293fe7eceb5b25e158b5a1974b7e40bf04543"}, - {file = "hiredis-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1c81c89ed765198da27412aa21478f30d54ef69bf5e4480089d9c3f77b8f882"}, - {file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:4664dedcd5933364756d7251a7ea86d60246ccf73a2e00912872dacbfcef8978"}, - {file = "hiredis-3.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:47de0bbccf4c8a9f99d82d225f7672b9dd690d8fd872007b933ef51a302c9fa6"}, - {file = "hiredis-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e43679eca508ba8240d016d8cca9d27342d70184773c15bea78a23c87a1922f1"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13c345e7278c210317e77e1934b27b61394fee0dec2e8bd47e71570900f75823"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00018f22f38530768b73ea86c11f47e8d4df65facd4e562bd78773bd1baef35e"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ea3a86405baa8eb0d3639ced6926ad03e07113de54cb00fd7510cb0db76a89d"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c073848d2b1d5561f3903879ccf4e1a70c9b1e7566c7bdcc98d082fa3e7f0a1d"}, - {file = "hiredis-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a8dffb5f5b3415a4669d25de48b617fd9d44b0bccfc4c2ab24b06406ecc9ecb"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22c17c96143c2a62dfd61b13803bc5de2ac526b8768d2141c018b965d0333b66"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3ece960008dab66c6b8bb3a1350764677ee7c74ccd6270aaf1b1caf9ccebb46"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f75999ae00a920f7dce6ecae76fa5e8674a3110e5a75f12c7a2c75ae1af53396"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e069967cbd5e1900aafc4b5943888f6d34937fc59bf8918a1a546cb729b4b1e4"}, - {file = "hiredis-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0aacc0a78e1d94d843a6d191f224a35893e6bdfeb77a4a89264155015c65f126"}, - {file = "hiredis-3.0.0-cp311-cp311-win32.whl", hash = "sha256:719c32147ba29528cb451f037bf837dcdda4ff3ddb6cdb12c4216b0973174718"}, - {file = "hiredis-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:bdc144d56333c52c853c31b4e2e52cfbdb22d3da4374c00f5f3d67c42158970f"}, - {file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a"}, - {file = "hiredis-3.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1"}, - {file = "hiredis-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9"}, - {file = "hiredis-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001"}, - {file = "hiredis-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232"}, - {file = "hiredis-3.0.0-cp312-cp312-win32.whl", hash = "sha256:120f2dda469b28d12ccff7c2230225162e174657b49cf4cd119db525414ae281"}, - {file = "hiredis-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e584fe5f4e6681d8762982be055f1534e0170f6308a7a90f58d737bab12ff6a8"}, - {file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:122171ff47d96ed8dd4bba6c0e41d8afaba3e8194949f7720431a62aa29d8895"}, - {file = "hiredis-3.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:ba9fc605ac558f0de67463fb588722878641e6fa1dabcda979e8e69ff581d0bd"}, - {file = "hiredis-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a631e2990b8be23178f655cae8ac6c7422af478c420dd54e25f2e26c29e766f1"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63482db3fadebadc1d01ad33afa6045ebe2ea528eb77ccaabd33ee7d9c2bad48"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f669212c390eebfbe03c4e20181f5970b82c5d0a0ad1df1785f7ffbe7d61150"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a49ef161739f8018c69b371528bdb47d7342edfdee9ddc75a4d8caddf45a6e"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98a152052b8878e5e43a2e3a14075218adafc759547c98668a21e9485882696c"}, - {file = "hiredis-3.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50a196af0ce657fcde9bf8a0bbe1032e22c64d8fcec2bc926a35e7ff68b3a166"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f2f312eef8aafc2255e3585dcf94d5da116c43ef837db91db9ecdc1bc930072d"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:6ca41fa40fa019cde42c21add74aadd775e71458051a15a352eabeb12eb4d084"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6eecb343c70629f5af55a8b3e53264e44fa04e155ef7989de13668a0cb102a90"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:c3fdad75e7837a475900a1d3a5cc09aa024293c3b0605155da2d42f41bc0e482"}, - {file = "hiredis-3.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8854969e7480e8d61ed7549eb232d95082a743e94138d98d7222ba4e9f7ecacd"}, - {file = "hiredis-3.0.0-cp38-cp38-win32.whl", hash = "sha256:f114a6c86edbf17554672b050cce72abf489fe58d583c7921904d5f1c9691605"}, - {file = "hiredis-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:7d99b91e42217d7b4b63354b15b41ce960e27d216783e04c4a350224d55842a4"}, - {file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:4c6efcbb5687cf8d2aedcc2c3ed4ac6feae90b8547427d417111194873b66b06"}, - {file = "hiredis-3.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5b5cff42a522a0d81c2ae7eae5e56d0ee7365e0c4ad50c4de467d8957aff4414"}, - {file = "hiredis-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:82f794d564f4bc76b80c50b03267fe5d6589e93f08e66b7a2f674faa2fa76ebc"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7a4c1791d7aa7e192f60fe028ae409f18ccdd540f8b1e6aeb0df7816c77e4a4"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2537b2cd98192323fce4244c8edbf11f3cac548a9d633dbbb12b48702f379f4"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fed69bbaa307040c62195a269f82fc3edf46b510a17abb6b30a15d7dab548df"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869f6d5537d243080f44253491bb30aa1ec3c21754003b3bddeadedeb65842b0"}, - {file = "hiredis-3.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d435ae89073d7cd51e6b6bf78369c412216261c9c01662e7008ff00978153729"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:204b79b30a0e6be0dc2301a4d385bb61472809f09c49f400497f1cdd5a165c66"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ea635101b739c12effd189cc19b2671c268abb03013fd1f6321ca29df3ca625"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f359175197fd833c8dd7a8c288f1516be45415bb5c939862ab60c2918e1e1943"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac6d929cb33dd12ad3424b75725975f0a54b5b12dbff95f2a2d660c510aa106d"}, - {file = "hiredis-3.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:100431e04d25a522ef2c3b94f294c4219c4de3bfc7d557b6253296145a144c11"}, - {file = "hiredis-3.0.0-cp39-cp39-win32.whl", hash = "sha256:e1a9c14ae9573d172dc050a6f63a644457df5d01ec4d35a6a0f097f812930f83"}, - {file = "hiredis-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:54a6dd7b478e6eb01ce15b3bb5bf771e108c6c148315bf194eb2ab776a3cac4d"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50da7a9edf371441dfcc56288d790985ee9840d982750580710a9789b8f4a290"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b285ef6bf1581310b0d5e8f6ce64f790a1c40e89c660e1320b35f7515433672"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dcfa684966f25b335072115de2f920228a3c2caf79d4bfa2b30f6e4f674a948"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a41be8af1fd78ca97bc948d789a09b730d1e7587d07ca53af05758f31f4b985d"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:038756db735e417ab36ee6fd7725ce412385ed2bd0767e8179a4755ea11b804f"}, - {file = "hiredis-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fcecbd39bd42cef905c0b51c9689c39d0cc8b88b1671e7f40d4fb213423aef3a"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a131377493a59fb0f5eaeb2afd49c6540cafcfba5b0b3752bed707be9e7c4eaf"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d22c53f0ec5c18ecb3d92aa9420563b1c5d657d53f01356114978107b00b860"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a91e9520fbc65a799943e5c970ffbcd67905744d8becf2e75f9f0a5e8414f0"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dc8043959b50141df58ab4f398e8ae84c6f9e673a2c9407be65fc789138f4a6"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51b99cfac514173d7b8abdfe10338193e8a0eccdfe1870b646009d2fb7cbe4b5"}, - {file = "hiredis-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fa1fcad89d8a41d8dc10b1e54951ec1e161deabd84ed5a2c95c3c7213bdb3514"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:898636a06d9bf575d2c594129085ad6b713414038276a4bfc5db7646b8a5be78"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:466f836dbcf86de3f9692097a7a01533dc9926986022c6617dc364a402b265c5"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23142a8af92a13fc1e3f2ca1d940df3dcf2af1d176be41fe8d89e30a837a0b60"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:793c80a3d6b0b0e8196a2d5de37a08330125668c8012922685e17aa9108c33ac"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:467d28112c7faa29b7db743f40803d927c8591e9da02b6ce3d5fadc170a542a2"}, - {file = "hiredis-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dc384874a719c767b50a30750f937af18842ee5e288afba95a5a3ed703b1515a"}, - {file = "hiredis-3.0.0.tar.gz", hash = "sha256:fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441"}, -] - -[[package]] -name = "hpack" -version = "4.0.0" -description = "Pure-Python HPACK header compression" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, - {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, -] - -[[package]] -name = "httpcore" -version = "1.0.5" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, - {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.13,<0.15" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.26.0)"] - -[[package]] -name = "httplib2" -version = "0.22.0" -description = "A comprehensive HTTP client library." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, - {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, -] - -[package.dependencies] -pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} - -[[package]] -name = "httptools" -version = "0.6.1" -description = "A collection of framework independent HTTP protocol utils." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, - {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, - {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, - {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, - {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, - {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, - {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, - {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, - {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, - {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, - {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, - {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, - {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, - {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, - {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, - {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, - {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, - {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, - {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, - {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, - {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, - {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, - {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, - {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, - {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, - {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, - {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, - {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, - {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, - {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, - {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, - {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, - {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, - {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, - {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, - {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, -] - -[package.extras] -test = ["Cython (>=0.29.24,<0.30.0)"] - -[[package]] -name = "httpx" -version = "0.27.0" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} -httpcore = "==1.*" -idna = "*" -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] - -[[package]] -name = "huggingface-hub" -version = "0.24.5" -description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "huggingface_hub-0.24.5-py3-none-any.whl", hash = "sha256:d93fb63b1f1a919a22ce91a14518974e81fc4610bf344dfe7572343ce8d3aced"}, - {file = "huggingface_hub-0.24.5.tar.gz", hash = "sha256:7b45d6744dd53ce9cbf9880957de00e9d10a9ae837f1c9b7255fc8fa4e8264f3"}, -] - -[package.dependencies] -filelock = "*" -fsspec = ">=2023.5.0" -packaging = ">=20.9" -pyyaml = ">=5.1" -requests = "*" -tqdm = ">=4.42.1" -typing-extensions = ">=3.7.4.3" - -[package.extras] -all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -cli = ["InquirerPy (==0.3.4)"] -dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] -hf-transfer = ["hf-transfer (>=0.1.4)"] -inference = ["aiohttp", "minijinja (>=1.0)"] -quality = ["mypy (==1.5.1)", "ruff (>=0.5.0)"] -tensorflow = ["graphviz", "pydot", "tensorflow"] -tensorflow-testing = ["keras (<3.0)", "tensorflow"] -testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] -torch = ["safetensors[torch]", "torch"] -typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] - -[[package]] -name = "humanfriendly" -version = "10.0" -description = "Human friendly output for text interfaces using Python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, - {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, -] - -[package.dependencies] -pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} - -[[package]] -name = "hyperframe" -version = "6.0.1" -description = "HTTP/2 framing layer for Python" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, - {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, -] - -[[package]] -name = "identify" -version = "2.6.0" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, - {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.7" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, -] - -[[package]] -name = "importlib-metadata" -version = "8.0.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, - {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] - -[[package]] -name = "importlib-resources" -version = "6.4.0" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, - {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "ipykernel" -version = "6.29.5" -description = "IPython Kernel for Jupyter" -optional = false -python-versions = ">=3.8" -files = [ - {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, - {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, -] - -[package.dependencies] -appnope = {version = "*", markers = "platform_system == \"Darwin\""} -comm = ">=0.1.1" -debugpy = ">=1.6.5" -ipython = ">=7.23.1" -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -matplotlib-inline = ">=0.1" -nest-asyncio = "*" -packaging = "*" -psutil = "*" -pyzmq = ">=24" -tornado = ">=6.1" -traitlets = ">=5.4.0" - -[package.extras] -cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] -pyqt5 = ["pyqt5"] -pyside6 = ["pyside6"] -test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "ipython" -version = "8.26.0" -description = "IPython: Productive Interactive Computing" -optional = false -python-versions = ">=3.10" -files = [ - {file = "ipython-8.26.0-py3-none-any.whl", hash = "sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff"}, - {file = "ipython-8.26.0.tar.gz", hash = "sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} -prompt-toolkit = ">=3.0.41,<3.1.0" -pygments = ">=2.4.0" -stack-data = "*" -traitlets = ">=5.13.0" -typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} - -[package.extras] -all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] -black = ["black"] -doc = ["docrepr", "exceptiongroup", "intersphinx-registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing-extensions"] -kernel = ["ipykernel"] -matplotlib = ["matplotlib"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["ipywidgets", "notebook"] -parallel = ["ipyparallel"] -qtconsole = ["qtconsole"] -test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] - -[[package]] -name = "isodate" -version = "0.6.1" -description = "An ISO 8601 date/time/duration parser and formatter" -optional = false -python-versions = "*" -files = [ - {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, - {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, -] - -[package.dependencies] -six = "*" - -[[package]] -name = "jedi" -version = "0.19.1" -description = "An autocompletion tool for Python that can be used for text editors." -optional = false -python-versions = ">=3.6" -files = [ - {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, -] - -[package.dependencies] -parso = ">=0.8.3,<0.9.0" - -[package.extras] -docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] - -[[package]] -name = "jinja2" -version = "3.1.4" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jiter" -version = "0.5.0" -description = "Fast iterable JSON parser." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jiter-0.5.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b599f4e89b3def9a94091e6ee52e1d7ad7bc33e238ebb9c4c63f211d74822c3f"}, - {file = "jiter-0.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a063f71c4b06225543dddadbe09d203dc0c95ba352d8b85f1221173480a71d5"}, - {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc0d5b8b3dd12e91dd184b87273f864b363dfabc90ef29a1092d269f18c7e28"}, - {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c22541f0b672f4d741382a97c65609332a783501551445ab2df137ada01e019e"}, - {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63314832e302cc10d8dfbda0333a384bf4bcfce80d65fe99b0f3c0da8945a91a"}, - {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a25fbd8a5a58061e433d6fae6d5298777c0814a8bcefa1e5ecfff20c594bd749"}, - {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:503b2c27d87dfff5ab717a8200fbbcf4714516c9d85558048b1fc14d2de7d8dc"}, - {file = "jiter-0.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d1f3d27cce923713933a844872d213d244e09b53ec99b7a7fdf73d543529d6d"}, - {file = "jiter-0.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c95980207b3998f2c3b3098f357994d3fd7661121f30669ca7cb945f09510a87"}, - {file = "jiter-0.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:afa66939d834b0ce063f57d9895e8036ffc41c4bd90e4a99631e5f261d9b518e"}, - {file = "jiter-0.5.0-cp310-none-win32.whl", hash = "sha256:f16ca8f10e62f25fd81d5310e852df6649af17824146ca74647a018424ddeccf"}, - {file = "jiter-0.5.0-cp310-none-win_amd64.whl", hash = "sha256:b2950e4798e82dd9176935ef6a55cf6a448b5c71515a556da3f6b811a7844f1e"}, - {file = "jiter-0.5.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4c8e1ed0ef31ad29cae5ea16b9e41529eb50a7fba70600008e9f8de6376d553"}, - {file = "jiter-0.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6f16e21276074a12d8421692515b3fd6d2ea9c94fd0734c39a12960a20e85f3"}, - {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5280e68e7740c8c128d3ae5ab63335ce6d1fb6603d3b809637b11713487af9e6"}, - {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:583c57fc30cc1fec360e66323aadd7fc3edeec01289bfafc35d3b9dcb29495e4"}, - {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26351cc14507bdf466b5f99aba3df3143a59da75799bf64a53a3ad3155ecded9"}, - {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829df14d656b3fb87e50ae8b48253a8851c707da9f30d45aacab2aa2ba2d614"}, - {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42a4bdcf7307b86cb863b2fb9bb55029b422d8f86276a50487982d99eed7c6e"}, - {file = "jiter-0.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04d461ad0aebf696f8da13c99bc1b3e06f66ecf6cfd56254cc402f6385231c06"}, - {file = "jiter-0.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6375923c5f19888c9226582a124b77b622f8fd0018b843c45eeb19d9701c403"}, - {file = "jiter-0.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cec323a853c24fd0472517113768c92ae0be8f8c384ef4441d3632da8baa646"}, - {file = "jiter-0.5.0-cp311-none-win32.whl", hash = "sha256:aa1db0967130b5cab63dfe4d6ff547c88b2a394c3410db64744d491df7f069bb"}, - {file = "jiter-0.5.0-cp311-none-win_amd64.whl", hash = "sha256:aa9d2b85b2ed7dc7697597dcfaac66e63c1b3028652f751c81c65a9f220899ae"}, - {file = "jiter-0.5.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9f664e7351604f91dcdd557603c57fc0d551bc65cc0a732fdacbf73ad335049a"}, - {file = "jiter-0.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:044f2f1148b5248ad2c8c3afb43430dccf676c5a5834d2f5089a4e6c5bbd64df"}, - {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:702e3520384c88b6e270c55c772d4bd6d7b150608dcc94dea87ceba1b6391248"}, - {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:528d742dcde73fad9d63e8242c036ab4a84389a56e04efd854062b660f559544"}, - {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf80e5fe6ab582c82f0c3331df27a7e1565e2dcf06265afd5173d809cdbf9ba"}, - {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44dfc9ddfb9b51a5626568ef4e55ada462b7328996294fe4d36de02fce42721f"}, - {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c451f7922992751a936b96c5f5b9bb9312243d9b754c34b33d0cb72c84669f4e"}, - {file = "jiter-0.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:308fce789a2f093dca1ff91ac391f11a9f99c35369117ad5a5c6c4903e1b3e3a"}, - {file = "jiter-0.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7f5ad4a7c6b0d90776fdefa294f662e8a86871e601309643de30bf94bb93a64e"}, - {file = "jiter-0.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ea189db75f8eca08807d02ae27929e890c7d47599ce3d0a6a5d41f2419ecf338"}, - {file = "jiter-0.5.0-cp312-none-win32.whl", hash = "sha256:e3bbe3910c724b877846186c25fe3c802e105a2c1fc2b57d6688b9f8772026e4"}, - {file = "jiter-0.5.0-cp312-none-win_amd64.whl", hash = "sha256:a586832f70c3f1481732919215f36d41c59ca080fa27a65cf23d9490e75b2ef5"}, - {file = "jiter-0.5.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f04bc2fc50dc77be9d10f73fcc4e39346402ffe21726ff41028f36e179b587e6"}, - {file = "jiter-0.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6f433a4169ad22fcb550b11179bb2b4fd405de9b982601914ef448390b2954f3"}, - {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad4a6398c85d3a20067e6c69890ca01f68659da94d74c800298581724e426c7e"}, - {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6baa88334e7af3f4d7a5c66c3a63808e5efbc3698a1c57626541ddd22f8e4fbf"}, - {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ece0a115c05efca597c6d938f88c9357c843f8c245dbbb53361a1c01afd7148"}, - {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:335942557162ad372cc367ffaf93217117401bf930483b4b3ebdb1223dbddfa7"}, - {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649b0ee97a6e6da174bffcb3c8c051a5935d7d4f2f52ea1583b5b3e7822fbf14"}, - {file = "jiter-0.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f4be354c5de82157886ca7f5925dbda369b77344b4b4adf2723079715f823989"}, - {file = "jiter-0.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5206144578831a6de278a38896864ded4ed96af66e1e63ec5dd7f4a1fce38a3a"}, - {file = "jiter-0.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8120c60f8121ac3d6f072b97ef0e71770cc72b3c23084c72c4189428b1b1d3b6"}, - {file = "jiter-0.5.0-cp38-none-win32.whl", hash = "sha256:6f1223f88b6d76b519cb033a4d3687ca157c272ec5d6015c322fc5b3074d8a5e"}, - {file = "jiter-0.5.0-cp38-none-win_amd64.whl", hash = "sha256:c59614b225d9f434ea8fc0d0bec51ef5fa8c83679afedc0433905994fb36d631"}, - {file = "jiter-0.5.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0af3838cfb7e6afee3f00dc66fa24695199e20ba87df26e942820345b0afc566"}, - {file = "jiter-0.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:550b11d669600dbc342364fd4adbe987f14d0bbedaf06feb1b983383dcc4b961"}, - {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:489875bf1a0ffb3cb38a727b01e6673f0f2e395b2aad3c9387f94187cb214bbf"}, - {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b250ca2594f5599ca82ba7e68785a669b352156260c5362ea1b4e04a0f3e2389"}, - {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ea18e01f785c6667ca15407cd6dabbe029d77474d53595a189bdc813347218e"}, - {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462a52be85b53cd9bffd94e2d788a09984274fe6cebb893d6287e1c296d50653"}, - {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92cc68b48d50fa472c79c93965e19bd48f40f207cb557a8346daa020d6ba973b"}, - {file = "jiter-0.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c834133e59a8521bc87ebcad773608c6fa6ab5c7a022df24a45030826cf10bc"}, - {file = "jiter-0.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab3a71ff31cf2d45cb216dc37af522d335211f3a972d2fe14ea99073de6cb104"}, - {file = "jiter-0.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cccd3af9c48ac500c95e1bcbc498020c87e1781ff0345dd371462d67b76643eb"}, - {file = "jiter-0.5.0-cp39-none-win32.whl", hash = "sha256:368084d8d5c4fc40ff7c3cc513c4f73e02c85f6009217922d0823a48ee7adf61"}, - {file = "jiter-0.5.0-cp39-none-win_amd64.whl", hash = "sha256:ce03f7b4129eb72f1687fa11300fbf677b02990618428934662406d2a76742a1"}, - {file = "jiter-0.5.0.tar.gz", hash = "sha256:1d916ba875bcab5c5f7d927df998c4cb694d27dceddf3392e58beaf10563368a"}, -] - -[[package]] -name = "joblib" -version = "1.4.2" -description = "Lightweight pipelining with Python functions" -optional = false -python-versions = ">=3.8" -files = [ - {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"}, - {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, -] - -[[package]] -name = "jsonschema" -version = "4.23.0" -description = "An implementation of JSON Schema validation for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, - {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" -referencing = ">=0.28.4" -rpds-py = ">=0.7.1" - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] - -[[package]] -name = "jsonschema-path" -version = "0.3.3" -description = "JSONSchema Spec with object-oriented paths" -optional = false -python-versions = "<4.0.0,>=3.8.0" -files = [ - {file = "jsonschema_path-0.3.3-py3-none-any.whl", hash = "sha256:203aff257f8038cd3c67be614fe6b2001043408cb1b4e36576bc4921e09d83c4"}, - {file = "jsonschema_path-0.3.3.tar.gz", hash = "sha256:f02e5481a4288ec062f8e68c808569e427d905bedfecb7f2e4c69ef77957c382"}, -] - -[package.dependencies] -pathable = ">=0.4.1,<0.5.0" -PyYAML = ">=5.1" -referencing = ">=0.28.0,<0.36.0" -requests = ">=2.31.0,<3.0.0" - -[[package]] -name = "jsonschema-specifications" -version = "2023.12.1" -description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, - {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, -] - -[package.dependencies] -referencing = ">=0.31.0" - -[[package]] -name = "jupyter-client" -version = "8.6.2" -description = "Jupyter protocol implementation and client libraries" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"}, - {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"}, -] - -[package.dependencies] -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -python-dateutil = ">=2.8.2" -pyzmq = ">=23.0" -tornado = ">=6.2" -traitlets = ">=5.3" - -[package.extras] -docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] - -[[package]] -name = "jupyter-core" -version = "5.7.2" -description = "Jupyter core package. A base package on which Jupyter projects rely." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, - {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, -] - -[package.dependencies] -platformdirs = ">=2.5" -pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} -traitlets = ">=5.3" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] -test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "jupyterlab-pygments" -version = "0.3.0" -description = "Pygments theme using JupyterLab CSS variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, - {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, -] - -[[package]] -name = "kubernetes" -version = "30.1.0" -description = "Kubernetes python client" -optional = false -python-versions = ">=3.6" -files = [ - {file = "kubernetes-30.1.0-py2.py3-none-any.whl", hash = "sha256:e212e8b7579031dd2e512168b617373bc1e03888d41ac4e04039240a292d478d"}, - {file = "kubernetes-30.1.0.tar.gz", hash = "sha256:41e4c77af9f28e7a6c314e3bd06a8c6229ddd787cad684e0ab9f69b498e98ebc"}, -] - -[package.dependencies] -certifi = ">=14.05.14" -google-auth = ">=1.0.1" -oauthlib = ">=3.2.2" -python-dateutil = ">=2.5.3" -pyyaml = ">=5.4.1" -requests = "*" -requests-oauthlib = "*" -six = ">=1.9.0" -urllib3 = ">=1.24.2" -websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" - -[package.extras] -adal = ["adal (>=1.0.2)"] - -[[package]] -name = "lazy-object-proxy" -version = "1.10.0" -description = "A fast and thorough lazy object proxy." -optional = false -python-versions = ">=3.8" -files = [ - {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9"}, - {file = "lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fec03caabbc6b59ea4a638bee5fce7117be8e99a4103d9d5ad77f15d6f81020c"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c83f957782cbbe8136bee26416686a6ae998c7b6191711a04da776dc9e47d4"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009e6bb1f1935a62889ddc8541514b6a9e1fcf302667dcb049a0be5c8f613e56"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75fc59fc450050b1b3c203c35020bc41bd2695ed692a392924c6ce180c6f1dc9"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:782e2c9b2aab1708ffb07d4bf377d12901d7a1d99e5e410d648d892f8967ab1f"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-win32.whl", hash = "sha256:edb45bb8278574710e68a6b021599a10ce730d156e5b254941754a9cc0b17d03"}, - {file = "lazy_object_proxy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:e271058822765ad5e3bca7f05f2ace0de58a3f4e62045a8c90a0dfd2f8ad8cc6"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e98c8af98d5707dcdecc9ab0863c0ea6e88545d42ca7c3feffb6b4d1e370c7ba"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:952c81d415b9b80ea261d2372d2a4a2332a3890c2b83e0535f263ddfe43f0d43"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b39d3a151309efc8cc48675918891b865bdf742a8616a337cb0090791a0de9"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e221060b701e2aa2ea991542900dd13907a5c90fa80e199dbf5a03359019e7a3"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92f09ff65ecff3108e56526f9e2481b8116c0b9e1425325e13245abfd79bdb1b"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-win32.whl", hash = "sha256:3ad54b9ddbe20ae9f7c1b29e52f123120772b06dbb18ec6be9101369d63a4074"}, - {file = "lazy_object_proxy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:127a789c75151db6af398b8972178afe6bda7d6f68730c057fbbc2e96b08d282"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4ed0518a14dd26092614412936920ad081a424bdcb54cc13349a8e2c6d106a"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9e6ed739285919aa9661a5bbed0aaf410aa60231373c5579c6b4801bd883c"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc0a92c02fa1ca1e84fc60fa258458e5bf89d90a1ddaeb8ed9cc3147f417255"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0aefc7591920bbd360d57ea03c995cebc204b424524a5bd78406f6e1b8b2a5d8"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5faf03a7d8942bb4476e3b62fd0f4cf94eaf4618e304a19865abf89a35c0bbee"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-win32.whl", hash = "sha256:e333e2324307a7b5d86adfa835bb500ee70bfcd1447384a822e96495796b0ca4"}, - {file = "lazy_object_proxy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:cb73507defd385b7705c599a94474b1d5222a508e502553ef94114a143ec6696"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366c32fe5355ef5fc8a232c5436f4cc66e9d3e8967c01fb2e6302fd6627e3d94"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297f08f08a2bb0d32a4265e98a006643cd7233fb7983032bd61ac7a02956b3b"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18dd842b49456aaa9a7cf535b04ca4571a302ff72ed8740d06b5adcd41fe0757"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:217138197c170a2a74ca0e05bddcd5f1796c735c37d0eee33e43259b192aa424"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a3a87cf1e133e5b1994144c12ca4aa3d9698517fe1e2ca82977781b16955658"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-win32.whl", hash = "sha256:30b339b2a743c5288405aa79a69e706a06e02958eab31859f7f3c04980853b70"}, - {file = "lazy_object_proxy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:a899b10e17743683b293a729d3a11f2f399e8a90c73b089e29f5d0fe3509f0dd"}, - {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "2.1.5" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, -] - -[[package]] -name = "marshmallow" -version = "3.21.3" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -optional = false -python-versions = ">=3.8" -files = [ - {file = "marshmallow-3.21.3-py3-none-any.whl", hash = "sha256:86ce7fb914aa865001a4b2092c4c2872d13bc347f3d42673272cabfdbad386f1"}, - {file = "marshmallow-3.21.3.tar.gz", hash = "sha256:4f57c5e050a54d66361e826f94fba213eb10b67b2fdb02c3e0343ce207ba1662"}, -] - -[package.dependencies] -packaging = ">=17.0" - -[package.extras] -dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"] -docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.3.7)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"] -tests = ["pytest", "pytz", "simplejson"] - -[[package]] -name = "matplotlib-inline" -version = "0.1.7" -description = "Inline Matplotlib backend for Jupyter" -optional = false -python-versions = ">=3.8" -files = [ - {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, - {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, -] - -[package.dependencies] -traitlets = "*" - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "microsoft-kiota-abstractions" -version = "1.3.3" -description = "Core abstractions for kiota generated libraries in Python" -optional = false -python-versions = "*" -files = [ - {file = "microsoft_kiota_abstractions-1.3.3-py2.py3-none-any.whl", hash = "sha256:deced0b01249459426d4ed45c8ab34e19250e514d4d05ce84c08893058ae06a1"}, - {file = "microsoft_kiota_abstractions-1.3.3.tar.gz", hash = "sha256:3cc01832a2e6dc6094c4e1abf7cbef3849a87d818a3b9193ad6c83a9f88e14ff"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.19.0" -opentelemetry-sdk = ">=1.19.0" -std-uritemplate = ">=0.0.38" - -[[package]] -name = "microsoft-kiota-authentication-azure" -version = "1.0.0" -description = "Authentication provider for Kiota using Azure Identity" -optional = false -python-versions = "*" -files = [ - {file = "microsoft_kiota_authentication_azure-1.0.0-py2.py3-none-any.whl", hash = "sha256:289fe002951ae661415a6d3fa7c422c096b739165acb32d786316988120a1b27"}, - {file = "microsoft_kiota_authentication_azure-1.0.0.tar.gz", hash = "sha256:752304f8d94b884cfec12583dd763ec0478805c7f80b29344e78c6d55a97bd01"}, -] - -[package.dependencies] -aiohttp = ">=3.8.0" -azure-core = ">=1.21.1" -microsoft-kiota-abstractions = ">=1.0.0,<2.0.0" -opentelemetry-api = ">=1.20.0" -opentelemetry-sdk = ">=1.20.0" - -[[package]] -name = "microsoft-kiota-http" -version = "1.3.3" -description = "Kiota http request adapter implementation for httpx library" -optional = false -python-versions = "*" -files = [ - {file = "microsoft_kiota_http-1.3.3-py2.py3-none-any.whl", hash = "sha256:21109a34140bf42e18855b7cf983939b891ae30739f21a9ce045c3a715f325fd"}, - {file = "microsoft_kiota_http-1.3.3.tar.gz", hash = "sha256:0b40f37c6c158c2e5b2dffa963a7fc342d368c1a64b8cca08631ba19d0ff94a9"}, -] - -[package.dependencies] -httpx = {version = ">=0.23.0", extras = ["http2"]} -microsoft-kiota_abstractions = ">=1.0.0,<2.0.0" -opentelemetry-api = ">=1.20.0" -opentelemetry-sdk = ">=1.20.0" - -[[package]] -name = "microsoft-kiota-serialization-form" -version = "0.1.0" -description = "Implementation of Kiota Serialization Interfaces for URI-Form encoded serialization" -optional = false -python-versions = "*" -files = [ - {file = "microsoft_kiota_serialization_form-0.1.0-py2.py3-none-any.whl", hash = "sha256:5bc76fb2fc67d7c1f878f876d252ea814e4fc38df505099b9b86de52d974380a"}, - {file = "microsoft_kiota_serialization_form-0.1.0.tar.gz", hash = "sha256:663ece0cb1a41fe9ddfc9195aa3f15f219e14d2a1ee51e98c53ad8d795b2785d"}, -] - -[package.dependencies] -microsoft-kiota_abstractions = ">=1.0.0,<2.0.0" -pendulum = ">=3.0.0" - -[[package]] -name = "microsoft-kiota-serialization-json" -version = "1.3.0" -description = "Implementation of Kiota Serialization interfaces for JSON" -optional = false -python-versions = "*" -files = [ - {file = "microsoft_kiota_serialization_json-1.3.0-py2.py3-none-any.whl", hash = "sha256:fbf82835d8b77ef21b496aa711a512fe4494fa94dfe88f7fd014dffe33778e20"}, - {file = "microsoft_kiota_serialization_json-1.3.0.tar.gz", hash = "sha256:235b680e6eb646479ffb7b59d2a6f0216c4f7e1c2ff1219fd4d59e898fa6b124"}, -] - -[package.dependencies] -microsoft-kiota_abstractions = ">=1.0.0,<2.0.0" -pendulum = ">=3.0.0b1" - -[[package]] -name = "microsoft-kiota-serialization-multipart" -version = "0.1.0" -description = "Implementation of Kiota Serialization Interfaces for Multipart serialization" -optional = false -python-versions = "*" -files = [ - {file = "microsoft_kiota_serialization_multipart-0.1.0-py2.py3-none-any.whl", hash = "sha256:ef183902e77807806b8a181cdde53ba5bc04c6c9bdb2f7d80f8bad5d720e0015"}, - {file = "microsoft_kiota_serialization_multipart-0.1.0.tar.gz", hash = "sha256:14e89e92582e6630ddbc70ac67b70bf189dacbfc41a96d3e1d10339e86c8dde5"}, -] - -[package.dependencies] -microsoft-kiota_abstractions = ">=1.0.0,<2.0.0" - -[[package]] -name = "microsoft-kiota-serialization-text" -version = "1.0.0" -description = "Implementation of Kiota Serialization interfaces for text/plain" -optional = false -python-versions = "*" -files = [ - {file = "microsoft_kiota_serialization_text-1.0.0-py2.py3-none-any.whl", hash = "sha256:1d3789e012b603e059a36cc675d1fd08cb81e0dde423d970c0af2eabce9c0d43"}, - {file = "microsoft_kiota_serialization_text-1.0.0.tar.gz", hash = "sha256:c3dd3f409b1c4f4963bd1e41d51b65f7e53e852130bb441d79b77dad88ee76ed"}, -] - -[package.dependencies] -microsoft-kiota_abstractions = ">=1.0.0,<2.0.0" -python-dateutil = ">=2.8.2" - -[[package]] -name = "milvus" -version = "2.3.5" -description = "Embeded Milvus" -optional = false -python-versions = ">=3.6" -files = [ - {file = "milvus-2.3.5-py3-none-macosx_12_0_arm64.whl", hash = "sha256:328d2ba24fb04a595f47ab226abf5565691bfe242beb88e61b31326d0416bf1a"}, - {file = "milvus-2.3.5-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:e35a8d6397da1f0f685d0f55afad8654296ff3b3aea296439e53ce9980d1ad22"}, - {file = "milvus-2.3.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:69515a0630ce29fd10e101fa442afea8ca1387b93a456cd9bd41fdf3deb93d04"}, -] - -[package.extras] -client = ["pymilvus (>=2.3.0b1,<2.4.0)"] - -[[package]] -name = "milvus-lite" -version = "2.4.9" -description = "A lightweight version of Milvus wrapped with Python." -optional = false -python-versions = ">=3.7" -files = [ - {file = "milvus_lite-2.4.9-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d3e617b3d68c09ad656d54bc3d8cc4ef6ef56c54015e1563d4fe4bcec6b7c90a"}, - {file = "milvus_lite-2.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6e7029282d6829b277ebb92f64e2370be72b938e34770e1eb649346bda5d1d7f"}, - {file = "milvus_lite-2.4.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9b8e991e4e433596f6a399a165c1a506f823ec9133332e03d7f8a114bff4550d"}, - {file = "milvus_lite-2.4.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:7f53e674602101cfbcf0a4a59d19eaa139dfd5580639f3040ad73d901f24fc0b"}, -] - -[package.dependencies] -tqdm = "*" - -[[package]] -name = "mistralai" -version = "0.4.2" -description = "" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "mistralai-0.4.2-py3-none-any.whl", hash = "sha256:63c98eea139585f0a3b2c4c6c09c453738bac3958055e6f2362d3866e96b0168"}, - {file = "mistralai-0.4.2.tar.gz", hash = "sha256:5eb656710517168ae053f9847b0bb7f617eda07f1f93f946ad6c91a4d407fd93"}, -] - -[package.dependencies] -httpx = ">=0.25,<1" -orjson = ">=3.9.10,<3.11" -pydantic = ">=2.5.2,<3" - -[[package]] -name = "mistune" -version = "3.0.2" -description = "A sane and fast Markdown parser with useful plugins and renderers" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"}, - {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, -] - -[[package]] -name = "mmh3" -version = "4.1.0" -description = "Python extension for MurmurHash (MurmurHash3), a set of fast and robust hash functions." -optional = false -python-versions = "*" -files = [ - {file = "mmh3-4.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:be5ac76a8b0cd8095784e51e4c1c9c318c19edcd1709a06eb14979c8d850c31a"}, - {file = "mmh3-4.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98a49121afdfab67cd80e912b36404139d7deceb6773a83620137aaa0da5714c"}, - {file = "mmh3-4.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5259ac0535874366e7d1a5423ef746e0d36a9e3c14509ce6511614bdc5a7ef5b"}, - {file = "mmh3-4.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5950827ca0453a2be357696da509ab39646044e3fa15cad364eb65d78797437"}, - {file = "mmh3-4.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dd0f652ae99585b9dd26de458e5f08571522f0402155809fd1dc8852a613a39"}, - {file = "mmh3-4.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99d25548070942fab1e4a6f04d1626d67e66d0b81ed6571ecfca511f3edf07e6"}, - {file = "mmh3-4.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53db8d9bad3cb66c8f35cbc894f336273f63489ce4ac416634932e3cbe79eb5b"}, - {file = "mmh3-4.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75da0f615eb55295a437264cc0b736753f830b09d102aa4c2a7d719bc445ec05"}, - {file = "mmh3-4.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b926b07fd678ea84b3a2afc1fa22ce50aeb627839c44382f3d0291e945621e1a"}, - {file = "mmh3-4.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c5b053334f9b0af8559d6da9dc72cef0a65b325ebb3e630c680012323c950bb6"}, - {file = "mmh3-4.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bf33dc43cd6de2cb86e0aa73a1cc6530f557854bbbe5d59f41ef6de2e353d7b"}, - {file = "mmh3-4.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fa7eacd2b830727ba3dd65a365bed8a5c992ecd0c8348cf39a05cc77d22f4970"}, - {file = "mmh3-4.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:42dfd6742b9e3eec599f85270617debfa0bbb913c545bb980c8a4fa7b2d047da"}, - {file = "mmh3-4.1.0-cp310-cp310-win32.whl", hash = "sha256:2974ad343f0d39dcc88e93ee6afa96cedc35a9883bc067febd7ff736e207fa47"}, - {file = "mmh3-4.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:74699a8984ded645c1a24d6078351a056f5a5f1fe5838870412a68ac5e28d865"}, - {file = "mmh3-4.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f0dc874cedc23d46fc488a987faa6ad08ffa79e44fb08e3cd4d4cf2877c00a00"}, - {file = "mmh3-4.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3280a463855b0eae64b681cd5b9ddd9464b73f81151e87bb7c91a811d25619e6"}, - {file = "mmh3-4.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:97ac57c6c3301769e757d444fa7c973ceb002cb66534b39cbab5e38de61cd896"}, - {file = "mmh3-4.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b6502cdb4dbd880244818ab363c8770a48cdccecf6d729ade0241b736b5ec0"}, - {file = "mmh3-4.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52ba2da04671a9621580ddabf72f06f0e72c1c9c3b7b608849b58b11080d8f14"}, - {file = "mmh3-4.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a5fef4c4ecc782e6e43fbeab09cff1bac82c998a1773d3a5ee6a3605cde343e"}, - {file = "mmh3-4.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5135358a7e00991f73b88cdc8eda5203bf9de22120d10a834c5761dbeb07dd13"}, - {file = "mmh3-4.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cff9ae76a54f7c6fe0167c9c4028c12c1f6de52d68a31d11b6790bb2ae685560"}, - {file = "mmh3-4.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f02576a4d106d7830ca90278868bf0983554dd69183b7bbe09f2fcd51cf54f"}, - {file = "mmh3-4.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:073d57425a23721730d3ff5485e2da489dd3c90b04e86243dd7211f889898106"}, - {file = "mmh3-4.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:71e32ddec7f573a1a0feb8d2cf2af474c50ec21e7a8263026e8d3b4b629805db"}, - {file = "mmh3-4.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7cbb20b29d57e76a58b40fd8b13a9130db495a12d678d651b459bf61c0714cea"}, - {file = "mmh3-4.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:a42ad267e131d7847076bb7e31050f6c4378cd38e8f1bf7a0edd32f30224d5c9"}, - {file = "mmh3-4.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4a013979fc9390abadc445ea2527426a0e7a4495c19b74589204f9b71bcaafeb"}, - {file = "mmh3-4.1.0-cp311-cp311-win32.whl", hash = "sha256:1d3b1cdad7c71b7b88966301789a478af142bddcb3a2bee563f7a7d40519a00f"}, - {file = "mmh3-4.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0dc6dc32eb03727467da8e17deffe004fbb65e8b5ee2b502d36250d7a3f4e2ec"}, - {file = "mmh3-4.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9ae3a5c1b32dda121c7dc26f9597ef7b01b4c56a98319a7fe86c35b8bc459ae6"}, - {file = "mmh3-4.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0033d60c7939168ef65ddc396611077a7268bde024f2c23bdc283a19123f9e9c"}, - {file = "mmh3-4.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d6af3e2287644b2b08b5924ed3a88c97b87b44ad08e79ca9f93d3470a54a41c5"}, - {file = "mmh3-4.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d82eb4defa245e02bb0b0dc4f1e7ee284f8d212633389c91f7fba99ba993f0a2"}, - {file = "mmh3-4.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba245e94b8d54765e14c2d7b6214e832557e7856d5183bc522e17884cab2f45d"}, - {file = "mmh3-4.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb04e2feeabaad6231e89cd43b3d01a4403579aa792c9ab6fdeef45cc58d4ec0"}, - {file = "mmh3-4.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e3b1a27def545ce11e36158ba5d5390cdbc300cfe456a942cc89d649cf7e3b2"}, - {file = "mmh3-4.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce0ab79ff736d7044e5e9b3bfe73958a55f79a4ae672e6213e92492ad5e734d5"}, - {file = "mmh3-4.1.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b02268be6e0a8eeb8a924d7db85f28e47344f35c438c1e149878bb1c47b1cd3"}, - {file = "mmh3-4.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:deb887f5fcdaf57cf646b1e062d56b06ef2f23421c80885fce18b37143cba828"}, - {file = "mmh3-4.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99dd564e9e2b512eb117bd0cbf0f79a50c45d961c2a02402787d581cec5448d5"}, - {file = "mmh3-4.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:08373082dfaa38fe97aa78753d1efd21a1969e51079056ff552e687764eafdfe"}, - {file = "mmh3-4.1.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:54b9c6a2ea571b714e4fe28d3e4e2db37abfd03c787a58074ea21ee9a8fd1740"}, - {file = "mmh3-4.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a7b1edf24c69e3513f879722b97ca85e52f9032f24a52284746877f6a7304086"}, - {file = "mmh3-4.1.0-cp312-cp312-win32.whl", hash = "sha256:411da64b951f635e1e2284b71d81a5a83580cea24994b328f8910d40bed67276"}, - {file = "mmh3-4.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bebc3ecb6ba18292e3d40c8712482b4477abd6981c2ebf0e60869bd90f8ac3a9"}, - {file = "mmh3-4.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:168473dd608ade6a8d2ba069600b35199a9af837d96177d3088ca91f2b3798e3"}, - {file = "mmh3-4.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:372f4b7e1dcde175507640679a2a8790185bb71f3640fc28a4690f73da986a3b"}, - {file = "mmh3-4.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:438584b97f6fe13e944faf590c90fc127682b57ae969f73334040d9fa1c7ffa5"}, - {file = "mmh3-4.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6e27931b232fc676675fac8641c6ec6b596daa64d82170e8597f5a5b8bdcd3b6"}, - {file = "mmh3-4.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:571a92bad859d7b0330e47cfd1850b76c39b615a8d8e7aa5853c1f971fd0c4b1"}, - {file = "mmh3-4.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a69d6afe3190fa08f9e3a58e5145549f71f1f3fff27bd0800313426929c7068"}, - {file = "mmh3-4.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afb127be0be946b7630220908dbea0cee0d9d3c583fa9114a07156f98566dc28"}, - {file = "mmh3-4.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:940d86522f36348ef1a494cbf7248ab3f4a1638b84b59e6c9e90408bd11ad729"}, - {file = "mmh3-4.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3dcccc4935686619a8e3d1f7b6e97e3bd89a4a796247930ee97d35ea1a39341"}, - {file = "mmh3-4.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01bb9b90d61854dfc2407c5e5192bfb47222d74f29d140cb2dd2a69f2353f7cc"}, - {file = "mmh3-4.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:bcb1b8b951a2c0b0fb8a5426c62a22557e2ffc52539e0a7cc46eb667b5d606a9"}, - {file = "mmh3-4.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6477a05d5e5ab3168e82e8b106e316210ac954134f46ec529356607900aea82a"}, - {file = "mmh3-4.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:da5892287e5bea6977364b15712a2573c16d134bc5fdcdd4cf460006cf849278"}, - {file = "mmh3-4.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:99180d7fd2327a6fffbaff270f760576839dc6ee66d045fa3a450f3490fda7f5"}, - {file = "mmh3-4.1.0-cp38-cp38-win32.whl", hash = "sha256:9b0d4f3949913a9f9a8fb1bb4cc6ecd52879730aab5ff8c5a3d8f5b593594b73"}, - {file = "mmh3-4.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:598c352da1d945108aee0c3c3cfdd0e9b3edef74108f53b49d481d3990402169"}, - {file = "mmh3-4.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:475d6d1445dd080f18f0f766277e1237fa2914e5fe3307a3b2a3044f30892103"}, - {file = "mmh3-4.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5ca07c41e6a2880991431ac717c2a049056fff497651a76e26fc22224e8b5732"}, - {file = "mmh3-4.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ebe052fef4bbe30c0548d12ee46d09f1b69035ca5208a7075e55adfe091be44"}, - {file = "mmh3-4.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaefd42e85afb70f2b855a011f7b4d8a3c7e19c3f2681fa13118e4d8627378c5"}, - {file = "mmh3-4.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0ae43caae5a47afe1b63a1ae3f0986dde54b5fb2d6c29786adbfb8edc9edfb"}, - {file = "mmh3-4.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6218666f74c8c013c221e7f5f8a693ac9cf68e5ac9a03f2373b32d77c48904de"}, - {file = "mmh3-4.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac59294a536ba447b5037f62d8367d7d93b696f80671c2c45645fa9f1109413c"}, - {file = "mmh3-4.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:086844830fcd1e5c84fec7017ea1ee8491487cfc877847d96f86f68881569d2e"}, - {file = "mmh3-4.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e42b38fad664f56f77f6fbca22d08450f2464baa68acdbf24841bf900eb98e87"}, - {file = "mmh3-4.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d08b790a63a9a1cde3b5d7d733ed97d4eb884bfbc92f075a091652d6bfd7709a"}, - {file = "mmh3-4.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:73ea4cc55e8aea28c86799ecacebca09e5f86500414870a8abaedfcbaf74d288"}, - {file = "mmh3-4.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:f90938ff137130e47bcec8dc1f4ceb02f10178c766e2ef58a9f657ff1f62d124"}, - {file = "mmh3-4.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:aa1f13e94b8631c8cd53259250556edcf1de71738936b60febba95750d9632bd"}, - {file = "mmh3-4.1.0-cp39-cp39-win32.whl", hash = "sha256:a3b680b471c181490cf82da2142029edb4298e1bdfcb67c76922dedef789868d"}, - {file = "mmh3-4.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:fefef92e9c544a8dbc08f77a8d1b6d48006a750c4375bbcd5ff8199d761e263b"}, - {file = "mmh3-4.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:8e2c1f6a2b41723a4f82bd5a762a777836d29d664fc0095f17910bea0adfd4a6"}, - {file = "mmh3-4.1.0.tar.gz", hash = "sha256:a1cf25348b9acd229dda464a094d6170f47d2850a1fcb762a3b6172d2ce6ca4a"}, -] - -[package.extras] -test = ["mypy (>=1.0)", "pytest (>=7.0.0)"] - -[[package]] -name = "monotonic" -version = "1.6" -description = "An implementation of time.monotonic() for Python 2 & < 3.3" -optional = false -python-versions = "*" -files = [ - {file = "monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c"}, - {file = "monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7"}, -] - -[[package]] -name = "more-itertools" -version = "10.4.0" -description = "More routines for operating on iterables, beyond itertools" -optional = false -python-versions = ">=3.8" -files = [ - {file = "more-itertools-10.4.0.tar.gz", hash = "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923"}, - {file = "more_itertools-10.4.0-py3-none-any.whl", hash = "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27"}, -] - -[[package]] -name = "motor" -version = "3.5.1" -description = "Non-blocking MongoDB driver for Tornado or asyncio" -optional = false -python-versions = ">=3.8" -files = [ - {file = "motor-3.5.1-py3-none-any.whl", hash = "sha256:f95a9ea0f011464235e0bd72910baa291db3a6009e617ac27b82f57885abafb8"}, - {file = "motor-3.5.1.tar.gz", hash = "sha256:1622bd7b39c3e6375607c14736f6e1d498128eadf6f5f93f8786cf17d37062ac"}, -] - -[package.dependencies] -pymongo = ">=4.5,<5" - -[package.extras] -aws = ["pymongo[aws] (>=4.5,<5)"] -docs = ["aiohttp", "readthedocs-sphinx-search (>=0.3,<1.0)", "sphinx (>=5.3,<8)", "sphinx-rtd-theme (>=2,<3)", "tornado"] -encryption = ["pymongo[encryption] (>=4.5,<5)"] -gssapi = ["pymongo[gssapi] (>=4.5,<5)"] -ocsp = ["pymongo[ocsp] (>=4.5,<5)"] -snappy = ["pymongo[snappy] (>=4.5,<5)"] -test = ["aiohttp (!=3.8.6)", "mockupdb", "pymongo[encryption] (>=4.5,<5)", "pytest (>=7)", "tornado (>=5)"] -zstd = ["pymongo[zstd] (>=4.5,<5)"] - -[[package]] -name = "mpmath" -version = "1.3.0" -description = "Python library for arbitrary-precision floating-point arithmetic" -optional = false -python-versions = "*" -files = [ - {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, - {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, -] - -[package.extras] -develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] -docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4)"] -tests = ["pytest (>=4.6)"] - -[[package]] -name = "msal" -version = "1.30.0" -description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." -optional = false -python-versions = ">=3.7" -files = [ - {file = "msal-1.30.0-py3-none-any.whl", hash = "sha256:423872177410cb61683566dc3932db7a76f661a5d2f6f52f02a047f101e1c1de"}, - {file = "msal-1.30.0.tar.gz", hash = "sha256:b4bf00850092e465157d814efa24a18f788284c9a479491024d62903085ea2fb"}, -] - -[package.dependencies] -cryptography = ">=2.5,<45" -PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} -requests = ">=2.0.0,<3" - -[package.extras] -broker = ["pymsalruntime (>=0.13.2,<0.17)"] - -[[package]] -name = "msal-extensions" -version = "1.2.0" -description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." -optional = false -python-versions = ">=3.7" -files = [ - {file = "msal_extensions-1.2.0-py3-none-any.whl", hash = "sha256:cf5ba83a2113fa6dc011a254a72f1c223c88d7dfad74cc30617c4679a417704d"}, - {file = "msal_extensions-1.2.0.tar.gz", hash = "sha256:6f41b320bfd2933d631a215c91ca0dd3e67d84bd1a2f50ce917d5874ec646bef"}, -] - -[package.dependencies] -msal = ">=1.29,<2" -portalocker = ">=1.4,<3" - -[[package]] -name = "msgraph-core" -version = "1.1.2" -description = "Core component of the Microsoft Graph Python SDK" -optional = false -python-versions = ">=3.8" -files = [ - {file = "msgraph_core-1.1.2-py3-none-any.whl", hash = "sha256:ed0695275d66914994a6ff71e7d71736ee4c4db3548a1021b2dd3a9605247def"}, - {file = "msgraph_core-1.1.2.tar.gz", hash = "sha256:c533cad1a23980487a4aa229dc5d9b00975fc6590e157e9f51046c6e80349288"}, -] - -[package.dependencies] -httpx = {version = ">=0.23.0", extras = ["http2"]} -microsoft-kiota-abstractions = ">=1.0.0,<2.0.0" -microsoft-kiota-authentication-azure = ">=1.0.0,<2.0.0" -microsoft-kiota-http = ">=1.0.0,<2.0.0" - -[package.extras] -dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] - -[[package]] -name = "msgraph-sdk" -version = "1.5.4" -description = "The Microsoft Graph Python SDK" -optional = false -python-versions = ">=3.8" -files = [ - {file = "msgraph_sdk-1.5.4-py3-none-any.whl", hash = "sha256:9ea349f30cc4a03edb587e26554c7a4839a38c2ef30d4b5396882fd2be82dcac"}, - {file = "msgraph_sdk-1.5.4.tar.gz", hash = "sha256:b0e146328d136d1db175938d8fc901f3bb32acf3ea6fe93c0dc7c5a0abc45e39"}, -] - -[package.dependencies] -azure-identity = ">=1.12.0" -microsoft-kiota-abstractions = ">=1.3.0,<2.0.0" -microsoft-kiota-authentication-azure = ">=1.0.0,<2.0.0" -microsoft-kiota-http = ">=1.0.0,<2.0.0" -microsoft-kiota-serialization-form = ">=0.1.0" -microsoft-kiota-serialization-json = ">=1.3.0,<2.0.0" -microsoft-kiota-serialization-multipart = ">=0.1.0" -microsoft-kiota-serialization-text = ">=1.0.0,<2.0.0" -msgraph_core = ">=1.0.0" - -[package.extras] -dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] - -[[package]] -name = "multidict" -version = "6.0.5" -description = "multidict implementation" -optional = false -python-versions = ">=3.7" -files = [ - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, - {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, - {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, - {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, - {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, - {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, - {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, - {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, - {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, - {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, - {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, - {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, - {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, - {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, - {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, - {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, -] - -[[package]] -name = "mypy" -version = "1.11.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, - {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, - {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, - {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, - {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, - {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, - {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, - {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, - {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, - {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, - {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, - {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, - {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, - {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, - {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, - {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, - {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, - {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, - {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, - {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, - {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, - {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, - {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, - {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, - {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, - {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, - {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "nbclient" -version = "0.10.0" -description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"}, - {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"}, -] - -[package.dependencies] -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -nbformat = ">=5.1" -traitlets = ">=5.4" - -[package.extras] -dev = ["pre-commit"] -docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] -test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] - -[[package]] -name = "nbconvert" -version = "7.16.4" -description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." -optional = false -python-versions = ">=3.8" -files = [ - {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"}, - {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"}, -] - -[package.dependencies] -beautifulsoup4 = "*" -bleach = "!=5.0.0" -defusedxml = "*" -jinja2 = ">=3.0" -jupyter-core = ">=4.7" -jupyterlab-pygments = "*" -markupsafe = ">=2.0" -mistune = ">=2.0.3,<4" -nbclient = ">=0.5.0" -nbformat = ">=5.7" -packaging = "*" -pandocfilters = ">=1.4.1" -pygments = ">=2.4.1" -tinycss2 = "*" -traitlets = ">=5.1" - -[package.extras] -all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"] -docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] -qtpdf = ["pyqtwebengine (>=5.15)"] -qtpng = ["pyqtwebengine (>=5.15)"] -serve = ["tornado (>=6.1)"] -test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"] -webpdf = ["playwright"] - -[[package]] -name = "nbformat" -version = "5.10.4" -description = "The Jupyter Notebook format" -optional = false -python-versions = ">=3.8" -files = [ - {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, - {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, -] - -[package.dependencies] -fastjsonschema = ">=2.15" -jsonschema = ">=2.6" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -traitlets = ">=5.1" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["pep440", "pre-commit", "pytest", "testpath"] - -[[package]] -name = "nest-asyncio" -version = "1.6.0" -description = "Patch asyncio to allow nested event loops" -optional = false -python-versions = ">=3.5" -files = [ - {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, - {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, -] - -[[package]] -name = "networkx" -version = "3.3" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.10" -files = [ - {file = "networkx-3.3-py3-none-any.whl", hash = "sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2"}, - {file = "networkx-3.3.tar.gz", hash = "sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9"}, -] - -[package.extras] -default = ["matplotlib (>=3.6)", "numpy (>=1.23)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["myst-nb (>=1.0)", "numpydoc (>=1.7)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=2.0)", "pygraphviz (>=1.12)", "sympy (>=1.10)"] -test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] - -[[package]] -name = "nodeenv" -version = "1.9.1" -description = "Node.js virtual environment builder" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, -] - -[[package]] -name = "numpy" -version = "1.26.4" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, - {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, - {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, - {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, - {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, - {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, - {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, - {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, - {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, - {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, - {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, -] - -[[package]] -name = "nvidia-cublas-cu12" -version = "12.1.3.1" -description = "CUBLAS native runtime libraries" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728"}, - {file = "nvidia_cublas_cu12-12.1.3.1-py3-none-win_amd64.whl", hash = "sha256:2b964d60e8cf11b5e1073d179d85fa340c120e99b3067558f3cf98dd69d02906"}, -] - -[[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.1.105" -description = "CUDA profiling tools runtime libs." -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e"}, - {file = "nvidia_cuda_cupti_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:bea8236d13a0ac7190bd2919c3e8e6ce1e402104276e6f9694479e48bb0eb2a4"}, -] - -[[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.1.105" -description = "NVRTC native runtime libraries" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2"}, - {file = "nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:0a98a522d9ff138b96c010a65e145dc1b4850e9ecb75a0172371793752fd46ed"}, -] - -[[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.1.105" -description = "CUDA Runtime native Libraries" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40"}, - {file = "nvidia_cuda_runtime_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:dfb46ef84d73fababab44cf03e3b83f80700d27ca300e537f85f636fac474344"}, -] - -[[package]] -name = "nvidia-cudnn-cu12" -version = "8.9.2.26" -description = "cuDNN runtime libraries" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9"}, -] - -[package.dependencies] -nvidia-cublas-cu12 = "*" - -[[package]] -name = "nvidia-cufft-cu12" -version = "11.0.2.54" -description = "CUFFT native runtime libraries" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56"}, - {file = "nvidia_cufft_cu12-11.0.2.54-py3-none-win_amd64.whl", hash = "sha256:d9ac353f78ff89951da4af698f80870b1534ed69993f10a4cf1d96f21357e253"}, -] - -[[package]] -name = "nvidia-curand-cu12" -version = "10.3.2.106" -description = "CURAND native runtime libraries" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0"}, - {file = "nvidia_curand_cu12-10.3.2.106-py3-none-win_amd64.whl", hash = "sha256:75b6b0c574c0037839121317e17fd01f8a69fd2ef8e25853d826fec30bdba74a"}, -] - -[[package]] -name = "nvidia-cusolver-cu12" -version = "11.4.5.107" -description = "CUDA solver native runtime libraries" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd"}, - {file = "nvidia_cusolver_cu12-11.4.5.107-py3-none-win_amd64.whl", hash = "sha256:74e0c3a24c78612192a74fcd90dd117f1cf21dea4822e66d89e8ea80e3cd2da5"}, -] - -[package.dependencies] -nvidia-cublas-cu12 = "*" -nvidia-cusparse-cu12 = "*" -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-cusparse-cu12" -version = "12.1.0.106" -description = "CUSPARSE native runtime libraries" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c"}, - {file = "nvidia_cusparse_cu12-12.1.0.106-py3-none-win_amd64.whl", hash = "sha256:b798237e81b9719373e8fae8d4f091b70a0cf09d9d85c95a557e11df2d8e9a5a"}, -] - -[package.dependencies] -nvidia-nvjitlink-cu12 = "*" - -[[package]] -name = "nvidia-nccl-cu12" -version = "2.19.3" -description = "NVIDIA Collective Communication Library (NCCL) Runtime" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_nccl_cu12-2.19.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:a9734707a2c96443331c1e48c717024aa6678a0e2a4cb66b2c364d18cee6b48d"}, -] - -[[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.6.20" -description = "Nvidia JIT LTO Library" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_nvjitlink_cu12-12.6.20-py3-none-manylinux2014_aarch64.whl", hash = "sha256:84fb38465a5bc7c70cbc320cfd0963eb302ee25a5e939e9f512bbba55b6072fb"}, - {file = "nvidia_nvjitlink_cu12-12.6.20-py3-none-manylinux2014_x86_64.whl", hash = "sha256:562ab97ea2c23164823b2a89cb328d01d45cb99634b8c65fe7cd60d14562bd79"}, - {file = "nvidia_nvjitlink_cu12-12.6.20-py3-none-win_amd64.whl", hash = "sha256:ed3c43a17f37b0c922a919203d2d36cbef24d41cc3e6b625182f8b58203644f6"}, -] - -[[package]] -name = "nvidia-nvtx-cu12" -version = "12.1.105" -description = "NVIDIA Tools Extension" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5"}, - {file = "nvidia_nvtx_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:65f4d98982b31b60026e0e6de73fbdfc09d08a96f4656dd3665ca616a11e1e82"}, -] - -[[package]] -name = "oauthlib" -version = "3.2.2" -description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" -optional = false -python-versions = ">=3.6" -files = [ - {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, - {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, -] - -[package.extras] -rsa = ["cryptography (>=3.0.0)"] -signals = ["blinker (>=1.4.0)"] -signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] - -[[package]] -name = "ollama" -version = "0.2.1" -description = "The official Python client for Ollama." -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "ollama-0.2.1-py3-none-any.whl", hash = "sha256:b6e2414921c94f573a903d1069d682ba2fb2607070ea9e19ca4a7872f2a460ec"}, - {file = "ollama-0.2.1.tar.gz", hash = "sha256:fa316baa9a81eac3beb4affb0a17deb3008fdd6ed05b123c26306cfbe4c349b6"}, -] - -[package.dependencies] -httpx = ">=0.27.0,<0.28.0" - -[[package]] -name = "onnxruntime" -version = "1.18.1" -description = "ONNX Runtime is a runtime accelerator for Machine Learning models" -optional = false -python-versions = "*" -files = [ - {file = "onnxruntime-1.18.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ef7683312393d4ba04252f1b287d964bd67d5e6048b94d2da3643986c74d80"}, - {file = "onnxruntime-1.18.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc706eb1df06ddf55776e15a30519fb15dda7697f987a2bbda4962845e3cec05"}, - {file = "onnxruntime-1.18.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7de69f5ced2a263531923fa68bbec52a56e793b802fcd81a03487b5e292bc3a"}, - {file = "onnxruntime-1.18.1-cp310-cp310-win32.whl", hash = "sha256:221e5b16173926e6c7de2cd437764492aa12b6811f45abd37024e7cf2ae5d7e3"}, - {file = "onnxruntime-1.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:75211b619275199c861ee94d317243b8a0fcde6032e5a80e1aa9ded8ab4c6060"}, - {file = "onnxruntime-1.18.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:f26582882f2dc581b809cfa41a125ba71ad9e715738ec6402418df356969774a"}, - {file = "onnxruntime-1.18.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef36f3a8b768506d02be349ac303fd95d92813ba3ba70304d40c3cd5c25d6a4c"}, - {file = "onnxruntime-1.18.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:170e711393e0618efa8ed27b59b9de0ee2383bd2a1f93622a97006a5ad48e434"}, - {file = "onnxruntime-1.18.1-cp311-cp311-win32.whl", hash = "sha256:9b6a33419b6949ea34e0dc009bc4470e550155b6da644571ecace4b198b0d88f"}, - {file = "onnxruntime-1.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c1380a9f1b7788da742c759b6a02ba771fe1ce620519b2b07309decbd1a2fe1"}, - {file = "onnxruntime-1.18.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:31bd57a55e3f983b598675dfc7e5d6f0877b70ec9864b3cc3c3e1923d0a01919"}, - {file = "onnxruntime-1.18.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9e03c4ba9f734500691a4d7d5b381cd71ee2f3ce80a1154ac8f7aed99d1ecaa"}, - {file = "onnxruntime-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:781aa9873640f5df24524f96f6070b8c550c66cb6af35710fd9f92a20b4bfbf6"}, - {file = "onnxruntime-1.18.1-cp312-cp312-win32.whl", hash = "sha256:3a2d9ab6254ca62adbb448222e630dc6883210f718065063518c8f93a32432be"}, - {file = "onnxruntime-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:ad93c560b1c38c27c0275ffd15cd7f45b3ad3fc96653c09ce2931179982ff204"}, - {file = "onnxruntime-1.18.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:3b55dc9d3c67626388958a3eb7ad87eb7c70f75cb0f7ff4908d27b8b42f2475c"}, - {file = "onnxruntime-1.18.1-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f80dbcfb6763cc0177a31168b29b4bd7662545b99a19e211de8c734b657e0669"}, - {file = "onnxruntime-1.18.1-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1ff2c61a16d6c8631796c54139bafea41ee7736077a0fc64ee8ae59432f5c58"}, - {file = "onnxruntime-1.18.1-cp38-cp38-win32.whl", hash = "sha256:219855bd272fe0c667b850bf1a1a5a02499269a70d59c48e6f27f9c8bcb25d02"}, - {file = "onnxruntime-1.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:afdf16aa607eb9a2c60d5ca2d5abf9f448e90c345b6b94c3ed14f4fb7e6a2d07"}, - {file = "onnxruntime-1.18.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:128df253ade673e60cea0955ec9d0e89617443a6d9ce47c2d79eb3f72a3be3de"}, - {file = "onnxruntime-1.18.1-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9839491e77e5c5a175cab3621e184d5a88925ee297ff4c311b68897197f4cde9"}, - {file = "onnxruntime-1.18.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad3187c1faff3ac15f7f0e7373ef4788c582cafa655a80fdbb33eaec88976c66"}, - {file = "onnxruntime-1.18.1-cp39-cp39-win32.whl", hash = "sha256:34657c78aa4e0b5145f9188b550ded3af626651b15017bf43d280d7e23dbf195"}, - {file = "onnxruntime-1.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:9c14fd97c3ddfa97da5feef595e2c73f14c2d0ec1d4ecbea99c8d96603c89589"}, -] - -[package.dependencies] -coloredlogs = "*" -flatbuffers = "*" -numpy = ">=1.21.6,<2.0" -packaging = "*" -protobuf = "*" -sympy = "*" - -[[package]] -name = "openai" -version = "1.41.1" -description = "The official Python library for the openai API" -optional = false -python-versions = ">=3.7.1" -files = [ - {file = "openai-1.41.1-py3-none-any.whl", hash = "sha256:56fb04105263f79559aff3ceea2e1dd16f8c5385e8238cb66cf0e6888fa8bfcf"}, - {file = "openai-1.41.1.tar.gz", hash = "sha256:e38e376efd91e0d4db071e2a6517b6b4cac1c2a6fd63efdc5ec6be10c5967c1b"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -jiter = ">=0.4.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -tqdm = ">4" -typing-extensions = ">=4.11,<5" - -[package.extras] -datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] - -[[package]] -name = "openapi-core" -version = "0.19.2" -description = "client-side and server-side support for the OpenAPI Specification v3" -optional = false -python-versions = "<4.0.0,>=3.8.0" -files = [ - {file = "openapi_core-0.19.2-py3-none-any.whl", hash = "sha256:b05f81031cc5b14f3a90b02f955d2ec756ccd5fba4f4e80bc4362520dac679a4"}, - {file = "openapi_core-0.19.2.tar.gz", hash = "sha256:db4e13dd3162d861d9485ae804f350586d9fd1d72808cdb264d6993d9b5ede3f"}, -] - -[package.dependencies] -isodate = "*" -jsonschema = ">=4.18.0,<5.0.0" -jsonschema-path = ">=0.3.1,<0.4.0" -more-itertools = "*" -openapi-schema-validator = ">=0.6.0,<0.7.0" -openapi-spec-validator = ">=0.7.1,<0.8.0" -parse = "*" -werkzeug = "*" - -[package.extras] -aiohttp = ["aiohttp (>=3.0)", "multidict (>=6.0.4,<7.0.0)"] -django = ["django (>=3.0)"] -falcon = ["falcon (>=3.0)"] -fastapi = ["fastapi (>=0.108.0,<0.109.0)"] -flask = ["flask"] -requests = ["requests"] -starlette = ["aioitertools (>=0.11.0,<0.12.0)", "starlette (>=0.26.1,<0.38.0)"] - -[[package]] -name = "openapi-schema-validator" -version = "0.6.2" -description = "OpenAPI schema validation for Python" -optional = false -python-versions = ">=3.8.0,<4.0.0" -files = [ - {file = "openapi_schema_validator-0.6.2-py3-none-any.whl", hash = "sha256:c4887c1347c669eb7cded9090f4438b710845cd0f90d1fb9e1b3303fb37339f8"}, - {file = "openapi_schema_validator-0.6.2.tar.gz", hash = "sha256:11a95c9c9017912964e3e5f2545a5b11c3814880681fcacfb73b1759bb4f2804"}, -] - -[package.dependencies] -jsonschema = ">=4.19.1,<5.0.0" -jsonschema-specifications = ">=2023.5.2,<2024.0.0" -rfc3339-validator = "*" - -[[package]] -name = "openapi-spec-validator" -version = "0.7.1" -description = "OpenAPI 2.0 (aka Swagger) and OpenAPI 3 spec validator" -optional = false -python-versions = ">=3.8.0,<4.0.0" -files = [ - {file = "openapi_spec_validator-0.7.1-py3-none-any.whl", hash = "sha256:3c81825043f24ccbcd2f4b149b11e8231abce5ba84f37065e14ec947d8f4e959"}, - {file = "openapi_spec_validator-0.7.1.tar.gz", hash = "sha256:8577b85a8268685da6f8aa30990b83b7960d4d1117e901d451b5d572605e5ec7"}, -] - -[package.dependencies] -jsonschema = ">=4.18.0,<5.0.0" -jsonschema-path = ">=0.3.1,<0.4.0" -lazy-object-proxy = ">=1.7.1,<2.0.0" -openapi-schema-validator = ">=0.6.0,<0.7.0" - -[[package]] -name = "opentelemetry-api" -version = "1.26.0" -description = "OpenTelemetry Python API" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_api-1.26.0-py3-none-any.whl", hash = "sha256:7d7ea33adf2ceda2dd680b18b1677e4152000b37ca76e679da71ff103b943064"}, - {file = "opentelemetry_api-1.26.0.tar.gz", hash = "sha256:2bd639e4bed5b18486fef0b5a520aaffde5a18fc225e808a1ac4df363f43a1ce"}, -] - -[package.dependencies] -deprecated = ">=1.2.6" -importlib-metadata = ">=6.0,<=8.0.0" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.26.0" -description = "OpenTelemetry Protobuf encoding" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_exporter_otlp_proto_common-1.26.0-py3-none-any.whl", hash = "sha256:ee4d8f8891a1b9c372abf8d109409e5b81947cf66423fd998e56880057afbc71"}, - {file = "opentelemetry_exporter_otlp_proto_common-1.26.0.tar.gz", hash = "sha256:bdbe50e2e22a1c71acaa0c8ba6efaadd58882e5a5978737a44a4c4b10d304c92"}, -] - -[package.dependencies] -opentelemetry-proto = "1.26.0" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.26.0" -description = "OpenTelemetry Collector Protobuf over gRPC Exporter" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_exporter_otlp_proto_grpc-1.26.0-py3-none-any.whl", hash = "sha256:e2be5eff72ebcb010675b818e8d7c2e7d61ec451755b8de67a140bc49b9b0280"}, - {file = "opentelemetry_exporter_otlp_proto_grpc-1.26.0.tar.gz", hash = "sha256:a65b67a9a6b06ba1ec406114568e21afe88c1cdb29c464f2507d529eb906d8ae"}, -] - -[package.dependencies] -deprecated = ">=1.2.6" -googleapis-common-protos = ">=1.52,<2.0" -grpcio = ">=1.0.0,<2.0.0" -opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.26.0" -opentelemetry-proto = "1.26.0" -opentelemetry-sdk = ">=1.26.0,<1.27.0" - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.47b0" -description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_instrumentation-0.47b0-py3-none-any.whl", hash = "sha256:88974ee52b1db08fc298334b51c19d47e53099c33740e48c4f084bd1afd052d5"}, - {file = "opentelemetry_instrumentation-0.47b0.tar.gz", hash = "sha256:96f9885e450c35e3f16a4f33145f2ebf620aea910c9fd74a392bbc0f807a350f"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.4,<2.0" -setuptools = ">=16.0" -wrapt = ">=1.0.0,<2.0.0" - -[[package]] -name = "opentelemetry-instrumentation-asgi" -version = "0.47b0" -description = "ASGI instrumentation for OpenTelemetry" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_instrumentation_asgi-0.47b0-py3-none-any.whl", hash = "sha256:b798dc4957b3edc9dfecb47a4c05809036a4b762234c5071212fda39ead80ade"}, - {file = "opentelemetry_instrumentation_asgi-0.47b0.tar.gz", hash = "sha256:e78b7822c1bca0511e5e9610ec484b8994a81670375e570c76f06f69af7c506a"}, -] - -[package.dependencies] -asgiref = ">=3.0,<4.0" -opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.47b0" -opentelemetry-semantic-conventions = "0.47b0" -opentelemetry-util-http = "0.47b0" - -[package.extras] -instruments = ["asgiref (>=3.0,<4.0)"] - -[[package]] -name = "opentelemetry-instrumentation-fastapi" -version = "0.47b0" -description = "OpenTelemetry FastAPI Instrumentation" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_instrumentation_fastapi-0.47b0-py3-none-any.whl", hash = "sha256:5ac28dd401160b02e4f544a85a9e4f61a8cbe5b077ea0379d411615376a2bd21"}, - {file = "opentelemetry_instrumentation_fastapi-0.47b0.tar.gz", hash = "sha256:0c7c10b5d971e99a420678ffd16c5b1ea4f0db3b31b62faf305fbb03b4ebee36"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.12,<2.0" -opentelemetry-instrumentation = "0.47b0" -opentelemetry-instrumentation-asgi = "0.47b0" -opentelemetry-semantic-conventions = "0.47b0" -opentelemetry-util-http = "0.47b0" - -[package.extras] -instruments = ["fastapi (>=0.58,<1.0)", "fastapi-slim (>=0.111.0,<0.112.0)"] - -[[package]] -name = "opentelemetry-proto" -version = "1.26.0" -description = "OpenTelemetry Python Proto" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_proto-1.26.0-py3-none-any.whl", hash = "sha256:6c4d7b4d4d9c88543bcf8c28ae3f8f0448a753dc291c18c5390444c90b76a725"}, - {file = "opentelemetry_proto-1.26.0.tar.gz", hash = "sha256:c5c18796c0cab3751fc3b98dee53855835e90c0422924b484432ac852d93dc1e"}, -] - -[package.dependencies] -protobuf = ">=3.19,<5.0" - -[[package]] -name = "opentelemetry-sdk" -version = "1.26.0" -description = "OpenTelemetry Python SDK" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_sdk-1.26.0-py3-none-any.whl", hash = "sha256:feb5056a84a88670c041ea0ded9921fca559efec03905dddeb3885525e0af897"}, - {file = "opentelemetry_sdk-1.26.0.tar.gz", hash = "sha256:c90d2868f8805619535c05562d699e2f4fb1f00dbd55a86dcefca4da6fa02f85"}, -] - -[package.dependencies] -opentelemetry-api = "1.26.0" -opentelemetry-semantic-conventions = "0.47b0" -typing-extensions = ">=3.7.4" - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.47b0" -description = "OpenTelemetry Semantic Conventions" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_semantic_conventions-0.47b0-py3-none-any.whl", hash = "sha256:4ff9d595b85a59c1c1413f02bba320ce7ea6bf9e2ead2b0913c4395c7bbc1063"}, - {file = "opentelemetry_semantic_conventions-0.47b0.tar.gz", hash = "sha256:a8d57999bbe3495ffd4d510de26a97dadc1dace53e0275001b2c1b2f67992a7e"}, -] - -[package.dependencies] -deprecated = ">=1.2.6" -opentelemetry-api = "1.26.0" - -[[package]] -name = "opentelemetry-util-http" -version = "0.47b0" -description = "Web util for OpenTelemetry" -optional = false -python-versions = ">=3.8" -files = [ - {file = "opentelemetry_util_http-0.47b0-py3-none-any.whl", hash = "sha256:3d3215e09c4a723b12da6d0233a31395aeb2bb33a64d7b15a1500690ba250f19"}, - {file = "opentelemetry_util_http-0.47b0.tar.gz", hash = "sha256:352a07664c18eef827eb8ddcbd64c64a7284a39dd1655e2f16f577eb046ccb32"}, -] - -[[package]] -name = "orjson" -version = "3.10.7" -description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" -optional = false -python-versions = ">=3.8" -files = [ - {file = "orjson-3.10.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12"}, - {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac"}, - {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7"}, - {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c"}, - {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9"}, - {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91"}, - {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250"}, - {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84"}, - {file = "orjson-3.10.7-cp310-none-win32.whl", hash = "sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175"}, - {file = "orjson-3.10.7-cp310-none-win_amd64.whl", hash = "sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c"}, - {file = "orjson-3.10.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2"}, - {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09"}, - {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0"}, - {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a"}, - {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e"}, - {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6"}, - {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6"}, - {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0"}, - {file = "orjson-3.10.7-cp311-none-win32.whl", hash = "sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f"}, - {file = "orjson-3.10.7-cp311-none-win_amd64.whl", hash = "sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5"}, - {file = "orjson-3.10.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f"}, - {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3"}, - {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93"}, - {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313"}, - {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864"}, - {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09"}, - {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5"}, - {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b"}, - {file = "orjson-3.10.7-cp312-none-win32.whl", hash = "sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb"}, - {file = "orjson-3.10.7-cp312-none-win_amd64.whl", hash = "sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1"}, - {file = "orjson-3.10.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149"}, - {file = "orjson-3.10.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe"}, - {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c"}, - {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad"}, - {file = "orjson-3.10.7-cp313-none-win32.whl", hash = "sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2"}, - {file = "orjson-3.10.7-cp313-none-win_amd64.whl", hash = "sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024"}, - {file = "orjson-3.10.7-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469"}, - {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1"}, - {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225"}, - {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23"}, - {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0"}, - {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98"}, - {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354"}, - {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866"}, - {file = "orjson-3.10.7-cp38-none-win32.whl", hash = "sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c"}, - {file = "orjson-3.10.7-cp38-none-win_amd64.whl", hash = "sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e"}, - {file = "orjson-3.10.7-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20"}, - {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960"}, - {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412"}, - {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9"}, - {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f"}, - {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff"}, - {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd"}, - {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5"}, - {file = "orjson-3.10.7-cp39-none-win32.whl", hash = "sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2"}, - {file = "orjson-3.10.7-cp39-none-win_amd64.whl", hash = "sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58"}, - {file = "orjson-3.10.7.tar.gz", hash = "sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3"}, -] - -[[package]] -name = "overrides" -version = "7.7.0" -description = "A decorator to automatically detect mismatch when overriding a method." -optional = false -python-versions = ">=3.6" -files = [ - {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, - {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, -] - -[[package]] -name = "packaging" -version = "24.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] - -[[package]] -name = "pandas" -version = "2.2.2" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, - {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, - {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, - {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, - {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, - {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"}, - {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"}, - {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"}, - {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"}, - {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"}, - {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"}, - {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"}, - {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, - {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, - {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, - {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, - {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, - {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, - {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, - {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, - {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, - {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, - {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, - {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, - {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"}, - {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"}, - {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, -] -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.7" - -[package.extras] -all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] -aws = ["s3fs (>=2022.11.0)"] -clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] -compression = ["zstandard (>=0.19.0)"] -computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] -feather = ["pyarrow (>=10.0.1)"] -fss = ["fsspec (>=2022.11.0)"] -gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] -hdf5 = ["tables (>=3.8.0)"] -html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] -mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] -parquet = ["pyarrow (>=10.0.1)"] -performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] -plot = ["matplotlib (>=3.6.3)"] -postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] -pyarrow = ["pyarrow (>=10.0.1)"] -spss = ["pyreadstat (>=1.2.0)"] -sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.9.2)"] - -[[package]] -name = "pandocfilters" -version = "1.5.1" -description = "Utilities for writing pandoc filters in python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"}, - {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"}, -] - -[[package]] -name = "parse" -version = "1.20.2" -description = "parse() is the opposite of format()" -optional = false -python-versions = "*" -files = [ - {file = "parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558"}, - {file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"}, -] - -[[package]] -name = "parso" -version = "0.8.4" -description = "A Python Parser" -optional = false -python-versions = ">=3.6" -files = [ - {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, - {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, -] - -[package.extras] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["docopt", "pytest"] - -[[package]] -name = "pathable" -version = "0.4.3" -description = "Object-oriented paths" -optional = false -python-versions = ">=3.7.0,<4.0.0" -files = [ - {file = "pathable-0.4.3-py3-none-any.whl", hash = "sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14"}, - {file = "pathable-0.4.3.tar.gz", hash = "sha256:5c869d315be50776cc8a993f3af43e0c60dc01506b399643f919034ebf4cdcab"}, -] - -[[package]] -name = "pendulum" -version = "3.0.0" -description = "Python datetimes made easy" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, - {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, - {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, - {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, - {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, - {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, - {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, - {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, - {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, - {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, - {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, -] - -[package.dependencies] -python-dateutil = ">=2.6" -tzdata = ">=2020.1" - -[package.extras] -test = ["time-machine (>=2.6.0)"] - -[[package]] -name = "pexpect" -version = "4.9.0" -description = "Pexpect allows easy control of interactive console applications." -optional = false -python-versions = "*" -files = [ - {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, - {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, -] - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "pillow" -version = "10.4.0" -description = "Python Imaging Library (Fork)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, - {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, - {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, - {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, - {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, - {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, - {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, - {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, - {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, - {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, - {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, - {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, - {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, - {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, - {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, - {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, - {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, - {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, - {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, - {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] -fpx = ["olefile"] -mic = ["olefile"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] -typing = ["typing-extensions"] -xmp = ["defusedxml"] - -[[package]] -name = "pinecone-client" -version = "5.0.1" -description = "Pinecone client and SDK" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "pinecone_client-5.0.1-py3-none-any.whl", hash = "sha256:c8f7835e1045ba84e295f217a8e85573ffb80b41501bbc1af6d92c9631c567a7"}, - {file = "pinecone_client-5.0.1.tar.gz", hash = "sha256:11c33ff5d1c38a6ce69e69fe532c0f22f312fb28d761bb30b3767816d3181d64"}, -] - -[package.dependencies] -certifi = ">=2019.11.17" -pinecone-plugin-inference = ">=1.0.3,<2.0.0" -pinecone-plugin-interface = ">=0.0.7,<0.0.8" -tqdm = ">=4.64.1" -typing-extensions = ">=3.7.4" -urllib3 = [ - {version = ">=1.26.0", markers = "python_version >= \"3.8\" and python_version < \"3.12\""}, - {version = ">=1.26.5", markers = "python_version >= \"3.12\" and python_version < \"4.0\""}, -] - -[package.extras] -grpc = ["googleapis-common-protos (>=1.53.0)", "grpcio (>=1.44.0)", "grpcio (>=1.59.0)", "lz4 (>=3.1.3)", "protobuf (>=4.25,<5.0)", "protoc-gen-openapiv2 (>=0.0.1,<0.0.2)"] - -[[package]] -name = "pinecone-plugin-inference" -version = "1.0.3" -description = "Embeddings plugin for Pinecone SDK" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "pinecone_plugin_inference-1.0.3-py3-none-any.whl", hash = "sha256:bbdfe5dba99a87374d9e3315b62b8e1bbca52d5fe069a64cd6b212efbc8b9afd"}, - {file = "pinecone_plugin_inference-1.0.3.tar.gz", hash = "sha256:c6519ba730123713a181c010f0db9d6449d11de451b8e79bec4efd662b096f41"}, -] - -[package.dependencies] -pinecone-plugin-interface = ">=0.0.7,<0.0.8" - -[[package]] -name = "pinecone-plugin-interface" -version = "0.0.7" -description = "Plugin interface for the Pinecone python client" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "pinecone_plugin_interface-0.0.7-py3-none-any.whl", hash = "sha256:875857ad9c9fc8bbc074dbe780d187a2afd21f5bfe0f3b08601924a61ef1bba8"}, - {file = "pinecone_plugin_interface-0.0.7.tar.gz", hash = "sha256:b8e6675e41847333aa13923cc44daa3f85676d7157324682dc1640588a982846"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "portalocker" -version = "2.10.1" -description = "Wraps the portalocker recipe for easy usage" -optional = false -python-versions = ">=3.8" -files = [ - {file = "portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf"}, - {file = "portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f"}, -] - -[package.dependencies] -pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} - -[package.extras] -docs = ["sphinx (>=1.7.1)"] -redis = ["redis"] -tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"] - -[[package]] -name = "posthog" -version = "3.5.0" -description = "Integrate PostHog into any python application." -optional = false -python-versions = "*" -files = [ - {file = "posthog-3.5.0-py2.py3-none-any.whl", hash = "sha256:3c672be7ba6f95d555ea207d4486c171d06657eb34b3ce25eb043bfe7b6b5b76"}, - {file = "posthog-3.5.0.tar.gz", hash = "sha256:8f7e3b2c6e8714d0c0c542a2109b83a7549f63b7113a133ab2763a89245ef2ef"}, -] - -[package.dependencies] -backoff = ">=1.10.0" -monotonic = ">=1.5" -python-dateutil = ">2.1" -requests = ">=2.7,<3.0" -six = ">=1.5" - -[package.extras] -dev = ["black", "flake8", "flake8-print", "isort", "pre-commit"] -sentry = ["django", "sentry-sdk"] -test = ["coverage", "flake8", "freezegun (==0.3.15)", "mock (>=2.0.0)", "pylint", "pytest", "pytest-timeout"] - -[[package]] -name = "prance" -version = "23.6.21.0" -description = "Resolving Swagger/OpenAPI 2.0 and 3.0.0 Parser" -optional = false -python-versions = ">=3.8" -files = [ - {file = "prance-23.6.21.0-py3-none-any.whl", hash = "sha256:6a4276fa07ed9f22feda4331097d7503c4adc3097e46ffae97425f2c1026bd9f"}, - {file = "prance-23.6.21.0.tar.gz", hash = "sha256:d8c15f8ac34019751cc4945f866d8d964d7888016d10de3592e339567177cabe"}, -] - -[package.dependencies] -chardet = ">=3.0" -packaging = ">=21.3" -requests = ">=2.25" -"ruamel.yaml" = ">=0.17.10" -six = ">=1.15,<2.0" - -[package.extras] -cli = ["click (>=7.0)"] -dev = ["bumpversion (>=0.6)", "pytest (>=6.1)", "pytest-cov (>=2.11)", "sphinx (>=3.4)", "towncrier (>=19.2)", "tox (>=3.4)"] -flex = ["flex (>=6.13,<7.0)"] -icu = ["PyICU (>=2.4,<3.0)"] -osv = ["openapi-spec-validator (>=0.5.1,<0.6.0)"] -ssv = ["swagger-spec-validator (>=2.4,<3.0)"] - -[[package]] -name = "pre-commit" -version = "3.8.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, - {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "prompt-toolkit" -version = "3.0.47" -description = "Library for building powerful interactive command lines in Python" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, - {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "proto-plus" -version = "1.24.0" -description = "Beautiful, Pythonic protocol buffers." -optional = false -python-versions = ">=3.7" -files = [ - {file = "proto-plus-1.24.0.tar.gz", hash = "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445"}, - {file = "proto_plus-1.24.0-py3-none-any.whl", hash = "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12"}, -] - -[package.dependencies] -protobuf = ">=3.19.0,<6.0.0dev" - -[package.extras] -testing = ["google-api-core (>=1.31.5)"] - -[[package]] -name = "protobuf" -version = "4.25.4" -description = "" -optional = false -python-versions = ">=3.8" -files = [ - {file = "protobuf-4.25.4-cp310-abi3-win32.whl", hash = "sha256:db9fd45183e1a67722cafa5c1da3e85c6492a5383f127c86c4c4aa4845867dc4"}, - {file = "protobuf-4.25.4-cp310-abi3-win_amd64.whl", hash = "sha256:ba3d8504116a921af46499471c63a85260c1a5fc23333154a427a310e015d26d"}, - {file = "protobuf-4.25.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:eecd41bfc0e4b1bd3fa7909ed93dd14dd5567b98c941d6c1ad08fdcab3d6884b"}, - {file = "protobuf-4.25.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:4c8a70fdcb995dcf6c8966cfa3a29101916f7225e9afe3ced4395359955d3835"}, - {file = "protobuf-4.25.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:3319e073562e2515c6ddc643eb92ce20809f5d8f10fead3332f71c63be6a7040"}, - {file = "protobuf-4.25.4-cp38-cp38-win32.whl", hash = "sha256:7e372cbbda66a63ebca18f8ffaa6948455dfecc4e9c1029312f6c2edcd86c4e1"}, - {file = "protobuf-4.25.4-cp38-cp38-win_amd64.whl", hash = "sha256:051e97ce9fa6067a4546e75cb14f90cf0232dcb3e3d508c448b8d0e4265b61c1"}, - {file = "protobuf-4.25.4-cp39-cp39-win32.whl", hash = "sha256:90bf6fd378494eb698805bbbe7afe6c5d12c8e17fca817a646cd6a1818c696ca"}, - {file = "protobuf-4.25.4-cp39-cp39-win_amd64.whl", hash = "sha256:ac79a48d6b99dfed2729ccccee547b34a1d3d63289c71cef056653a846a2240f"}, - {file = "protobuf-4.25.4-py3-none-any.whl", hash = "sha256:bfbebc1c8e4793cfd58589acfb8a1026be0003e852b9da7db5a4285bde996978"}, - {file = "protobuf-4.25.4.tar.gz", hash = "sha256:0dc4a62cc4052a036ee2204d26fe4d835c62827c855c8a03f29fe6da146b380d"}, -] - -[[package]] -name = "psutil" -version = "6.0.0" -description = "Cross-platform lib for process and system monitoring in Python." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, - {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, - {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, - {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, - {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, - {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, - {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, - {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, - {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, - {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, -] - -[package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] - -[[package]] -name = "psycopg" -version = "3.2.1" -description = "PostgreSQL database adapter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"}, - {file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"}, -] - -[package.dependencies] -psycopg-binary = {version = "3.2.1", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} -psycopg-pool = {version = "*", optional = true, markers = "extra == \"pool\""} -typing-extensions = ">=4.4" -tzdata = {version = "*", markers = "sys_platform == \"win32\""} - -[package.extras] -binary = ["psycopg-binary (==3.2.1)"] -c = ["psycopg-c (==3.2.1)"] -dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.6)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] -docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] -pool = ["psycopg-pool"] -test = ["anyio (>=4.0)", "mypy (>=1.6)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] - -[[package]] -name = "psycopg-binary" -version = "3.2.1" -description = "PostgreSQL database adapter for Python -- C optimisation distribution" -optional = false -python-versions = ">=3.8" -files = [ - {file = "psycopg_binary-3.2.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:cad2de17804c4cfee8640ae2b279d616bb9e4734ac3c17c13db5e40982bd710d"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:592b27d6c46a40f9eeaaeea7c1fef6f3c60b02c634365eb649b2d880669f149f"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a997efbaadb5e1a294fb5760e2f5643d7b8e4e3fe6cb6f09e6d605fd28e0291"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1d2b6438fb83376f43ebb798bf0ad5e57bc56c03c9c29c85bc15405c8c0ac5a"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f087bd84bdcac78bf9f024ebdbfacd07fc0a23ec8191448a50679e2ac4a19e"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:415c3b72ea32119163255c6504085f374e47ae7345f14bc3f0ef1f6e0976a879"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f092114f10f81fb6bae544a0ec027eb720e2d9c74a4fcdaa9dd3899873136935"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06a7aae34edfe179ddc04da005e083ff6c6b0020000399a2cbf0a7121a8a22ea"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b018631e5c80ce9bc210b71ea885932f9cca6db131e4df505653d7e3873a938"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8a509aeaac364fa965454e80cd110fe6d48ba2c80f56c9b8563423f0b5c3cfd"}, - {file = "psycopg_binary-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:413977d18412ff83486eeb5875eb00b185a9391c57febac45b8993bf9c0ff489"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:62b1b7b07e00ee490afb39c0a47d8282a9c2822c7cfed9553a04b0058adf7e7f"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f8afb07114ea9b924a4a0305ceb15354ccf0ef3c0e14d54b8dbeb03e50182dd7"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40bb515d042f6a345714ec0403df68ccf13f73b05e567837d80c886c7c9d3805"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6418712ba63cebb0c88c050b3997185b0ef54173b36568522d5634ac06153040"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:101472468d59c74bb8565fab603e032803fd533d16be4b2d13da1bab8deb32a3"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa3931f308ab4a479d0ee22dc04bea867a6365cac0172e5ddcba359da043854b"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc314a47d44fe1a8069b075a64abffad347a3a1d8652fed1bab5d3baea37acb2"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cc304a46be1e291031148d9d95c12451ffe783ff0cc72f18e2cc7ec43cdb8c68"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f9e13600647087df5928875559f0eb8f496f53e6278b7da9511b4b3d0aff960"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b140182830c76c74d17eba27df3755a46442ce8d4fb299e7f1cf2f74a87c877b"}, - {file = "psycopg_binary-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:3c838806eeb99af39f934b7999e35f947a8e577997cc892c12b5053a97a9057f"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7066d3dca196ed0dc6172f9777b2d62e4f138705886be656cccff2d555234d60"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:28ada5f610468c57d8a4a055a8ea915d0085a43d794266c4f3b9d02f4288f4db"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e8213bf50af073b1aa8dc3cff123bfeedac86332a16c1b7274910bc88a847c7"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74d623261655a169bc84a9669890975c229f2fa6e19a7f2d10a77675dcf1a707"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42781ba94e8842ee98bca5a7d0c44cc9d067500fedca2d6a90fa3609b6d16b42"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e6669091d09f8ba36e10ce678a6d9916e110446236a9b92346464a3565635e"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b09e8a576a2ac69d695032ee76f31e03b30781828b5dd6d18c6a009e5a3d1c35"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8f28ff0cb9f1defdc4a6f8c958bf6787274247e7dfeca811f6e2f56602695fb1"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c84fcac8a3a3479ac14673095cc4e1fdba2935499f72c436785ac679bec0d1a"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:950fd666ec9e9fe6a8eeb2b5a8f17301790e518953730ad44d715b59ffdbc67f"}, - {file = "psycopg_binary-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:334046a937bb086c36e2c6889fe327f9f29bfc085d678f70fac0b0618949f674"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:1d6833f607f3fc7b22226a9e121235d3b84c0eda1d3caab174673ef698f63788"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d353e028b8f848b9784450fc2abf149d53a738d451eab3ee4c85703438128b9"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34e369891f77d0738e5d25727c307d06d5344948771e5379ea29c76c6d84555"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ab58213cc976a1666f66bc1cb2e602315cd753b7981a8e17237ac2a185bd4a1"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0104a72a17aa84b3b7dcab6c84826c595355bf54bb6ea6d284dcb06d99c6801"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:059cbd4e6da2337e17707178fe49464ed01de867dc86c677b30751755ec1dc51"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:73f9c9b984be9c322b5ec1515b12df1ee5896029f5e72d46160eb6517438659c"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:af0469c00f24c4bec18c3d2ede124bf62688d88d1b8a5f3c3edc2f61046fe0d7"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:463d55345f73ff391df8177a185ad57b552915ad33f5cc2b31b930500c068b22"}, - {file = "psycopg_binary-3.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:302b86f92c0d76e99fe1b5c22c492ae519ce8b98b88d37ef74fda4c9e24c6b46"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0879b5d76b7d48678d31278242aaf951bc2d69ca4e4d7cef117e4bbf7bfefda9"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f99e59f8a5f4dcd9cbdec445f3d8ac950a492fc0e211032384d6992ed3c17eb7"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84837e99353d16c6980603b362d0f03302d4b06c71672a6651f38df8a482923d"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ce965caf618061817f66c0906f0452aef966c293ae0933d4fa5a16ea6eaf5bb"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78c2007caf3c90f08685c5378e3ceb142bafd5636be7495f7d86ec8a977eaeef"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7a84b5eb194a258116154b2a4ff2962ea60ea52de089508db23a51d3d6b1c7d1"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4a42b8f9ab39affcd5249b45cac763ac3cf12df962b67e23fd15a2ee2932afe5"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:788ffc43d7517c13e624c83e0e553b7b8823c9655e18296566d36a829bfb373f"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:21927f41c4d722ae8eb30d62a6ce732c398eac230509af5ba1749a337f8a63e2"}, - {file = "psycopg_binary-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:921f0c7f39590763d64a619de84d1b142587acc70fd11cbb5ba8fa39786f3073"}, -] - -[[package]] -name = "psycopg-pool" -version = "3.2.2" -description = "Connection Pool for Psycopg" -optional = false -python-versions = ">=3.8" -files = [ - {file = "psycopg_pool-3.2.2-py3-none-any.whl", hash = "sha256:273081d0fbfaced4f35e69200c89cb8fbddfe277c38cc86c235b90a2ec2c8153"}, - {file = "psycopg_pool-3.2.2.tar.gz", hash = "sha256:9e22c370045f6d7f2666a5ad1b0caf345f9f1912195b0b25d0d3bcc4f3a7389c"}, -] - -[package.dependencies] -typing-extensions = ">=4.4" - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -optional = false -python-versions = "*" -files = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] - -[[package]] -name = "pure-eval" -version = "0.2.3" -description = "Safely evaluate AST nodes without side effects" -optional = false -python-versions = "*" -files = [ - {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, - {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "pyarrow" -version = "17.0.0" -description = "Python library for Apache Arrow" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyarrow-17.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07"}, - {file = "pyarrow-17.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655"}, - {file = "pyarrow-17.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545"}, - {file = "pyarrow-17.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2"}, - {file = "pyarrow-17.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8"}, - {file = "pyarrow-17.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047"}, - {file = "pyarrow-17.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087"}, - {file = "pyarrow-17.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977"}, - {file = "pyarrow-17.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3"}, - {file = "pyarrow-17.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15"}, - {file = "pyarrow-17.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597"}, - {file = "pyarrow-17.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420"}, - {file = "pyarrow-17.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4"}, - {file = "pyarrow-17.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03"}, - {file = "pyarrow-17.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22"}, - {file = "pyarrow-17.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053"}, - {file = "pyarrow-17.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a"}, - {file = "pyarrow-17.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc"}, - {file = "pyarrow-17.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a"}, - {file = "pyarrow-17.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b"}, - {file = "pyarrow-17.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7"}, - {file = "pyarrow-17.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204"}, - {file = "pyarrow-17.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8"}, - {file = "pyarrow-17.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155"}, - {file = "pyarrow-17.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145"}, - {file = "pyarrow-17.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c"}, - {file = "pyarrow-17.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c"}, - {file = "pyarrow-17.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca"}, - {file = "pyarrow-17.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb"}, - {file = "pyarrow-17.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df"}, - {file = "pyarrow-17.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687"}, - {file = "pyarrow-17.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b"}, - {file = "pyarrow-17.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5"}, - {file = "pyarrow-17.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda"}, - {file = "pyarrow-17.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204"}, - {file = "pyarrow-17.0.0.tar.gz", hash = "sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28"}, -] - -[package.dependencies] -numpy = ">=1.16.6" - -[package.extras] -test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] - -[[package]] -name = "pyasn1" -version = "0.6.0" -description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, - {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.0" -description = "A collection of ASN.1-based protocols modules" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"}, - {file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"}, -] - -[package.dependencies] -pyasn1 = ">=0.4.6,<0.7.0" - -[[package]] -name = "pybars4" -version = "0.9.13" -description = "Handlebars.js templating for Python 3" -optional = false -python-versions = "*" -files = [ - {file = "pybars4-0.9.13.tar.gz", hash = "sha256:425817da20d4ad320bc9b8e77a60cab1bb9d3c677df3dce224925c3310fcd635"}, -] - -[package.dependencies] -PyMeta3 = ">=0.5.1" - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pydantic" -version = "2.8.2" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, -] - -[package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.20.1" -typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} - -[package.extras] -email = ["email-validator (>=2.0.0)"] - -[[package]] -name = "pydantic-core" -version = "2.20.1" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pydantic-settings" -version = "2.4.0" -description = "Settings management using Pydantic" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_settings-2.4.0-py3-none-any.whl", hash = "sha256:bb6849dc067f1687574c12a639e231f3a6feeed0a12d710c1382045c5db1c315"}, - {file = "pydantic_settings-2.4.0.tar.gz", hash = "sha256:ed81c3a0f46392b4d7c0a565c05884e6e54b3456e6f0fe4d8814981172dc9a88"}, -] - -[package.dependencies] -pydantic = ">=2.7.0" -python-dotenv = ">=0.21.0" - -[package.extras] -azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] -toml = ["tomli (>=2.0.1)"] -yaml = ["pyyaml (>=6.0.1)"] - -[[package]] -name = "pygments" -version = "2.18.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyjwt" -version = "2.9.0" -description = "JSON Web Token implementation in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, - {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, -] - -[package.dependencies] -cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] - -[[package]] -name = "pymeta3" -version = "0.5.1" -description = "Pattern-matching language based on OMeta for Python 3 and 2" -optional = false -python-versions = "*" -files = [ - {file = "PyMeta3-0.5.1.tar.gz", hash = "sha256:18bda326d9a9bbf587bfc0ee0bc96864964d78b067288bcf55d4d98681d05bcb"}, -] - -[[package]] -name = "pymilvus" -version = "2.4.5" -description = "Python Sdk for Milvus" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pymilvus-2.4.5-py3-none-any.whl", hash = "sha256:dc4f2d1eac8db9cf3951de39566a1a244695760bb94d8310fbfc73d6d62bb267"}, - {file = "pymilvus-2.4.5.tar.gz", hash = "sha256:1a497fe9b41d6bf62b1d5e1c412960922dde1598576fcbb8818040c8af11149f"}, -] - -[package.dependencies] -environs = "<=9.5.0" -grpcio = ">=1.49.1,<=1.63.0" -milvus-lite = {version = ">=2.4.0,<2.5.0", markers = "sys_platform != \"win32\""} -pandas = ">=1.2.4" -protobuf = ">=3.20.0" -setuptools = ">69" -ujson = ">=2.0.0" - -[package.extras] -bulk-writer = ["azure-storage-blob", "minio (>=7.0.0)", "pyarrow (>=12.0.0)", "requests"] -dev = ["black", "grpcio (==1.62.2)", "grpcio-testing (==1.62.2)", "grpcio-tools (==1.62.2)", "pytest (>=5.3.4)", "pytest-cov (>=2.8.1)", "pytest-timeout (>=1.3.4)", "ruff (>0.4.0)"] -model = ["milvus-model (>=0.1.0)"] - -[[package]] -name = "pymongo" -version = "4.8.0" -description = "Python driver for MongoDB " -optional = false -python-versions = ">=3.8" -files = [ - {file = "pymongo-4.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2b7bec27e047e84947fbd41c782f07c54c30c76d14f3b8bf0c89f7413fac67a"}, - {file = "pymongo-4.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c68fe128a171493018ca5c8020fc08675be130d012b7ab3efe9e22698c612a1"}, - {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920d4f8f157a71b3cb3f39bc09ce070693d6e9648fb0e30d00e2657d1dca4e49"}, - {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52b4108ac9469febba18cea50db972605cc43978bedaa9fea413378877560ef8"}, - {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:180d5eb1dc28b62853e2f88017775c4500b07548ed28c0bd9c005c3d7bc52526"}, - {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aec2b9088cdbceb87e6ca9c639d0ff9b9d083594dda5ca5d3c4f6774f4c81b33"}, - {file = "pymongo-4.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0cf61450feadca81deb1a1489cb1a3ae1e4266efd51adafecec0e503a8dcd84"}, - {file = "pymongo-4.8.0-cp310-cp310-win32.whl", hash = "sha256:8b18c8324809539c79bd6544d00e0607e98ff833ca21953df001510ca25915d1"}, - {file = "pymongo-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e5df28f74002e37bcbdfdc5109799f670e4dfef0fb527c391ff84f078050e7b5"}, - {file = "pymongo-4.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b50040d9767197b77ed420ada29b3bf18a638f9552d80f2da817b7c4a4c9c68"}, - {file = "pymongo-4.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:417369ce39af2b7c2a9c7152c1ed2393edfd1cbaf2a356ba31eb8bcbd5c98dd7"}, - {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf821bd3befb993a6db17229a2c60c1550e957de02a6ff4dd0af9476637b2e4d"}, - {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9365166aa801c63dff1a3cb96e650be270da06e3464ab106727223123405510f"}, - {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc8b8582f4209c2459b04b049ac03c72c618e011d3caa5391ff86d1bda0cc486"}, - {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e5019f75f6827bb5354b6fef8dfc9d6c7446894a27346e03134d290eb9e758"}, - {file = "pymongo-4.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b5802151fc2b51cd45492c80ed22b441d20090fb76d1fd53cd7760b340ff554"}, - {file = "pymongo-4.8.0-cp311-cp311-win32.whl", hash = "sha256:4bf58e6825b93da63e499d1a58de7de563c31e575908d4e24876234ccb910eba"}, - {file = "pymongo-4.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b747c0e257b9d3e6495a018309b9e0c93b7f0d65271d1d62e572747f4ffafc88"}, - {file = "pymongo-4.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e6a720a3d22b54183352dc65f08cd1547204d263e0651b213a0a2e577e838526"}, - {file = "pymongo-4.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31e4d21201bdf15064cf47ce7b74722d3e1aea2597c6785882244a3bb58c7eab"}, - {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b804bb4f2d9dc389cc9e827d579fa327272cdb0629a99bfe5b83cb3e269ebf"}, - {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2fbdb87fe5075c8beb17a5c16348a1ea3c8b282a5cb72d173330be2fecf22f5"}, - {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd39455b7ee70aabee46f7399b32ab38b86b236c069ae559e22be6b46b2bbfc4"}, - {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:940d456774b17814bac5ea7fc28188c7a1338d4a233efbb6ba01de957bded2e8"}, - {file = "pymongo-4.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:236bbd7d0aef62e64caf4b24ca200f8c8670d1a6f5ea828c39eccdae423bc2b2"}, - {file = "pymongo-4.8.0-cp312-cp312-win32.whl", hash = "sha256:47ec8c3f0a7b2212dbc9be08d3bf17bc89abd211901093e3ef3f2adea7de7a69"}, - {file = "pymongo-4.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e84bc7707492f06fbc37a9f215374d2977d21b72e10a67f1b31893ec5a140ad8"}, - {file = "pymongo-4.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:519d1bab2b5e5218c64340b57d555d89c3f6c9d717cecbf826fb9d42415e7750"}, - {file = "pymongo-4.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87075a1feb1e602e539bdb1ef8f4324a3427eb0d64208c3182e677d2c0718b6f"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f53429515d2b3e86dcc83dadecf7ff881e538c168d575f3688698a8707b80a"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdc20cd1e1141b04696ffcdb7c71e8a4a665db31fe72e51ec706b3bdd2d09f36"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:284d0717d1a7707744018b0b6ee7801b1b1ff044c42f7be7a01bb013de639470"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5bf0eb8b6ef40fa22479f09375468c33bebb7fe49d14d9c96c8fd50355188b0"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ecd71b9226bd1d49416dc9f999772038e56f415a713be51bf18d8676a0841c8"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0061af6e8c5e68b13f1ec9ad5251247726653c5af3c0bbdfbca6cf931e99216"}, - {file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:658d0170f27984e0d89c09fe5c42296613b711a3ffd847eb373b0dbb5b648d5f"}, - {file = "pymongo-4.8.0-cp38-cp38-win32.whl", hash = "sha256:3ed1c316718a2836f7efc3d75b4b0ffdd47894090bc697de8385acd13c513a70"}, - {file = "pymongo-4.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:7148419eedfea9ecb940961cfe465efaba90595568a1fb97585fb535ea63fe2b"}, - {file = "pymongo-4.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8400587d594761e5136a3423111f499574be5fd53cf0aefa0d0f05b180710b0"}, - {file = "pymongo-4.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af3e98dd9702b73e4e6fd780f6925352237f5dce8d99405ff1543f3771201704"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de3a860f037bb51f968de320baef85090ff0bbb42ec4f28ec6a5ddf88be61871"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fc18b3a093f3db008c5fea0e980dbd3b743449eee29b5718bc2dc15ab5088bb"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18c9d8f975dd7194c37193583fd7d1eb9aea0c21ee58955ecf35362239ff31ac"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:408b2f8fdbeca3c19e4156f28fff1ab11c3efb0407b60687162d49f68075e63c"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6564780cafd6abeea49759fe661792bd5a67e4f51bca62b88faab497ab5fe89"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d18d86bc9e103f4d3d4f18b85a0471c0e13ce5b79194e4a0389a224bb70edd53"}, - {file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9097c331577cecf8034422956daaba7ec74c26f7b255d718c584faddd7fa2e3c"}, - {file = "pymongo-4.8.0-cp39-cp39-win32.whl", hash = "sha256:d5428dbcd43d02f6306e1c3c95f692f68b284e6ee5390292242f509004c9e3a8"}, - {file = "pymongo-4.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:ef7225755ed27bfdb18730c68f6cb023d06c28f2b734597480fb4c0e500feb6f"}, - {file = "pymongo-4.8.0.tar.gz", hash = "sha256:454f2295875744dc70f1881e4b2eb99cdad008a33574bc8aaf120530f66c0cde"}, -] - -[package.dependencies] -dnspython = ">=1.16.0,<3.0.0" - -[package.extras] -aws = ["pymongo-auth-aws (>=1.1.0,<2.0.0)"] -docs = ["furo (==2023.9.10)", "readthedocs-sphinx-search (>=0.3,<1.0)", "sphinx (>=5.3,<8)", "sphinx-rtd-theme (>=2,<3)", "sphinxcontrib-shellcheck (>=1,<2)"] -encryption = ["certifi", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.6.0,<2.0.0)"] -gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] -ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] -snappy = ["python-snappy"] -test = ["pytest (>=7)"] -zstd = ["zstandard"] - -[[package]] -name = "pyparsing" -version = "3.1.2" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, - {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pypika" -version = "0.48.9" -description = "A SQL query builder API for Python" -optional = false -python-versions = "*" -files = [ - {file = "PyPika-0.48.9.tar.gz", hash = "sha256:838836a61747e7c8380cd1b7ff638694b7a7335345d0f559b04b2cd832ad5378"}, -] - -[[package]] -name = "pyproject-hooks" -version = "1.1.0" -description = "Wrappers to call pyproject.toml-based build backend hooks." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyproject_hooks-1.1.0-py3-none-any.whl", hash = "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2"}, - {file = "pyproject_hooks-1.1.0.tar.gz", hash = "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965"}, -] - -[[package]] -name = "pyreadline3" -version = "3.4.1" -description = "A python implementation of GNU readline." -optional = false -python-versions = "*" -files = [ - {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, - {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, -] - -[[package]] -name = "pytest" -version = "8.3.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.23.8" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, - {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, -] - -[package.dependencies] -pytest = ">=7.0.0,<9" - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-cov" -version = "5.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-xdist" -version = "3.6.1" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, -] - -[package.dependencies] -execnet = ">=2.1" -psutil = {version = ">=3.0", optional = true, markers = "extra == \"psutil\""} -pytest = ">=7.0.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-dotenv" -version = "1.0.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "pytz" -version = "2024.1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, -] - -[[package]] -name = "pywin32" -version = "306" -description = "Python for Window Extensions" -optional = false -python-versions = "*" -files = [ - {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, - {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, - {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, - {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, - {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, - {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, - {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, - {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, - {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, - {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, - {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, - {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, - {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, - {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "pyzmq" -version = "26.1.0" -description = "Python bindings for 0MQ" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyzmq-26.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:263cf1e36862310bf5becfbc488e18d5d698941858860c5a8c079d1511b3b18e"}, - {file = "pyzmq-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d5c8b17f6e8f29138678834cf8518049e740385eb2dbf736e8f07fc6587ec682"}, - {file = "pyzmq-26.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a95c2358fcfdef3374cb8baf57f1064d73246d55e41683aaffb6cfe6862917"}, - {file = "pyzmq-26.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99de52b8fbdb2a8f5301ae5fc0f9e6b3ba30d1d5fc0421956967edcc6914242"}, - {file = "pyzmq-26.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bcbfbab4e1895d58ab7da1b5ce9a327764f0366911ba5b95406c9104bceacb0"}, - {file = "pyzmq-26.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:77ce6a332c7e362cb59b63f5edf730e83590d0ab4e59c2aa5bd79419a42e3449"}, - {file = "pyzmq-26.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ba0a31d00e8616149a5ab440d058ec2da621e05d744914774c4dde6837e1f545"}, - {file = "pyzmq-26.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8b88641384e84a258b740801cd4dbc45c75f148ee674bec3149999adda4a8598"}, - {file = "pyzmq-26.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2fa76ebcebe555cce90f16246edc3ad83ab65bb7b3d4ce408cf6bc67740c4f88"}, - {file = "pyzmq-26.1.0-cp310-cp310-win32.whl", hash = "sha256:fbf558551cf415586e91160d69ca6416f3fce0b86175b64e4293644a7416b81b"}, - {file = "pyzmq-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a7b8aab50e5a288c9724d260feae25eda69582be84e97c012c80e1a5e7e03fb2"}, - {file = "pyzmq-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:08f74904cb066e1178c1ec706dfdb5c6c680cd7a8ed9efebeac923d84c1f13b1"}, - {file = "pyzmq-26.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:46d6800b45015f96b9d92ece229d92f2aef137d82906577d55fadeb9cf5fcb71"}, - {file = "pyzmq-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5bc2431167adc50ba42ea3e5e5f5cd70d93e18ab7b2f95e724dd8e1bd2c38120"}, - {file = "pyzmq-26.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3bb34bebaa1b78e562931a1687ff663d298013f78f972a534f36c523311a84d"}, - {file = "pyzmq-26.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3f6329340cef1c7ba9611bd038f2d523cea79f09f9c8f6b0553caba59ec562"}, - {file = "pyzmq-26.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:471880c4c14e5a056a96cd224f5e71211997d40b4bf5e9fdded55dafab1f98f2"}, - {file = "pyzmq-26.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ce6f2b66799971cbae5d6547acefa7231458289e0ad481d0be0740535da38d8b"}, - {file = "pyzmq-26.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a1f6ea5b1d6cdbb8cfa0536f0d470f12b4b41ad83625012e575f0e3ecfe97f0"}, - {file = "pyzmq-26.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b45e6445ac95ecb7d728604bae6538f40ccf4449b132b5428c09918523abc96d"}, - {file = "pyzmq-26.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:94c4262626424683feea0f3c34951d39d49d354722db2745c42aa6bb50ecd93b"}, - {file = "pyzmq-26.1.0-cp311-cp311-win32.whl", hash = "sha256:a0f0ab9df66eb34d58205913f4540e2ad17a175b05d81b0b7197bc57d000e829"}, - {file = "pyzmq-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:8efb782f5a6c450589dbab4cb0f66f3a9026286333fe8f3a084399149af52f29"}, - {file = "pyzmq-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f133d05aaf623519f45e16ab77526e1e70d4e1308e084c2fb4cedb1a0c764bbb"}, - {file = "pyzmq-26.1.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:3d3146b1c3dcc8a1539e7cc094700b2be1e605a76f7c8f0979b6d3bde5ad4072"}, - {file = "pyzmq-26.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d9270fbf038bf34ffca4855bcda6e082e2c7f906b9eb8d9a8ce82691166060f7"}, - {file = "pyzmq-26.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:995301f6740a421afc863a713fe62c0aaf564708d4aa057dfdf0f0f56525294b"}, - {file = "pyzmq-26.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7eca8b89e56fb8c6c26dd3e09bd41b24789022acf1cf13358e96f1cafd8cae3"}, - {file = "pyzmq-26.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d4feb2e83dfe9ace6374a847e98ee9d1246ebadcc0cb765482e272c34e5820"}, - {file = "pyzmq-26.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d4fafc2eb5d83f4647331267808c7e0c5722c25a729a614dc2b90479cafa78bd"}, - {file = "pyzmq-26.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:58c33dc0e185dd97a9ac0288b3188d1be12b756eda67490e6ed6a75cf9491d79"}, - {file = "pyzmq-26.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:68a0a1d83d33d8367ddddb3e6bb4afbb0f92bd1dac2c72cd5e5ddc86bdafd3eb"}, - {file = "pyzmq-26.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ae7c57e22ad881af78075e0cea10a4c778e67234adc65c404391b417a4dda83"}, - {file = "pyzmq-26.1.0-cp312-cp312-win32.whl", hash = "sha256:347e84fc88cc4cb646597f6d3a7ea0998f887ee8dc31c08587e9c3fd7b5ccef3"}, - {file = "pyzmq-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:9f136a6e964830230912f75b5a116a21fe8e34128dcfd82285aa0ef07cb2c7bd"}, - {file = "pyzmq-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:a4b7a989c8f5a72ab1b2bbfa58105578753ae77b71ba33e7383a31ff75a504c4"}, - {file = "pyzmq-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d416f2088ac8f12daacffbc2e8918ef4d6be8568e9d7155c83b7cebed49d2322"}, - {file = "pyzmq-26.1.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:ecb6c88d7946166d783a635efc89f9a1ff11c33d680a20df9657b6902a1d133b"}, - {file = "pyzmq-26.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:471312a7375571857a089342beccc1a63584315188560c7c0da7e0a23afd8a5c"}, - {file = "pyzmq-26.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e6cea102ffa16b737d11932c426f1dc14b5938cf7bc12e17269559c458ac334"}, - {file = "pyzmq-26.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec7248673ffc7104b54e4957cee38b2f3075a13442348c8d651777bf41aa45ee"}, - {file = "pyzmq-26.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0614aed6f87d550b5cecb03d795f4ddbb1544b78d02a4bd5eecf644ec98a39f6"}, - {file = "pyzmq-26.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e8746ce968be22a8a1801bf4a23e565f9687088580c3ed07af5846580dd97f76"}, - {file = "pyzmq-26.1.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:7688653574392d2eaeef75ddcd0b2de5b232d8730af29af56c5adf1df9ef8d6f"}, - {file = "pyzmq-26.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8d4dac7d97f15c653a5fedcafa82626bd6cee1450ccdaf84ffed7ea14f2b07a4"}, - {file = "pyzmq-26.1.0-cp313-cp313-win32.whl", hash = "sha256:ccb42ca0a4a46232d716779421bbebbcad23c08d37c980f02cc3a6bd115ad277"}, - {file = "pyzmq-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e1e5d0a25aea8b691a00d6b54b28ac514c8cc0d8646d05f7ca6cb64b97358250"}, - {file = "pyzmq-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:fc82269d24860cfa859b676d18850cbb8e312dcd7eada09e7d5b007e2f3d9eb1"}, - {file = "pyzmq-26.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:416ac51cabd54f587995c2b05421324700b22e98d3d0aa2cfaec985524d16f1d"}, - {file = "pyzmq-26.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:ff832cce719edd11266ca32bc74a626b814fff236824aa1aeaad399b69fe6eae"}, - {file = "pyzmq-26.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:393daac1bcf81b2a23e696b7b638eedc965e9e3d2112961a072b6cd8179ad2eb"}, - {file = "pyzmq-26.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9869fa984c8670c8ab899a719eb7b516860a29bc26300a84d24d8c1b71eae3ec"}, - {file = "pyzmq-26.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b3b8e36fd4c32c0825b4461372949ecd1585d326802b1321f8b6dc1d7e9318c"}, - {file = "pyzmq-26.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3ee647d84b83509b7271457bb428cc347037f437ead4b0b6e43b5eba35fec0aa"}, - {file = "pyzmq-26.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:45cb1a70eb00405ce3893041099655265fabcd9c4e1e50c330026e82257892c1"}, - {file = "pyzmq-26.1.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:5cca7b4adb86d7470e0fc96037771981d740f0b4cb99776d5cb59cd0e6684a73"}, - {file = "pyzmq-26.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:91d1a20bdaf3b25f3173ff44e54b1cfbc05f94c9e8133314eb2962a89e05d6e3"}, - {file = "pyzmq-26.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c0665d85535192098420428c779361b8823d3d7ec4848c6af3abb93bc5c915bf"}, - {file = "pyzmq-26.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:96d7c1d35ee4a495df56c50c83df7af1c9688cce2e9e0edffdbf50889c167595"}, - {file = "pyzmq-26.1.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b281b5ff5fcc9dcbfe941ac5c7fcd4b6c065adad12d850f95c9d6f23c2652384"}, - {file = "pyzmq-26.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5384c527a9a004445c5074f1e20db83086c8ff1682a626676229aafd9cf9f7d1"}, - {file = "pyzmq-26.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:754c99a9840839375ee251b38ac5964c0f369306eddb56804a073b6efdc0cd88"}, - {file = "pyzmq-26.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9bdfcb74b469b592972ed881bad57d22e2c0acc89f5e8c146782d0d90fb9f4bf"}, - {file = "pyzmq-26.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bd13f0231f4788db619347b971ca5f319c5b7ebee151afc7c14632068c6261d3"}, - {file = "pyzmq-26.1.0-cp37-cp37m-win32.whl", hash = "sha256:c5668dac86a869349828db5fc928ee3f58d450dce2c85607067d581f745e4fb1"}, - {file = "pyzmq-26.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad875277844cfaeca7fe299ddf8c8d8bfe271c3dc1caf14d454faa5cdbf2fa7a"}, - {file = "pyzmq-26.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:65c6e03cc0222eaf6aad57ff4ecc0a070451e23232bb48db4322cc45602cede0"}, - {file = "pyzmq-26.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:038ae4ffb63e3991f386e7fda85a9baab7d6617fe85b74a8f9cab190d73adb2b"}, - {file = "pyzmq-26.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bdeb2c61611293f64ac1073f4bf6723b67d291905308a7de9bb2ca87464e3273"}, - {file = "pyzmq-26.1.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:61dfa5ee9d7df297c859ac82b1226d8fefaf9c5113dc25c2c00ecad6feeeb04f"}, - {file = "pyzmq-26.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3292d384537b9918010769b82ab3e79fca8b23d74f56fc69a679106a3e2c2cf"}, - {file = "pyzmq-26.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f9499c70c19ff0fbe1007043acb5ad15c1dec7d8e84ab429bca8c87138e8f85c"}, - {file = "pyzmq-26.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d3dd5523ed258ad58fed7e364c92a9360d1af8a9371e0822bd0146bdf017ef4c"}, - {file = "pyzmq-26.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baba2fd199b098c5544ef2536b2499d2e2155392973ad32687024bd8572a7d1c"}, - {file = "pyzmq-26.1.0-cp38-cp38-win32.whl", hash = "sha256:ddbb2b386128d8eca92bd9ca74e80f73fe263bcca7aa419f5b4cbc1661e19741"}, - {file = "pyzmq-26.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:79e45a4096ec8388cdeb04a9fa5e9371583bcb826964d55b8b66cbffe7b33c86"}, - {file = "pyzmq-26.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:add52c78a12196bc0fda2de087ba6c876ea677cbda2e3eba63546b26e8bf177b"}, - {file = "pyzmq-26.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c03bd7f3339ff47de7ea9ac94a2b34580a8d4df69b50128bb6669e1191a895"}, - {file = "pyzmq-26.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dcc37d9d708784726fafc9c5e1232de655a009dbf97946f117aefa38d5985a0f"}, - {file = "pyzmq-26.1.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5a6ed52f0b9bf8dcc64cc82cce0607a3dfed1dbb7e8c6f282adfccc7be9781de"}, - {file = "pyzmq-26.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451e16ae8bea3d95649317b463c9f95cd9022641ec884e3d63fc67841ae86dfe"}, - {file = "pyzmq-26.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:906e532c814e1d579138177a00ae835cd6becbf104d45ed9093a3aaf658f6a6a"}, - {file = "pyzmq-26.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05bacc4f94af468cc82808ae3293390278d5f3375bb20fef21e2034bb9a505b6"}, - {file = "pyzmq-26.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:57bb2acba798dc3740e913ffadd56b1fcef96f111e66f09e2a8db3050f1f12c8"}, - {file = "pyzmq-26.1.0-cp39-cp39-win32.whl", hash = "sha256:f774841bb0e8588505002962c02da420bcfb4c5056e87a139c6e45e745c0e2e2"}, - {file = "pyzmq-26.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:359c533bedc62c56415a1f5fcfd8279bc93453afdb0803307375ecf81c962402"}, - {file = "pyzmq-26.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:7907419d150b19962138ecec81a17d4892ea440c184949dc29b358bc730caf69"}, - {file = "pyzmq-26.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b24079a14c9596846bf7516fe75d1e2188d4a528364494859106a33d8b48be38"}, - {file = "pyzmq-26.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59d0acd2976e1064f1b398a00e2c3e77ed0a157529779e23087d4c2fb8aaa416"}, - {file = "pyzmq-26.1.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:911c43a4117915203c4cc8755e0f888e16c4676a82f61caee2f21b0c00e5b894"}, - {file = "pyzmq-26.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10163e586cc609f5f85c9b233195554d77b1e9a0801388907441aaeb22841c5"}, - {file = "pyzmq-26.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:28a8b2abb76042f5fd7bd720f7fea48c0fd3e82e9de0a1bf2c0de3812ce44a42"}, - {file = "pyzmq-26.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bef24d3e4ae2c985034439f449e3f9e06bf579974ce0e53d8a507a1577d5b2ab"}, - {file = "pyzmq-26.1.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2cd0f4d314f4a2518e8970b6f299ae18cff7c44d4a1fc06fc713f791c3a9e3ea"}, - {file = "pyzmq-26.1.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fa25a620eed2a419acc2cf10135b995f8f0ce78ad00534d729aa761e4adcef8a"}, - {file = "pyzmq-26.1.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef3b048822dca6d231d8a8ba21069844ae38f5d83889b9b690bf17d2acc7d099"}, - {file = "pyzmq-26.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:9a6847c92d9851b59b9f33f968c68e9e441f9a0f8fc972c5580c5cd7cbc6ee24"}, - {file = "pyzmq-26.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9b9305004d7e4e6a824f4f19b6d8f32b3578aad6f19fc1122aaf320cbe3dc83"}, - {file = "pyzmq-26.1.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:63c1d3a65acb2f9c92dce03c4e1758cc552f1ae5c78d79a44e3bb88d2fa71f3a"}, - {file = "pyzmq-26.1.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d36b8fffe8b248a1b961c86fbdfa0129dfce878731d169ede7fa2631447331be"}, - {file = "pyzmq-26.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67976d12ebfd61a3bc7d77b71a9589b4d61d0422282596cf58c62c3866916544"}, - {file = "pyzmq-26.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:998444debc8816b5d8d15f966e42751032d0f4c55300c48cc337f2b3e4f17d03"}, - {file = "pyzmq-26.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5c88b2f13bcf55fee78ea83567b9fe079ba1a4bef8b35c376043440040f7edb"}, - {file = "pyzmq-26.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d906d43e1592be4b25a587b7d96527cb67277542a5611e8ea9e996182fae410"}, - {file = "pyzmq-26.1.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b0c9942430d731c786545da6be96d824a41a51742e3e374fedd9018ea43106"}, - {file = "pyzmq-26.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:314d11564c00b77f6224d12eb3ddebe926c301e86b648a1835c5b28176c83eab"}, - {file = "pyzmq-26.1.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:093a1a3cae2496233f14b57f4b485da01b4ff764582c854c0f42c6dd2be37f3d"}, - {file = "pyzmq-26.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3c397b1b450f749a7e974d74c06d69bd22dd362142f370ef2bd32a684d6b480c"}, - {file = "pyzmq-26.1.0.tar.gz", hash = "sha256:6c5aeea71f018ebd3b9115c7cb13863dd850e98ca6b9258509de1246461a7e7f"}, -] - -[package.dependencies] -cffi = {version = "*", markers = "implementation_name == \"pypy\""} - -[[package]] -name = "qdrant-client" -version = "1.11.0" -description = "Client library for the Qdrant vector search engine" -optional = false -python-versions = ">=3.8" -files = [ - {file = "qdrant_client-1.11.0-py3-none-any.whl", hash = "sha256:1f574ccebb91c0bc8a620c9a41a5a010084fbc4d8c6f1cd0ab7b2eeb97336fc0"}, - {file = "qdrant_client-1.11.0.tar.gz", hash = "sha256:7c1d4d7a96cfd1ee0cde2a21c607e9df86bcca795ad8d1fd274d295ab64b8458"}, -] - -[package.dependencies] -grpcio = ">=1.41.0" -grpcio-tools = ">=1.41.0" -httpx = {version = ">=0.20.0", extras = ["http2"]} -numpy = [ - {version = ">=1.21", markers = "python_version >= \"3.8\" and python_version < \"3.12\""}, - {version = ">=1.26", markers = "python_version >= \"3.12\""}, -] -portalocker = ">=2.7.0,<3.0.0" -pydantic = ">=1.10.8" -urllib3 = ">=1.26.14,<3" - -[package.extras] -fastembed = ["fastembed (==0.3.4)"] -fastembed-gpu = ["fastembed-gpu (==0.3.4)"] - -[[package]] -name = "redis" -version = "5.0.8" -description = "Python client for Redis database and key-value store" -optional = false -python-versions = ">=3.7" -files = [ - {file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"}, - {file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"}, -] - -[package.dependencies] -async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} -hiredis = {version = ">1.0.0", optional = true, markers = "extra == \"hiredis\""} - -[package.extras] -hiredis = ["hiredis (>1.0.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] - -[[package]] -name = "referencing" -version = "0.35.1" -description = "JSON Referencing + Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, - {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -rpds-py = ">=0.7.0" - -[[package]] -name = "regex" -version = "2024.7.24" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.8" -files = [ - {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"}, - {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"}, - {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"}, - {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"}, - {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"}, - {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"}, - {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"}, - {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"}, - {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"}, - {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"}, - {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"}, - {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"}, - {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"}, - {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"}, -] - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -description = "OAuthlib authentication support for Requests." -optional = false -python-versions = ">=3.4" -files = [ - {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, - {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, -] - -[package.dependencies] -oauthlib = ">=3.0.0" -requests = ">=2.0.0" - -[package.extras] -rsa = ["oauthlib[signedtoken] (>=3.0.0)"] - -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -description = "A pure python RFC3339 validator" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, - {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, -] - -[package.dependencies] -six = "*" - -[[package]] -name = "rich" -version = "13.7.1" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "rpds-py" -version = "0.20.0" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, - {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, - {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, - {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, - {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, - {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, - {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, - {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, - {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, - {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, - {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, - {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, - {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, - {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, - {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, - {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, - {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, - {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, - {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, - {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, - {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, - {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, - {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, - {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, - {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, - {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, - {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, - {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, - {file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"}, - {file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"}, - {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"}, - {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"}, - {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"}, - {file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"}, - {file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"}, - {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, - {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, - {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, - {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, - {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, - {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, - {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, - {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, -] - -[[package]] -name = "rsa" -version = "4.9" -description = "Pure-Python RSA implementation" -optional = false -python-versions = ">=3.6,<4" -files = [ - {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, - {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, -] - -[package.dependencies] -pyasn1 = ">=0.1.3" - -[[package]] -name = "ruamel-yaml" -version = "0.18.6" -description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, - {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, -] - -[package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} - -[package.extras] -docs = ["mercurial (>5.7)", "ryd"] -jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.8" -description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -optional = false -python-versions = ">=3.6" -files = [ - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, - {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, - {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, -] - -[[package]] -name = "ruff" -version = "0.5.7" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, - {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, - {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, - {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, - {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, - {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, - {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, -] - -[[package]] -name = "safetensors" -version = "0.4.4" -description = "" -optional = false -python-versions = ">=3.7" -files = [ - {file = "safetensors-0.4.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2adb497ada13097f30e386e88c959c0fda855a5f6f98845710f5bb2c57e14f12"}, - {file = "safetensors-0.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7db7fdc2d71fd1444d85ca3f3d682ba2df7d61a637dfc6d80793f439eae264ab"}, - {file = "safetensors-0.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4f0eed76b430f009fbefca1a0028ddb112891b03cb556d7440d5cd68eb89a9"}, - {file = "safetensors-0.4.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d216fab0b5c432aabf7170883d7c11671622bde8bd1436c46d633163a703f6"}, - {file = "safetensors-0.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d9b76322e49c056bcc819f8bdca37a2daa5a6d42c07f30927b501088db03309"}, - {file = "safetensors-0.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32f0d1f6243e90ee43bc6ee3e8c30ac5b09ca63f5dd35dbc985a1fc5208c451a"}, - {file = "safetensors-0.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d464bdc384874601a177375028012a5f177f1505279f9456fea84bbc575c7f"}, - {file = "safetensors-0.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63144e36209ad8e4e65384dbf2d52dd5b1866986079c00a72335402a38aacdc5"}, - {file = "safetensors-0.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:051d5ecd490af7245258000304b812825974d5e56f14a3ff7e1b8b2ba6dc2ed4"}, - {file = "safetensors-0.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51bc8429d9376224cd3cf7e8ce4f208b4c930cd10e515b6ac6a72cbc3370f0d9"}, - {file = "safetensors-0.4.4-cp310-none-win32.whl", hash = "sha256:fb7b54830cee8cf9923d969e2df87ce20e625b1af2fd194222ab902d3adcc29c"}, - {file = "safetensors-0.4.4-cp310-none-win_amd64.whl", hash = "sha256:4b3e8aa8226d6560de8c2b9d5ff8555ea482599c670610758afdc97f3e021e9c"}, - {file = "safetensors-0.4.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bbaa31f2cb49013818bde319232ccd72da62ee40f7d2aa532083eda5664e85ff"}, - {file = "safetensors-0.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9fdcb80f4e9fbb33b58e9bf95e7dbbedff505d1bcd1c05f7c7ce883632710006"}, - {file = "safetensors-0.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55c14c20be247b8a1aeaf3ab4476265e3ca83096bb8e09bb1a7aa806088def4f"}, - {file = "safetensors-0.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:949aaa1118660f992dbf0968487b3e3cfdad67f948658ab08c6b5762e90cc8b6"}, - {file = "safetensors-0.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c11a4ab7debc456326a2bac67f35ee0ac792bcf812c7562a4a28559a5c795e27"}, - {file = "safetensors-0.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0cea44bba5c5601b297bc8307e4075535b95163402e4906b2e9b82788a2a6df"}, - {file = "safetensors-0.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9d752c97f6bbe327352f76e5b86442d776abc789249fc5e72eacb49e6916482"}, - {file = "safetensors-0.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03f2bb92e61b055ef6cc22883ad1ae898010a95730fa988c60a23800eb742c2c"}, - {file = "safetensors-0.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:87bf3f91a9328a941acc44eceffd4e1f5f89b030985b2966637e582157173b98"}, - {file = "safetensors-0.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:20d218ec2b6899d29d6895419a58b6e44cc5ff8f0cc29fac8d236a8978ab702e"}, - {file = "safetensors-0.4.4-cp311-none-win32.whl", hash = "sha256:8079486118919f600c603536e2490ca37b3dbd3280e3ad6eaacfe6264605ac8a"}, - {file = "safetensors-0.4.4-cp311-none-win_amd64.whl", hash = "sha256:2f8c2eb0615e2e64ee27d478c7c13f51e5329d7972d9e15528d3e4cfc4a08f0d"}, - {file = "safetensors-0.4.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:baec5675944b4a47749c93c01c73d826ef7d42d36ba8d0dba36336fa80c76426"}, - {file = "safetensors-0.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f15117b96866401825f3e94543145028a2947d19974429246ce59403f49e77c6"}, - {file = "safetensors-0.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a13a9caea485df164c51be4eb0c87f97f790b7c3213d635eba2314d959fe929"}, - {file = "safetensors-0.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b54bc4ca5f9b9bba8cd4fb91c24b2446a86b5ae7f8975cf3b7a277353c3127c"}, - {file = "safetensors-0.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:08332c22e03b651c8eb7bf5fc2de90044f3672f43403b3d9ac7e7e0f4f76495e"}, - {file = "safetensors-0.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb62841e839ee992c37bb75e75891c7f4904e772db3691c59daaca5b4ab960e1"}, - {file = "safetensors-0.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5b927acc5f2f59547270b0309a46d983edc44be64e1ca27a7fcb0474d6cd67"}, - {file = "safetensors-0.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a69c71b1ae98a8021a09a0b43363b0143b0ce74e7c0e83cacba691b62655fb8"}, - {file = "safetensors-0.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23654ad162c02a5636f0cd520a0310902c4421aab1d91a0b667722a4937cc445"}, - {file = "safetensors-0.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0677c109d949cf53756859160b955b2e75b0eefe952189c184d7be30ecf7e858"}, - {file = "safetensors-0.4.4-cp312-none-win32.whl", hash = "sha256:a51d0ddd4deb8871c6de15a772ef40b3dbd26a3c0451bb9e66bc76fc5a784e5b"}, - {file = "safetensors-0.4.4-cp312-none-win_amd64.whl", hash = "sha256:2d065059e75a798bc1933c293b68d04d79b586bb7f8c921e0ca1e82759d0dbb1"}, - {file = "safetensors-0.4.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9d625692578dd40a112df30c02a1adf068027566abd8e6a74893bb13d441c150"}, - {file = "safetensors-0.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7cabcf39c81e5b988d0adefdaea2eb9b4fd9bd62d5ed6559988c62f36bfa9a89"}, - {file = "safetensors-0.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8359bef65f49d51476e9811d59c015f0ddae618ee0e44144f5595278c9f8268c"}, - {file = "safetensors-0.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a32c662e7df9226fd850f054a3ead0e4213a96a70b5ce37b2d26ba27004e013"}, - {file = "safetensors-0.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c329a4dcc395364a1c0d2d1574d725fe81a840783dda64c31c5a60fc7d41472c"}, - {file = "safetensors-0.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:239ee093b1db877c9f8fe2d71331a97f3b9c7c0d3ab9f09c4851004a11f44b65"}, - {file = "safetensors-0.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd574145d930cf9405a64f9923600879a5ce51d9f315443a5f706374841327b6"}, - {file = "safetensors-0.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f6784eed29f9e036acb0b7769d9e78a0dc2c72c2d8ba7903005350d817e287a4"}, - {file = "safetensors-0.4.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:65a4a6072436bf0a4825b1c295d248cc17e5f4651e60ee62427a5bcaa8622a7a"}, - {file = "safetensors-0.4.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:df81e3407630de060ae8313da49509c3caa33b1a9415562284eaf3d0c7705f9f"}, - {file = "safetensors-0.4.4-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:e4a0f374200e8443d9746e947ebb346c40f83a3970e75a685ade0adbba5c48d9"}, - {file = "safetensors-0.4.4-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:181fb5f3dee78dae7fd7ec57d02e58f7936498d587c6b7c1c8049ef448c8d285"}, - {file = "safetensors-0.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb4ac1d8f6b65ec84ddfacd275079e89d9df7c92f95675ba96c4f790a64df6e"}, - {file = "safetensors-0.4.4-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76897944cd9239e8a70955679b531b9a0619f76e25476e57ed373322d9c2075d"}, - {file = "safetensors-0.4.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a9e9d1a27e51a0f69e761a3d581c3af46729ec1c988fa1f839e04743026ae35"}, - {file = "safetensors-0.4.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:005ef9fc0f47cb9821c40793eb029f712e97278dae84de91cb2b4809b856685d"}, - {file = "safetensors-0.4.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26987dac3752688c696c77c3576f951dbbdb8c57f0957a41fb6f933cf84c0b62"}, - {file = "safetensors-0.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c05270b290acd8d249739f40d272a64dd597d5a4b90f27d830e538bc2549303c"}, - {file = "safetensors-0.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:068d3a33711fc4d93659c825a04480ff5a3854e1d78632cdc8f37fee917e8a60"}, - {file = "safetensors-0.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:063421ef08ca1021feea8b46951251b90ae91f899234dd78297cbe7c1db73b99"}, - {file = "safetensors-0.4.4-cp37-none-win32.whl", hash = "sha256:d52f5d0615ea83fd853d4e1d8acf93cc2e0223ad4568ba1e1f6ca72e94ea7b9d"}, - {file = "safetensors-0.4.4-cp37-none-win_amd64.whl", hash = "sha256:88a5ac3280232d4ed8e994cbc03b46a1807ce0aa123867b40c4a41f226c61f94"}, - {file = "safetensors-0.4.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3467ab511bfe3360967d7dc53b49f272d59309e57a067dd2405b4d35e7dcf9dc"}, - {file = "safetensors-0.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2ab4c96d922e53670ce25fbb9b63d5ea972e244de4fa1dd97b590d9fd66aacef"}, - {file = "safetensors-0.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87df18fce4440477c3ef1fd7ae17c704a69a74a77e705a12be135ee0651a0c2d"}, - {file = "safetensors-0.4.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e5fe345b2bc7d88587149ac11def1f629d2671c4c34f5df38aed0ba59dc37f8"}, - {file = "safetensors-0.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f1a3e01dce3cd54060791e7e24588417c98b941baa5974700eeb0b8eb65b0a0"}, - {file = "safetensors-0.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6bf35e9a8998d8339fd9a05ac4ce465a4d2a2956cc0d837b67c4642ed9e947"}, - {file = "safetensors-0.4.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:166c0c52f6488b8538b2a9f3fbc6aad61a7261e170698779b371e81b45f0440d"}, - {file = "safetensors-0.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:87e9903b8668a16ef02c08ba4ebc91e57a49c481e9b5866e31d798632805014b"}, - {file = "safetensors-0.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9c421153aa23c323bd8483d4155b4eee82c9a50ac11cccd83539104a8279c64"}, - {file = "safetensors-0.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a4b8617499b2371c7353302c5116a7e0a3a12da66389ce53140e607d3bf7b3d3"}, - {file = "safetensors-0.4.4-cp38-none-win32.whl", hash = "sha256:c6280f5aeafa1731f0a3709463ab33d8e0624321593951aefada5472f0b313fd"}, - {file = "safetensors-0.4.4-cp38-none-win_amd64.whl", hash = "sha256:6ceed6247fc2d33b2a7b7d25d8a0fe645b68798856e0bc7a9800c5fd945eb80f"}, - {file = "safetensors-0.4.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5cf6c6f6193797372adf50c91d0171743d16299491c75acad8650107dffa9269"}, - {file = "safetensors-0.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:419010156b914a3e5da4e4adf992bee050924d0fe423c4b329e523e2c14c3547"}, - {file = "safetensors-0.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88f6fd5a5c1302ce79993cc5feeadcc795a70f953c762544d01fb02b2db4ea33"}, - {file = "safetensors-0.4.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d468cffb82d90789696d5b4d8b6ab8843052cba58a15296691a7a3df55143cd2"}, - {file = "safetensors-0.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9353c2af2dd467333d4850a16edb66855e795561cd170685178f706c80d2c71e"}, - {file = "safetensors-0.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83c155b4a33368d9b9c2543e78f2452090fb030c52401ca608ef16fa58c98353"}, - {file = "safetensors-0.4.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9850754c434e636ce3dc586f534bb23bcbd78940c304775bee9005bf610e98f1"}, - {file = "safetensors-0.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:275f500b4d26f67b6ec05629a4600645231bd75e4ed42087a7c1801bff04f4b3"}, - {file = "safetensors-0.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5c2308de665b7130cd0e40a2329278226e4cf083f7400c51ca7e19ccfb3886f3"}, - {file = "safetensors-0.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e06a9ebc8656e030ccfe44634f2a541b4b1801cd52e390a53ad8bacbd65f8518"}, - {file = "safetensors-0.4.4-cp39-none-win32.whl", hash = "sha256:ef73df487b7c14b477016947c92708c2d929e1dee2bacdd6fff5a82ed4539537"}, - {file = "safetensors-0.4.4-cp39-none-win_amd64.whl", hash = "sha256:83d054818a8d1198d8bd8bc3ea2aac112a2c19def2bf73758321976788706398"}, - {file = "safetensors-0.4.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1d1f34c71371f0e034004a0b583284b45d233dd0b5f64a9125e16b8a01d15067"}, - {file = "safetensors-0.4.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a8043a33d58bc9b30dfac90f75712134ca34733ec3d8267b1bd682afe7194f5"}, - {file = "safetensors-0.4.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8db8f0c59c84792c12661f8efa85de160f80efe16b87a9d5de91b93f9e0bce3c"}, - {file = "safetensors-0.4.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfc1fc38e37630dd12d519bdec9dcd4b345aec9930bb9ce0ed04461f49e58b52"}, - {file = "safetensors-0.4.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5c9d86d9b13b18aafa88303e2cd21e677f5da2a14c828d2c460fe513af2e9a5"}, - {file = "safetensors-0.4.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:43251d7f29a59120a26f5a0d9583b9e112999e500afabcfdcb91606d3c5c89e3"}, - {file = "safetensors-0.4.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:2c42e9b277513b81cf507e6121c7b432b3235f980cac04f39f435b7902857f91"}, - {file = "safetensors-0.4.4-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3daacc9a4e3f428a84dd56bf31f20b768eb0b204af891ed68e1f06db9edf546f"}, - {file = "safetensors-0.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:218bbb9b883596715fc9997bb42470bf9f21bb832c3b34c2bf744d6fa8f2bbba"}, - {file = "safetensors-0.4.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bd5efc26b39f7fc82d4ab1d86a7f0644c8e34f3699c33f85bfa9a717a030e1b"}, - {file = "safetensors-0.4.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56ad9776b65d8743f86698a1973292c966cf3abff627efc44ed60e66cc538ddd"}, - {file = "safetensors-0.4.4-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:30f23e6253c5f43a809dea02dc28a9f5fa747735dc819f10c073fe1b605e97d4"}, - {file = "safetensors-0.4.4-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5512078d00263de6cb04e9d26c9ae17611098f52357fea856213e38dc462f81f"}, - {file = "safetensors-0.4.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b96c3d9266439d17f35fc2173111d93afc1162f168e95aed122c1ca517b1f8f1"}, - {file = "safetensors-0.4.4-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:08d464aa72a9a13826946b4fb9094bb4b16554bbea2e069e20bd903289b6ced9"}, - {file = "safetensors-0.4.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:210160816d5a36cf41f48f38473b6f70d7bcb4b0527bedf0889cc0b4c3bb07db"}, - {file = "safetensors-0.4.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb276a53717f2bcfb6df0bcf284d8a12069002508d4c1ca715799226024ccd45"}, - {file = "safetensors-0.4.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2c28c6487f17d8db0089e8b2cdc13de859366b94cc6cdc50e1b0a4147b56551"}, - {file = "safetensors-0.4.4-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7915f0c60e4e6e65d90f136d85dd3b429ae9191c36b380e626064694563dbd9f"}, - {file = "safetensors-0.4.4-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:00eea99ae422fbfa0b46065acbc58b46bfafadfcec179d4b4a32d5c45006af6c"}, - {file = "safetensors-0.4.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bb1ed4fcb0b3c2f3ea2c5767434622fe5d660e5752f21ac2e8d737b1e5e480bb"}, - {file = "safetensors-0.4.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:73fc9a0a4343188bdb421783e600bfaf81d0793cd4cce6bafb3c2ed567a74cd5"}, - {file = "safetensors-0.4.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c37e6b714200824c73ca6eaf007382de76f39466a46e97558b8dc4cf643cfbf"}, - {file = "safetensors-0.4.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f75698c5c5c542417ac4956acfc420f7d4a2396adca63a015fd66641ea751759"}, - {file = "safetensors-0.4.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca1a209157f242eb183e209040097118472e169f2e069bfbd40c303e24866543"}, - {file = "safetensors-0.4.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:177f2b60a058f92a3cec7a1786c9106c29eca8987ecdfb79ee88126e5f47fa31"}, - {file = "safetensors-0.4.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ee9622e84fe6e4cd4f020e5fda70d6206feff3157731df7151d457fdae18e541"}, - {file = "safetensors-0.4.4.tar.gz", hash = "sha256:5fe3e9b705250d0172ed4e100a811543108653fb2b66b9e702a088ad03772a07"}, -] - -[package.extras] -all = ["safetensors[jax]", "safetensors[numpy]", "safetensors[paddlepaddle]", "safetensors[pinned-tf]", "safetensors[quality]", "safetensors[testing]", "safetensors[torch]"] -dev = ["safetensors[all]"] -jax = ["flax (>=0.6.3)", "jax (>=0.3.25)", "jaxlib (>=0.3.25)", "safetensors[numpy]"] -mlx = ["mlx (>=0.0.9)"] -numpy = ["numpy (>=1.21.6)"] -paddlepaddle = ["paddlepaddle (>=2.4.1)", "safetensors[numpy]"] -pinned-tf = ["safetensors[numpy]", "tensorflow (==2.11.0)"] -quality = ["black (==22.3)", "click (==8.0.4)", "flake8 (>=3.8.3)", "isort (>=5.5.4)"] -tensorflow = ["safetensors[numpy]", "tensorflow (>=2.11.0)"] -testing = ["h5py (>=3.7.0)", "huggingface-hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools-rust (>=1.5.2)"] -torch = ["safetensors[numpy]", "torch (>=1.10)"] - -[[package]] -name = "scikit-learn" -version = "1.5.1" -description = "A set of python modules for machine learning and data mining" -optional = false -python-versions = ">=3.9" -files = [ - {file = "scikit_learn-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:781586c414f8cc58e71da4f3d7af311e0505a683e112f2f62919e3019abd3745"}, - {file = "scikit_learn-1.5.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f5b213bc29cc30a89a3130393b0e39c847a15d769d6e59539cd86b75d276b1a7"}, - {file = "scikit_learn-1.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ff4ba34c2abff5ec59c803ed1d97d61b036f659a17f55be102679e88f926fac"}, - {file = "scikit_learn-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:161808750c267b77b4a9603cf9c93579c7a74ba8486b1336034c2f1579546d21"}, - {file = "scikit_learn-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:10e49170691514a94bb2e03787aa921b82dbc507a4ea1f20fd95557862c98dc1"}, - {file = "scikit_learn-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:154297ee43c0b83af12464adeab378dee2d0a700ccd03979e2b821e7dd7cc1c2"}, - {file = "scikit_learn-1.5.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b5e865e9bd59396220de49cb4a57b17016256637c61b4c5cc81aaf16bc123bbe"}, - {file = "scikit_learn-1.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909144d50f367a513cee6090873ae582dba019cb3fca063b38054fa42704c3a4"}, - {file = "scikit_learn-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:689b6f74b2c880276e365fe84fe4f1befd6a774f016339c65655eaff12e10cbf"}, - {file = "scikit_learn-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:9a07f90846313a7639af6a019d849ff72baadfa4c74c778821ae0fad07b7275b"}, - {file = "scikit_learn-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5944ce1faada31c55fb2ba20a5346b88e36811aab504ccafb9f0339e9f780395"}, - {file = "scikit_learn-1.5.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0828673c5b520e879f2af6a9e99eee0eefea69a2188be1ca68a6121b809055c1"}, - {file = "scikit_learn-1.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:508907e5f81390e16d754e8815f7497e52139162fd69c4fdbd2dfa5d6cc88915"}, - {file = "scikit_learn-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97625f217c5c0c5d0505fa2af28ae424bd37949bb2f16ace3ff5f2f81fb4498b"}, - {file = "scikit_learn-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:da3f404e9e284d2b0a157e1b56b6566a34eb2798205cba35a211df3296ab7a74"}, - {file = "scikit_learn-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:88e0672c7ac21eb149d409c74cc29f1d611d5158175846e7a9c2427bd12b3956"}, - {file = "scikit_learn-1.5.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:7b073a27797a283187a4ef4ee149959defc350b46cbf63a84d8514fe16b69855"}, - {file = "scikit_learn-1.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b59e3e62d2be870e5c74af4e793293753565c7383ae82943b83383fdcf5cc5c1"}, - {file = "scikit_learn-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd8d3a19d4bd6dc5a7d4f358c8c3a60934dc058f363c34c0ac1e9e12a31421d"}, - {file = "scikit_learn-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:5f57428de0c900a98389c4a433d4a3cf89de979b3aa24d1c1d251802aa15e44d"}, - {file = "scikit_learn-1.5.1.tar.gz", hash = "sha256:0ea5d40c0e3951df445721927448755d3fe1d80833b0b7308ebff5d2a45e6414"}, -] - -[package.dependencies] -joblib = ">=1.2.0" -numpy = ">=1.19.5" -scipy = ">=1.6.0" -threadpoolctl = ">=3.1.0" - -[package.extras] -benchmark = ["matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "pandas (>=1.1.5)"] -build = ["cython (>=3.0.10)", "meson-python (>=0.16.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)"] -docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "polars (>=0.20.23)", "pooch (>=1.6.0)", "pydata-sphinx-theme (>=0.15.3)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=7.3.7)", "sphinx-copybutton (>=0.5.2)", "sphinx-design (>=0.5.0)", "sphinx-gallery (>=0.16.0)", "sphinx-prompt (>=1.4.0)", "sphinx-remove-toctrees (>=1.0.0.post1)", "sphinxcontrib-sass (>=0.3.4)", "sphinxext-opengraph (>=0.9.1)"] -examples = ["matplotlib (>=3.3.4)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)"] -install = ["joblib (>=1.2.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)", "threadpoolctl (>=3.1.0)"] -maintenance = ["conda-lock (==2.5.6)"] -tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.20.23)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.2.1)", "scikit-image (>=0.17.2)"] - -[[package]] -name = "scipy" -version = "1.14.0" -description = "Fundamental algorithms for scientific computing in Python" -optional = false -python-versions = ">=3.10" -files = [ - {file = "scipy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e911933d54ead4d557c02402710c2396529540b81dd554fc1ba270eb7308484"}, - {file = "scipy-1.14.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:687af0a35462402dd851726295c1a5ae5f987bd6e9026f52e9505994e2f84ef6"}, - {file = "scipy-1.14.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:07e179dc0205a50721022344fb85074f772eadbda1e1b3eecdc483f8033709b7"}, - {file = "scipy-1.14.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a9c9a9b226d9a21e0a208bdb024c3982932e43811b62d202aaf1bb59af264b1"}, - {file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076c27284c768b84a45dcf2e914d4000aac537da74236a0d45d82c6fa4b7b3c0"}, - {file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42470ea0195336df319741e230626b6225a740fd9dce9642ca13e98f667047c0"}, - {file = "scipy-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:176c6f0d0470a32f1b2efaf40c3d37a24876cebf447498a4cefb947a79c21e9d"}, - {file = "scipy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad36af9626d27a4326c8e884917b7ec321d8a1841cd6dacc67d2a9e90c2f0359"}, - {file = "scipy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d056a8709ccda6cf36cdd2eac597d13bc03dba38360f418560a93050c76a16e"}, - {file = "scipy-1.14.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f0a50da861a7ec4573b7c716b2ebdcdf142b66b756a0d392c236ae568b3a93fb"}, - {file = "scipy-1.14.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94c164a9e2498e68308e6e148646e486d979f7fcdb8b4cf34b5441894bdb9caf"}, - {file = "scipy-1.14.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a7d46c3e0aea5c064e734c3eac5cf9eb1f8c4ceee756262f2c7327c4c2691c86"}, - {file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eee2989868e274aae26125345584254d97c56194c072ed96cb433f32f692ed8"}, - {file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3154691b9f7ed73778d746da2df67a19d046a6c8087c8b385bc4cdb2cfca74"}, - {file = "scipy-1.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c40003d880f39c11c1edbae8144e3813904b10514cd3d3d00c277ae996488cdb"}, - {file = "scipy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:5b083c8940028bb7e0b4172acafda6df762da1927b9091f9611b0bcd8676f2bc"}, - {file = "scipy-1.14.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff2438ea1330e06e53c424893ec0072640dac00f29c6a43a575cbae4c99b2b9"}, - {file = "scipy-1.14.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bbc0471b5f22c11c389075d091d3885693fd3f5e9a54ce051b46308bc787e5d4"}, - {file = "scipy-1.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:64b2ff514a98cf2bb734a9f90d32dc89dc6ad4a4a36a312cd0d6327170339eb0"}, - {file = "scipy-1.14.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:7d3da42fbbbb860211a811782504f38ae7aaec9de8764a9bef6b262de7a2b50f"}, - {file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d91db2c41dd6c20646af280355d41dfa1ec7eead235642178bd57635a3f82209"}, - {file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a01cc03bcdc777c9da3cfdcc74b5a75caffb48a6c39c8450a9a05f82c4250a14"}, - {file = "scipy-1.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:65df4da3c12a2bb9ad52b86b4dcf46813e869afb006e58be0f516bc370165159"}, - {file = "scipy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:4c4161597c75043f7154238ef419c29a64ac4a7c889d588ea77690ac4d0d9b20"}, - {file = "scipy-1.14.0.tar.gz", hash = "sha256:b5923f48cb840380f9854339176ef21763118a7300a88203ccd0bdd26e58527b"}, -] - -[package.dependencies] -numpy = ">=1.23.5,<2.3" - -[package.extras] -dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] -doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] -test = ["Cython", "array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] - -[[package]] -name = "sentence-transformers" -version = "2.7.0" -description = "Multilingual text embeddings" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "sentence_transformers-2.7.0-py3-none-any.whl", hash = "sha256:6a7276b05a95931581bbfa4ba49d780b2cf6904fa4a171ec7fd66c343f761c98"}, - {file = "sentence_transformers-2.7.0.tar.gz", hash = "sha256:2f7df99d1c021dded471ed2d079e9d1e4fc8e30ecb06f957be060511b36f24ea"}, -] - -[package.dependencies] -huggingface-hub = ">=0.15.1" -numpy = "*" -Pillow = "*" -scikit-learn = "*" -scipy = "*" -torch = ">=1.11.0" -tqdm = "*" -transformers = ">=4.34.0,<5.0.0" - -[package.extras] -dev = ["pre-commit", "pytest", "ruff (>=0.3.0)"] - -[[package]] -name = "setuptools" -version = "72.1.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"}, - {file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"}, -] - -[package.extras] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "shapely" -version = "2.0.5" -description = "Manipulation and analysis of geometric objects" -optional = false -python-versions = ">=3.7" -files = [ - {file = "shapely-2.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89d34787c44f77a7d37d55ae821f3a784fa33592b9d217a45053a93ade899375"}, - {file = "shapely-2.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:798090b426142df2c5258779c1d8d5734ec6942f778dab6c6c30cfe7f3bf64ff"}, - {file = "shapely-2.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45211276900c4790d6bfc6105cbf1030742da67594ea4161a9ce6812a6721e68"}, - {file = "shapely-2.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e119444bc27ca33e786772b81760f2028d930ac55dafe9bc50ef538b794a8e1"}, - {file = "shapely-2.0.5-cp310-cp310-win32.whl", hash = "sha256:9a4492a2b2ccbeaebf181e7310d2dfff4fdd505aef59d6cb0f217607cb042fb3"}, - {file = "shapely-2.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5cb5ee72f1bc7ace737c9ecd30dc174a5295fae412972d3879bac2e82c8fae"}, - {file = "shapely-2.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5bbfb048a74cf273db9091ff3155d373020852805a37dfc846ab71dde4be93ec"}, - {file = "shapely-2.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93be600cbe2fbaa86c8eb70656369f2f7104cd231f0d6585c7d0aa555d6878b8"}, - {file = "shapely-2.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8e71bb9a46814019f6644c4e2560a09d44b80100e46e371578f35eaaa9da1c"}, - {file = "shapely-2.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5251c28a29012e92de01d2e84f11637eb1d48184ee8f22e2df6c8c578d26760"}, - {file = "shapely-2.0.5-cp311-cp311-win32.whl", hash = "sha256:35110e80070d664781ec7955c7de557456b25727a0257b354830abb759bf8311"}, - {file = "shapely-2.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c6b78c0007a34ce7144f98b7418800e0a6a5d9a762f2244b00ea560525290c9"}, - {file = "shapely-2.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:03bd7b5fa5deb44795cc0a503999d10ae9d8a22df54ae8d4a4cd2e8a93466195"}, - {file = "shapely-2.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ff9521991ed9e201c2e923da014e766c1aa04771bc93e6fe97c27dcf0d40ace"}, - {file = "shapely-2.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b65365cfbf657604e50d15161ffcc68de5cdb22a601bbf7823540ab4918a98d"}, - {file = "shapely-2.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21f64e647a025b61b19585d2247137b3a38a35314ea68c66aaf507a1c03ef6fe"}, - {file = "shapely-2.0.5-cp312-cp312-win32.whl", hash = "sha256:3ac7dc1350700c139c956b03d9c3df49a5b34aaf91d024d1510a09717ea39199"}, - {file = "shapely-2.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:30e8737983c9d954cd17feb49eb169f02f1da49e24e5171122cf2c2b62d65c95"}, - {file = "shapely-2.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ff7731fea5face9ec08a861ed351734a79475631b7540ceb0b66fb9732a5f529"}, - {file = "shapely-2.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff9e520af0c5a578e174bca3c18713cd47a6c6a15b6cf1f50ac17dc8bb8db6a2"}, - {file = "shapely-2.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b299b91557b04acb75e9732645428470825061f871a2edc36b9417d66c1fc5"}, - {file = "shapely-2.0.5-cp37-cp37m-win32.whl", hash = "sha256:b5870633f8e684bf6d1ae4df527ddcb6f3895f7b12bced5c13266ac04f47d231"}, - {file = "shapely-2.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:401cb794c5067598f50518e5a997e270cd7642c4992645479b915c503866abed"}, - {file = "shapely-2.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e91ee179af539100eb520281ba5394919067c6b51824e6ab132ad4b3b3e76dd0"}, - {file = "shapely-2.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8af6f7260f809c0862741ad08b1b89cb60c130ae30efab62320bbf4ee9cc71fa"}, - {file = "shapely-2.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5456dd522800306ba3faef77c5ba847ec30a0bd73ab087a25e0acdd4db2514f"}, - {file = "shapely-2.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b714a840402cde66fd7b663bb08cacb7211fa4412ea2a209688f671e0d0631fd"}, - {file = "shapely-2.0.5-cp38-cp38-win32.whl", hash = "sha256:7e8cf5c252fac1ea51b3162be2ec3faddedc82c256a1160fc0e8ddbec81b06d2"}, - {file = "shapely-2.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:4461509afdb15051e73ab178fae79974387f39c47ab635a7330d7fee02c68a3f"}, - {file = "shapely-2.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7545a39c55cad1562be302d74c74586f79e07b592df8ada56b79a209731c0219"}, - {file = "shapely-2.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c83a36f12ec8dee2066946d98d4d841ab6512a6ed7eb742e026a64854019b5f"}, - {file = "shapely-2.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89e640c2cd37378480caf2eeda9a51be64201f01f786d127e78eaeff091ec897"}, - {file = "shapely-2.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06efe39beafde3a18a21dde169d32f315c57da962826a6d7d22630025200c5e6"}, - {file = "shapely-2.0.5-cp39-cp39-win32.whl", hash = "sha256:8203a8b2d44dcb366becbc8c3d553670320e4acf0616c39e218c9561dd738d92"}, - {file = "shapely-2.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:7fed9dbfbcfec2682d9a047b9699db8dcc890dfca857ecba872c42185fc9e64e"}, - {file = "shapely-2.0.5.tar.gz", hash = "sha256:bff2366bc786bfa6cb353d6b47d0443c570c32776612e527ee47b6df63fcfe32"}, -] - -[package.dependencies] -numpy = ">=1.14,<3" - -[package.extras] -docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] -test = ["pytest", "pytest-cov"] - -[[package]] -name = "shellingham" -version = "1.5.4" -description = "Tool to Detect Surrounding Shell" -optional = false -python-versions = ">=3.7" -files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, -] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "snoop" -version = "0.4.3" -description = "Powerful debugging tools for Python" -optional = false -python-versions = "*" -files = [ - {file = "snoop-0.4.3-py2.py3-none-any.whl", hash = "sha256:b7418581889ff78b29d9dc5ad4625c4c475c74755fb5cba82c693c6e32afadc0"}, - {file = "snoop-0.4.3.tar.gz", hash = "sha256:2e0930bb19ff0dbdaa6f5933f88e89ed5984210ea9f9de0e1d8231fa5c1c1f25"}, -] - -[package.dependencies] -asttokens = "*" -cheap-repr = ">=0.4.0" -executing = "*" -pygments = "*" -six = "*" - -[package.extras] -tests = ["Django", "birdseye", "littleutils", "numpy (>=1.16.5)", "pandas (>=0.24.2)", "pprintpp", "prettyprinter", "pytest", "pytest-order", "pytest-order (<=0.11.0)"] - -[[package]] -name = "soupsieve" -version = "2.6" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -files = [ - {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, - {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, -] - -[[package]] -name = "stack-data" -version = "0.6.3" -description = "Extract data from python stack frames and tracebacks for informative displays" -optional = false -python-versions = "*" -files = [ - {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, - {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, -] - -[package.dependencies] -asttokens = ">=2.1.0" -executing = ">=1.2.0" -pure-eval = "*" - -[package.extras] -tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] - -[[package]] -name = "starlette" -version = "0.37.2" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.8" -files = [ - {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, - {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, -] - -[package.dependencies] -anyio = ">=3.4.0,<5" - -[package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] - -[[package]] -name = "std-uritemplate" -version = "1.0.5" -description = "std-uritemplate implementation for Python" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "std_uritemplate-1.0.5-py3-none-any.whl", hash = "sha256:8daf745b350ef3bc7b4ef82460a6c48aa459ca65fce8bda8657178959e3832d7"}, - {file = "std_uritemplate-1.0.5.tar.gz", hash = "sha256:6ea31e72f96ab2b54d93c774de2175ce5350a833fbf7c024bb3718a3a539f605"}, -] - -[[package]] -name = "sympy" -version = "1.13.2" -description = "Computer algebra system (CAS) in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sympy-1.13.2-py3-none-any.whl", hash = "sha256:c51d75517712f1aed280d4ce58506a4a88d635d6b5dd48b39102a7ae1f3fcfe9"}, - {file = "sympy-1.13.2.tar.gz", hash = "sha256:401449d84d07be9d0c7a46a64bd54fe097667d5e7181bfe67ec777be9e01cb13"}, -] - -[package.dependencies] -mpmath = ">=1.1.0,<1.4" - -[package.extras] -dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] - -[[package]] -name = "tenacity" -version = "9.0.0" -description = "Retry code until it succeeds" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, - {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, -] - -[package.extras] -doc = ["reno", "sphinx"] -test = ["pytest", "tornado (>=4.5)", "typeguard"] - -[[package]] -name = "threadpoolctl" -version = "3.5.0" -description = "threadpoolctl" -optional = false -python-versions = ">=3.8" -files = [ - {file = "threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467"}, - {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, -] - -[[package]] -name = "tinycss2" -version = "1.3.0" -description = "A tiny CSS parser" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"}, - {file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"}, -] - -[package.dependencies] -webencodings = ">=0.4" - -[package.extras] -doc = ["sphinx", "sphinx_rtd_theme"] -test = ["pytest", "ruff"] - -[[package]] -name = "tokenizers" -version = "0.19.1" -description = "" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tokenizers-0.19.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:952078130b3d101e05ecfc7fc3640282d74ed26bcf691400f872563fca15ac97"}, - {file = "tokenizers-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82c8b8063de6c0468f08e82c4e198763e7b97aabfe573fd4cf7b33930ca4df77"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f03727225feaf340ceeb7e00604825addef622d551cbd46b7b775ac834c1e1c4"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:453e4422efdfc9c6b6bf2eae00d5e323f263fff62b29a8c9cd526c5003f3f642"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02e81bf089ebf0e7f4df34fa0207519f07e66d8491d963618252f2e0729e0b46"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b07c538ba956843833fee1190cf769c60dc62e1cf934ed50d77d5502194d63b1"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28cab1582e0eec38b1f38c1c1fb2e56bce5dc180acb1724574fc5f47da2a4fe"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b01afb7193d47439f091cd8f070a1ced347ad0f9144952a30a41836902fe09e"}, - {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7fb297edec6c6841ab2e4e8f357209519188e4a59b557ea4fafcf4691d1b4c98"}, - {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e8a3dd055e515df7054378dc9d6fa8c8c34e1f32777fb9a01fea81496b3f9d3"}, - {file = "tokenizers-0.19.1-cp310-none-win32.whl", hash = "sha256:7ff898780a155ea053f5d934925f3902be2ed1f4d916461e1a93019cc7250837"}, - {file = "tokenizers-0.19.1-cp310-none-win_amd64.whl", hash = "sha256:bea6f9947e9419c2fda21ae6c32871e3d398cba549b93f4a65a2d369662d9403"}, - {file = "tokenizers-0.19.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5c88d1481f1882c2e53e6bb06491e474e420d9ac7bdff172610c4f9ad3898059"}, - {file = "tokenizers-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddf672ed719b4ed82b51499100f5417d7d9f6fb05a65e232249268f35de5ed14"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dadc509cc8a9fe460bd274c0e16ac4184d0958117cf026e0ea8b32b438171594"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfedf31824ca4915b511b03441784ff640378191918264268e6923da48104acc"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac11016d0a04aa6487b1513a3a36e7bee7eec0e5d30057c9c0408067345c48d2"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76951121890fea8330d3a0df9a954b3f2a37e3ec20e5b0530e9a0044ca2e11fe"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b342d2ce8fc8d00f376af068e3274e2e8649562e3bc6ae4a67784ded6b99428d"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d16ff18907f4909dca9b076b9c2d899114dd6abceeb074eca0c93e2353f943aa"}, - {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:706a37cc5332f85f26efbe2bdc9ef8a9b372b77e4645331a405073e4b3a8c1c6"}, - {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16baac68651701364b0289979ecec728546133e8e8fe38f66fe48ad07996b88b"}, - {file = "tokenizers-0.19.1-cp311-none-win32.whl", hash = "sha256:9ed240c56b4403e22b9584ee37d87b8bfa14865134e3e1c3fb4b2c42fafd3256"}, - {file = "tokenizers-0.19.1-cp311-none-win_amd64.whl", hash = "sha256:ad57d59341710b94a7d9dbea13f5c1e7d76fd8d9bcd944a7a6ab0b0da6e0cc66"}, - {file = "tokenizers-0.19.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:621d670e1b1c281a1c9698ed89451395d318802ff88d1fc1accff0867a06f153"}, - {file = "tokenizers-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d924204a3dbe50b75630bd16f821ebda6a5f729928df30f582fb5aade90c818a"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f3fefdc0446b1a1e6d81cd4c07088ac015665d2e812f6dbba4a06267d1a2c95"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9620b78e0b2d52ef07b0d428323fb34e8ea1219c5eac98c2596311f20f1f9266"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04ce49e82d100594715ac1b2ce87d1a36e61891a91de774755f743babcd0dd52"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5c2ff13d157afe413bf7e25789879dd463e5a4abfb529a2d8f8473d8042e28f"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3174c76efd9d08f836bfccaca7cfec3f4d1c0a4cf3acbc7236ad577cc423c840"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9d5b6c0e7a1e979bec10ff960fae925e947aab95619a6fdb4c1d8ff3708ce3"}, - {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a179856d1caee06577220ebcfa332af046d576fb73454b8f4d4b0ba8324423ea"}, - {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:952b80dac1a6492170f8c2429bd11fcaa14377e097d12a1dbe0ef2fb2241e16c"}, - {file = "tokenizers-0.19.1-cp312-none-win32.whl", hash = "sha256:01d62812454c188306755c94755465505836fd616f75067abcae529c35edeb57"}, - {file = "tokenizers-0.19.1-cp312-none-win_amd64.whl", hash = "sha256:b70bfbe3a82d3e3fb2a5e9b22a39f8d1740c96c68b6ace0086b39074f08ab89a"}, - {file = "tokenizers-0.19.1-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:bb9dfe7dae85bc6119d705a76dc068c062b8b575abe3595e3c6276480e67e3f1"}, - {file = "tokenizers-0.19.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:1f0360cbea28ea99944ac089c00de7b2e3e1c58f479fb8613b6d8d511ce98267"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:71e3ec71f0e78780851fef28c2a9babe20270404c921b756d7c532d280349214"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b82931fa619dbad979c0ee8e54dd5278acc418209cc897e42fac041f5366d626"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8ff5b90eabdcdaa19af697885f70fe0b714ce16709cf43d4952f1f85299e73a"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e742d76ad84acbdb1a8e4694f915fe59ff6edc381c97d6dfdd054954e3478ad4"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8c5d59d7b59885eab559d5bc082b2985555a54cda04dda4c65528d90ad252ad"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b2da5c32ed869bebd990c9420df49813709e953674c0722ff471a116d97b22d"}, - {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:638e43936cc8b2cbb9f9d8dde0fe5e7e30766a3318d2342999ae27f68fdc9bd6"}, - {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:78e769eb3b2c79687d9cb0f89ef77223e8e279b75c0a968e637ca7043a84463f"}, - {file = "tokenizers-0.19.1-cp37-none-win32.whl", hash = "sha256:72791f9bb1ca78e3ae525d4782e85272c63faaef9940d92142aa3eb79f3407a3"}, - {file = "tokenizers-0.19.1-cp37-none-win_amd64.whl", hash = "sha256:f3bbb7a0c5fcb692950b041ae11067ac54826204318922da754f908d95619fbc"}, - {file = "tokenizers-0.19.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:07f9295349bbbcedae8cefdbcfa7f686aa420be8aca5d4f7d1ae6016c128c0c5"}, - {file = "tokenizers-0.19.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10a707cc6c4b6b183ec5dbfc5c34f3064e18cf62b4a938cb41699e33a99e03c1"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6309271f57b397aa0aff0cbbe632ca9d70430839ca3178bf0f06f825924eca22"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ad23d37d68cf00d54af184586d79b84075ada495e7c5c0f601f051b162112dc"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:427c4f0f3df9109314d4f75b8d1f65d9477033e67ffaec4bca53293d3aca286d"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e83a31c9cf181a0a3ef0abad2b5f6b43399faf5da7e696196ddd110d332519ee"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c27b99889bd58b7e301468c0838c5ed75e60c66df0d4db80c08f43462f82e0d3"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bac0b0eb952412b0b196ca7a40e7dce4ed6f6926489313414010f2e6b9ec2adf"}, - {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8a6298bde623725ca31c9035a04bf2ef63208d266acd2bed8c2cb7d2b7d53ce6"}, - {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:08a44864e42fa6d7d76d7be4bec62c9982f6f6248b4aa42f7302aa01e0abfd26"}, - {file = "tokenizers-0.19.1-cp38-none-win32.whl", hash = "sha256:1de5bc8652252d9357a666e609cb1453d4f8e160eb1fb2830ee369dd658e8975"}, - {file = "tokenizers-0.19.1-cp38-none-win_amd64.whl", hash = "sha256:0bcce02bf1ad9882345b34d5bd25ed4949a480cf0e656bbd468f4d8986f7a3f1"}, - {file = "tokenizers-0.19.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0b9394bd204842a2a1fd37fe29935353742be4a3460b6ccbaefa93f58a8df43d"}, - {file = "tokenizers-0.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4692ab92f91b87769d950ca14dbb61f8a9ef36a62f94bad6c82cc84a51f76f6a"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6258c2ef6f06259f70a682491c78561d492e885adeaf9f64f5389f78aa49a051"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85cf76561fbd01e0d9ea2d1cbe711a65400092bc52b5242b16cfd22e51f0c58"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:670b802d4d82bbbb832ddb0d41df7015b3e549714c0e77f9bed3e74d42400fbe"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85aa3ab4b03d5e99fdd31660872249df5e855334b6c333e0bc13032ff4469c4a"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbf001afbbed111a79ca47d75941e9e5361297a87d186cbfc11ed45e30b5daba"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c89aa46c269e4e70c4d4f9d6bc644fcc39bb409cb2a81227923404dd6f5227"}, - {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:39c1ec76ea1027438fafe16ecb0fb84795e62e9d643444c1090179e63808c69d"}, - {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c2a0d47a89b48d7daa241e004e71fb5a50533718897a4cd6235cb846d511a478"}, - {file = "tokenizers-0.19.1-cp39-none-win32.whl", hash = "sha256:61b7fe8886f2e104d4caf9218b157b106207e0f2a4905c9c7ac98890688aabeb"}, - {file = "tokenizers-0.19.1-cp39-none-win_amd64.whl", hash = "sha256:f97660f6c43efd3e0bfd3f2e3e5615bf215680bad6ee3d469df6454b8c6e8256"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b11853f17b54c2fe47742c56d8a33bf49ce31caf531e87ac0d7d13d327c9334"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d26194ef6c13302f446d39972aaa36a1dda6450bc8949f5eb4c27f51191375bd"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e8d1ed93beda54bbd6131a2cb363a576eac746d5c26ba5b7556bc6f964425594"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca407133536f19bdec44b3da117ef0d12e43f6d4b56ac4c765f37eca501c7bda"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce05fde79d2bc2e46ac08aacbc142bead21614d937aac950be88dc79f9db9022"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:35583cd46d16f07c054efd18b5d46af4a2f070a2dd0a47914e66f3ff5efb2b1e"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:43350270bfc16b06ad3f6f07eab21f089adb835544417afda0f83256a8bf8b75"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b4399b59d1af5645bcee2072a463318114c39b8547437a7c2d6a186a1b5a0e2d"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6852c5b2a853b8b0ddc5993cd4f33bfffdca4fcc5d52f89dd4b8eada99379285"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd266ae85c3d39df2f7e7d0e07f6c41a55e9a3123bb11f854412952deacd828"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecb2651956eea2aa0a2d099434134b1b68f1c31f9a5084d6d53f08ed43d45ff2"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b279ab506ec4445166ac476fb4d3cc383accde1ea152998509a94d82547c8e2a"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:89183e55fb86e61d848ff83753f64cded119f5d6e1f553d14ffee3700d0a4a49"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2edbc75744235eea94d595a8b70fe279dd42f3296f76d5a86dde1d46e35f574"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:0e64bfde9a723274e9a71630c3e9494ed7b4c0f76a1faacf7fe294cd26f7ae7c"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0b5ca92bfa717759c052e345770792d02d1f43b06f9e790ca0a1db62838816f3"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f8a20266e695ec9d7a946a019c1d5ca4eddb6613d4f466888eee04f16eedb85"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63c38f45d8f2a2ec0f3a20073cccb335b9f99f73b3c69483cd52ebc75369d8a1"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dd26e3afe8a7b61422df3176e06664503d3f5973b94f45d5c45987e1cb711876"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:eddd5783a4a6309ce23432353cdb36220e25cbb779bfa9122320666508b44b88"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:56ae39d4036b753994476a1b935584071093b55c7a72e3b8288e68c313ca26e7"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f9939ca7e58c2758c01b40324a59c034ce0cebad18e0d4563a9b1beab3018243"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c330c0eb815d212893c67a032e9dc1b38a803eccb32f3e8172c19cc69fbb439"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec11802450a2487cdf0e634b750a04cbdc1c4d066b97d94ce7dd2cb51ebb325b"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b718f316b596f36e1dae097a7d5b91fc5b85e90bf08b01ff139bd8953b25af"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ed69af290c2b65169f0ba9034d1dc39a5db9459b32f1dd8b5f3f32a3fcf06eab"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f8a9c828277133af13f3859d1b6bf1c3cb6e9e1637df0e45312e6b7c2e622b1f"}, - {file = "tokenizers-0.19.1.tar.gz", hash = "sha256:ee59e6680ed0fdbe6b724cf38bd70400a0c1dd623b07ac729087270caeac88e3"}, -] - -[package.dependencies] -huggingface-hub = ">=0.16.4,<1.0" - -[package.extras] -dev = ["tokenizers[testing]"] -docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] -testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests", "ruff"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "torch" -version = "2.2.2" -description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "torch-2.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:bc889d311a855dd2dfd164daf8cc903a6b7273a747189cebafdd89106e4ad585"}, - {file = "torch-2.2.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:15dffa4cc3261fa73d02f0ed25f5fa49ecc9e12bf1ae0a4c1e7a88bbfaad9030"}, - {file = "torch-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:11e8fe261233aeabd67696d6b993eeb0896faa175c6b41b9a6c9f0334bdad1c5"}, - {file = "torch-2.2.2-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:b2e2200b245bd9f263a0d41b6a2dab69c4aca635a01b30cca78064b0ef5b109e"}, - {file = "torch-2.2.2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:877b3e6593b5e00b35bbe111b7057464e76a7dd186a287280d941b564b0563c2"}, - {file = "torch-2.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:ad4c03b786e074f46606f4151c0a1e3740268bcf29fbd2fdf6666d66341c1dcb"}, - {file = "torch-2.2.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:32827fa1fbe5da8851686256b4cd94cc7b11be962862c2293811c94eea9457bf"}, - {file = "torch-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:f9ef0a648310435511e76905f9b89612e45ef2c8b023bee294f5e6f7e73a3e7c"}, - {file = "torch-2.2.2-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:95b9b44f3bcebd8b6cd8d37ec802048c872d9c567ba52c894bba90863a439059"}, - {file = "torch-2.2.2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:49aa4126ede714c5aeef7ae92969b4b0bbe67f19665106463c39f22e0a1860d1"}, - {file = "torch-2.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:cf12cdb66c9c940227ad647bc9cf5dba7e8640772ae10dfe7569a0c1e2a28aca"}, - {file = "torch-2.2.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:89ddac2a8c1fb6569b90890955de0c34e1724f87431cacff4c1979b5f769203c"}, - {file = "torch-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:451331406b760f4b1ab298ddd536486ab3cfb1312614cfe0532133535be60bea"}, - {file = "torch-2.2.2-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:eb4d6e9d3663e26cd27dc3ad266b34445a16b54908e74725adb241aa56987533"}, - {file = "torch-2.2.2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:bf9558da7d2bf7463390b3b2a61a6a3dbb0b45b161ee1dd5ec640bf579d479fc"}, - {file = "torch-2.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd2bf7697c9e95fb5d97cc1d525486d8cf11a084c6af1345c2c2c22a6b0029d0"}, - {file = "torch-2.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b421448d194496e1114d87a8b8d6506bce949544e513742b097e2ab8f7efef32"}, - {file = "torch-2.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:3dbcd563a9b792161640c0cffe17e3270d85e8f4243b1f1ed19cca43d28d235b"}, - {file = "torch-2.2.2-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:31f4310210e7dda49f1fb52b0ec9e59382cfcb938693f6d5378f25b43d7c1d29"}, - {file = "torch-2.2.2-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:c795feb7e8ce2e0ef63f75f8e1ab52e7fd5e1a4d7d0c31367ade1e3de35c9e95"}, - {file = "torch-2.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a6e5770d68158d07456bfcb5318b173886f579fdfbf747543901ce718ea94782"}, - {file = "torch-2.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:67dcd726edff108e2cd6c51ff0e416fd260c869904de95750e80051358680d24"}, - {file = "torch-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:539d5ef6c4ce15bd3bd47a7b4a6e7c10d49d4d21c0baaa87c7d2ef8698632dfb"}, - {file = "torch-2.2.2-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:dff696de90d6f6d1e8200e9892861fd4677306d0ef604cb18f2134186f719f82"}, - {file = "torch-2.2.2-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:3a4dd910663fd7a124c056c878a52c2b0be4a5a424188058fe97109d4436ee42"}, -] - -[package.dependencies] -filelock = "*" -fsspec = "*" -jinja2 = "*" -networkx = "*" -nvidia-cublas-cu12 = {version = "12.1.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-cupti-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-nvrtc-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cuda-runtime-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cudnn-cu12 = {version = "8.9.2.26", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cufft-cu12 = {version = "11.0.2.54", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-curand-cu12 = {version = "10.3.2.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusolver-cu12 = {version = "11.4.5.107", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-cusparse-cu12 = {version = "12.1.0.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nccl-cu12 = {version = "2.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -nvidia-nvtx-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} -sympy = "*" -triton = {version = "2.2.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\" and python_version < \"3.12\""} -typing-extensions = ">=4.8.0" - -[package.extras] -opt-einsum = ["opt-einsum (>=3.3)"] -optree = ["optree (>=0.9.1)"] - -[[package]] -name = "tornado" -version = "6.4.1" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -optional = false -python-versions = ">=3.8" -files = [ - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, - {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, - {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, - {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, -] - -[[package]] -name = "tqdm" -version = "4.66.5" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, - {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "traitlets" -version = "5.14.3" -description = "Traitlets Python configuration system" -optional = false -python-versions = ">=3.8" -files = [ - {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, - {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, -] - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] - -[[package]] -name = "transformers" -version = "4.44.0" -description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "transformers-4.44.0-py3-none-any.whl", hash = "sha256:ea0ff72def71e9f4812d9414d4803b22681b1617aa6f511bd51cfff2b44a6fca"}, - {file = "transformers-4.44.0.tar.gz", hash = "sha256:75699495e30b7635ca444d8d372e138c687ab51a875b387e33f1fb759c37f196"}, -] - -[package.dependencies] -accelerate = {version = ">=0.21.0", optional = true, markers = "extra == \"torch\""} -filelock = "*" -huggingface-hub = ">=0.23.2,<1.0" -numpy = ">=1.17" -packaging = ">=20.0" -pyyaml = ">=5.1" -regex = "!=2019.12.17" -requests = "*" -safetensors = ">=0.4.1" -tokenizers = ">=0.19,<0.20" -torch = {version = "*", optional = true, markers = "extra == \"torch\""} -tqdm = ">=4.27" - -[package.extras] -accelerate = ["accelerate (>=0.21.0)"] -agents = ["Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "datasets (!=2.5.0)", "diffusers", "opencv-python", "sentencepiece (>=0.1.91,!=0.1.92)", "torch"] -all = ["Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "av (==9.2.0)", "codecarbon (==1.2.0)", "decord (==0.6.0)", "flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "librosa", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "phonemizer", "protobuf", "pyctcdecode (>=0.4.0)", "ray[tune] (>=2.7.0)", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timm (<=0.9.16)", "tokenizers (>=0.19,<0.20)", "torch", "torchaudio", "torchvision"] -audio = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] -benchmark = ["optimum-benchmark (>=0.2.0)"] -codecarbon = ["codecarbon (==1.2.0)"] -deepspeed = ["accelerate (>=0.21.0)", "deepspeed (>=0.9.3)"] -deepspeed-testing = ["GitPython (<3.1.19)", "accelerate (>=0.21.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "deepspeed (>=0.9.3)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "nltk", "optuna", "parameterized", "protobuf", "psutil", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.5.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "timeout-decorator"] -dev = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "av (==9.2.0)", "beautifulsoup4", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "decord (==0.6.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flax (>=0.4.1,<=0.7.0)", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "librosa", "nltk", "onnxconverter-common", "optax (>=0.0.8,<=0.1.4)", "optuna", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.5.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "scipy (<1.13.0)", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "timm (<=0.9.16)", "tokenizers (>=0.19,<0.20)", "torch", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"] -dev-tensorflow = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "isort (>=5.5.4)", "kenlm", "keras-nlp (>=0.3.1,<0.14.0)", "librosa", "nltk", "onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.5.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx", "timeout-decorator", "tokenizers (>=0.19,<0.20)", "urllib3 (<2.0.0)"] -dev-torch = ["GitPython (<3.1.19)", "Pillow (>=10.0.1,<=15.0)", "accelerate (>=0.21.0)", "beautifulsoup4", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "kenlm", "librosa", "nltk", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "optuna", "parameterized", "phonemizer", "protobuf", "psutil", "pyctcdecode (>=0.4.0)", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "ray[tune] (>=2.7.0)", "rhoknp (>=1.1.0,<1.3.1)", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.5.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorboard", "timeout-decorator", "timm (<=0.9.16)", "tokenizers (>=0.19,<0.20)", "torch", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)", "urllib3 (<2.0.0)"] -flax = ["flax (>=0.4.1,<=0.7.0)", "jax (>=0.4.1,<=0.4.13)", "jaxlib (>=0.4.1,<=0.4.13)", "optax (>=0.0.8,<=0.1.4)", "scipy (<1.13.0)"] -flax-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] -ftfy = ["ftfy"] -integrations = ["optuna", "ray[tune] (>=2.7.0)", "sigopt"] -ja = ["fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "rhoknp (>=1.1.0,<1.3.1)", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"] -modelcreation = ["cookiecutter (==1.7.3)"] -natten = ["natten (>=0.14.6,<0.15.0)"] -onnx = ["onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "tf2onnx"] -onnxruntime = ["onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)"] -optuna = ["optuna"] -quality = ["GitPython (<3.1.19)", "datasets (!=2.5.0)", "isort (>=5.5.4)", "ruff (==0.5.1)", "urllib3 (<2.0.0)"] -ray = ["ray[tune] (>=2.7.0)"] -retrieval = ["datasets (!=2.5.0)", "faiss-cpu"] -ruff = ["ruff (==0.5.1)"] -sagemaker = ["sagemaker (>=2.31.0)"] -sentencepiece = ["protobuf", "sentencepiece (>=0.1.91,!=0.1.92)"] -serving = ["fastapi", "pydantic", "starlette", "uvicorn"] -sigopt = ["sigopt"] -sklearn = ["scikit-learn"] -speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] -testing = ["GitPython (<3.1.19)", "beautifulsoup4", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "nltk", "parameterized", "psutil", "pydantic", "pytest (>=7.2.0,<8.0.0)", "pytest-rich", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (==0.5.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorboard", "timeout-decorator"] -tf = ["keras-nlp (>=0.3.1,<0.14.0)", "onnxconverter-common", "tensorflow (>2.9,<2.16)", "tensorflow-text (<2.16)", "tf2onnx"] -tf-cpu = ["keras (>2.9,<2.16)", "keras-nlp (>=0.3.1,<0.14.0)", "onnxconverter-common", "tensorflow-cpu (>2.9,<2.16)", "tensorflow-probability (<0.24)", "tensorflow-text (<2.16)", "tf2onnx"] -tf-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"] -timm = ["timm (<=0.9.16)"] -tokenizers = ["tokenizers (>=0.19,<0.20)"] -torch = ["accelerate (>=0.21.0)", "torch"] -torch-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"] -torch-vision = ["Pillow (>=10.0.1,<=15.0)", "torchvision"] -torchhub = ["filelock", "huggingface-hub (>=0.23.2,<1.0)", "importlib-metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.19,<0.20)", "torch", "tqdm (>=4.27)"] -video = ["av (==9.2.0)", "decord (==0.6.0)"] -vision = ["Pillow (>=10.0.1,<=15.0)"] - -[[package]] -name = "triton" -version = "2.2.0" -description = "A language and compiler for custom Deep Learning operations" -optional = false -python-versions = "*" -files = [ - {file = "triton-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2294514340cfe4e8f4f9e5c66c702744c4a117d25e618bd08469d0bfed1e2e5"}, - {file = "triton-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da58a152bddb62cafa9a857dd2bc1f886dbf9f9c90a2b5da82157cd2b34392b0"}, - {file = "triton-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af58716e721460a61886668b205963dc4d1e4ac20508cc3f623aef0d70283d5"}, - {file = "triton-2.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8fe46d3ab94a8103e291bd44c741cc294b91d1d81c1a2888254cbf7ff846dab"}, - {file = "triton-2.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8ce26093e539d727e7cf6f6f0d932b1ab0574dc02567e684377630d86723ace"}, - {file = "triton-2.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:227cc6f357c5efcb357f3867ac2a8e7ecea2298cd4606a8ba1e931d1d5a947df"}, -] - -[package.dependencies] -filelock = "*" - -[package.extras] -build = ["cmake (>=3.20)", "lit"] -tests = ["autopep8", "flake8", "isort", "numpy", "pytest", "scipy (>=1.7.1)", "torch"] -tutorials = ["matplotlib", "pandas", "tabulate", "torch"] - -[[package]] -name = "typer" -version = "0.12.3" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.7" -files = [ - {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, - {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, -] - -[package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" -shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" - -[[package]] -name = "types-cffi" -version = "1.16.0.20240331" -description = "Typing stubs for cffi" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-cffi-1.16.0.20240331.tar.gz", hash = "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee"}, - {file = "types_cffi-1.16.0.20240331-py3-none-any.whl", hash = "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0"}, -] - -[package.dependencies] -types-setuptools = "*" - -[[package]] -name = "types-pyopenssl" -version = "24.1.0.20240722" -description = "Typing stubs for pyOpenSSL" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39"}, - {file = "types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54"}, -] - -[package.dependencies] -cryptography = ">=35.0.0" -types-cffi = "*" - -[[package]] -name = "types-pyyaml" -version = "6.0.12.20240808" -description = "Typing stubs for PyYAML" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-PyYAML-6.0.12.20240808.tar.gz", hash = "sha256:b8f76ddbd7f65440a8bda5526a9607e4c7a322dc2f8e1a8c405644f9a6f4b9af"}, - {file = "types_PyYAML-6.0.12.20240808-py3-none-any.whl", hash = "sha256:deda34c5c655265fc517b546c902aa6eed2ef8d3e921e4765fe606fe2afe8d35"}, -] - -[[package]] -name = "types-redis" -version = "4.6.0.20240806" -description = "Typing stubs for redis" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-redis-4.6.0.20240806.tar.gz", hash = "sha256:60dd02c2b91ea2d42ad079ac58dedc31d71d6eedb1c21d3796811b02baac655d"}, - {file = "types_redis-4.6.0.20240806-py3-none-any.whl", hash = "sha256:9d8fbe0ce37e3660c0a06982db7812384295d10a93d637c7f8604a2f3c88b0e6"}, -] - -[package.dependencies] -cryptography = ">=35.0.0" -types-pyOpenSSL = "*" - -[[package]] -name = "types-setuptools" -version = "71.1.0.20240813" -description = "Typing stubs for setuptools" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-setuptools-71.1.0.20240813.tar.gz", hash = "sha256:94ff4f0af18c7c24ac88932bcb0f5655fb7187a001b7c61e53a1bfdaf9877b54"}, - {file = "types_setuptools-71.1.0.20240813-py3-none-any.whl", hash = "sha256:d9d9ba2936f5d3b47b59ae9bf65942a60063ac1d6bbee180a8a79fbb43f22ce5"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "tzdata" -version = "2024.1" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, -] - -[[package]] -name = "ujson" -version = "5.10.0" -description = "Ultra fast JSON encoder and decoder for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"}, - {file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"}, - {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6"}, - {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569"}, - {file = "ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770"}, - {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1"}, - {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5"}, - {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51"}, - {file = "ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518"}, - {file = "ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f"}, - {file = "ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00"}, - {file = "ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126"}, - {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8"}, - {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b"}, - {file = "ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9"}, - {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f"}, - {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4"}, - {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1"}, - {file = "ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f"}, - {file = "ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720"}, - {file = "ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5"}, - {file = "ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e"}, - {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043"}, - {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1"}, - {file = "ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3"}, - {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21"}, - {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2"}, - {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e"}, - {file = "ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e"}, - {file = "ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc"}, - {file = "ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287"}, - {file = "ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e"}, - {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557"}, - {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988"}, - {file = "ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816"}, - {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20"}, - {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0"}, - {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f"}, - {file = "ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165"}, - {file = "ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539"}, - {file = "ujson-5.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050"}, - {file = "ujson-5.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd"}, - {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb"}, - {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a"}, - {file = "ujson-5.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d"}, - {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe"}, - {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7"}, - {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4"}, - {file = "ujson-5.10.0-cp38-cp38-win32.whl", hash = "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8"}, - {file = "ujson-5.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc"}, - {file = "ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b"}, - {file = "ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27"}, - {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76"}, - {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5"}, - {file = "ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0"}, - {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1"}, - {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1"}, - {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996"}, - {file = "ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9"}, - {file = "ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88"}, - {file = "ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6"}, - {file = "ujson-5.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e"}, - {file = "ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7"}, - {file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"}, -] - -[[package]] -name = "uritemplate" -version = "4.1.1" -description = "Implementation of RFC 6570 URI Templates" -optional = false -python-versions = ">=3.6" -files = [ - {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, - {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, -] - -[[package]] -name = "urllib3" -version = "2.2.2" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "usearch" -version = "2.13.2" -description = "Smaller & Faster Single-File Vector Search Engine from Unum" -optional = false -python-versions = "*" -files = [ - {file = "usearch-2.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9d1e39e46bc132df19930b8432a32722946f339ebbdbdd0075fbc0819ba00103"}, - {file = "usearch-2.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c6cb9ab2448c531c17847135e06cf00abdb6a45bfc06e13330144e0baf0b3fdb"}, - {file = "usearch-2.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1f649031009b4828ae87aba650ee620a617a98bfcacd501f76f0b92ad93aef77"}, - {file = "usearch-2.13.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c13219c73e506663fcb577722c57a91bcdbafc7e8d20f9d3233efee643dba72"}, - {file = "usearch-2.13.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2ce68c330273d7a1eb3e1ef39dc318f60bd74eca055877ece865c7c45c2440eb"}, - {file = "usearch-2.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3cc1ef99a7023d13d9c6e2d0cf182fe9f13b5fcafba559247c4cecfc12fa47ee"}, - {file = "usearch-2.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f57ff2e6d4c517b86908b9f77ebfb71e18db25110589f2b7c28b5f713d582ba2"}, - {file = "usearch-2.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a11768735610d221f775ad34a9a904a637d94e71c9b0746243da7383197ca03e"}, - {file = "usearch-2.13.2-cp310-cp310-win_arm64.whl", hash = "sha256:906ad9304b0dc678fa79afd5282869c48bb88039914c4d4c14cf98b3fd8596da"}, - {file = "usearch-2.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3ba61347e7eda059c2f02dec4ad4ff89b317e10c9d25a73b06f92b8b2f40a855"}, - {file = "usearch-2.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:11796b2d10bce16d373f9d2badc2ed361bd44b5b96e02fbd30c48adbb084c63d"}, - {file = "usearch-2.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5de2eb6d6468a369c051f7523d5431fa64d3b2331c6191b6430d7344de575eb"}, - {file = "usearch-2.13.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30ca771280bb47de63cb3d77d727b5c5537f60477b1da9857e40d9835db7a664"}, - {file = "usearch-2.13.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2c75f980536893d6e7879d2be34ad426b0823c3197b4a5e8d07cd6944787784"}, - {file = "usearch-2.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:682b5b7b2935269269d862ac651356bc80b5005e3943d7cbaecb949828a82359"}, - {file = "usearch-2.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4905b65f00b02f609f3fff6b954b1e912b0349498e907f926290094838d5e996"}, - {file = "usearch-2.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:f3dfcabd448547f1cd1d315a4f7493c360e0972a4bce0d0217a95a58e60d6369"}, - {file = "usearch-2.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:7881dc53571fbb8b81ee4c41ca4d666d76441fe69f3e99641fa8da99b98ecbf1"}, - {file = "usearch-2.13.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:cadf54a120e76472ae8a355ba5189d524ef0a0a0cadf07c399669283128a47c8"}, - {file = "usearch-2.13.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14effe0f433847f41b7a2304165a23b6c6a0955e46a26731fc89cb4488d3debf"}, - {file = "usearch-2.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa17338313cf50e04cf11785e5892976513152a4b5f37b019602f772e35c4cc3"}, - {file = "usearch-2.13.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed6f52e841fb49e244bcbcdad982febaacd782eff1e8cf31377de02baa4e504"}, - {file = "usearch-2.13.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9d66e3274dbb71f978df4acd741da288bbdb129b9af6f5ac6223182f7f7f9fb8"}, - {file = "usearch-2.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:14b0fb3ac8829e805e4971d846d248e80f7b5274c59d845678bcaa6fbe84426d"}, - {file = "usearch-2.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9ce2f471bf3e947841f446f8e44963edffa90db66f5d315d0e0e738f0369264f"}, - {file = "usearch-2.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:9a69c6ae3d35e9fa03366782984ff97df3a8ee4d6995d51dee5bdf59fb13c5be"}, - {file = "usearch-2.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:9bfecb48814b77c439f8c0d72eb6e645d3a00a16f9385643f78732e4c207b68a"}, - {file = "usearch-2.13.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f6055b056557a02b63506c2c6bf30b97f7645f212accba1f4fdce8826ccfa823"}, - {file = "usearch-2.13.2-cp37-cp37m-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5115c25e1ba4a5beff0fa4780ea7db3b60a827efe3f72453b7fee6b299878d19"}, - {file = "usearch-2.13.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:04aca42da4dcccd20c6524a3ece6e4e3e458ea5a15fd51f2d39bc9b353d475c0"}, - {file = "usearch-2.13.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ba46879670aa27fff4d5446296a95d1ff62e52d9165d8ac6ac3fdd949998d0c9"}, - {file = "usearch-2.13.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d25bcea3f81d1bf2e836dc35f3c83d7d39b7123b4b39f77827af547fec5b8d15"}, - {file = "usearch-2.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7dcd9c9c1509dc6435d332569f05312eba6dab820b5ed28674e0b0444de23057"}, - {file = "usearch-2.13.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:16014dd2e8b798eb8012223c51847b59d9ad8b7a9424b6ae32101f3f31d6e711"}, - {file = "usearch-2.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:158aeb09fecc25d725465c0c6dee0793fe34eae668e23545eb927706e9ac1e35"}, - {file = "usearch-2.13.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd0c6f27c07505929f09a90637c59f3719a0b2201faed61ee3cbeca65af56165"}, - {file = "usearch-2.13.2-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64cf63b0e0a707d0064fd0d0eb73899d36a6ed6f694603d24e3fb6921903b09c"}, - {file = "usearch-2.13.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:2bbae49cabea6982cb1a8f68aab0a3772c8f9ce0e9e6a9842969b39d391c919b"}, - {file = "usearch-2.13.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:cb586334a6b801fe2a6ca7dae5af7a1b7c26aa01efffef708eff35cda45ce5a3"}, - {file = "usearch-2.13.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:94d144f5a5616a1b5f333219ee3f05420aa2fd44d6463e58affaf0e62bd1143d"}, - {file = "usearch-2.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:30dac0f71a6f05c80075f62e32b1a535b41a5073499ecbe577ca0298c1be8a8c"}, - {file = "usearch-2.13.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b7eeeda7d2f9f3b5e0fbd0c6befc783461c43777a97ae46a358acd44500ce8a4"}, - {file = "usearch-2.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4b01ce27094c30e370766b145190842f2715362113da712322bc9eed7a1099d2"}, - {file = "usearch-2.13.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f0a4afa048fec3893651841c6430e6b98f85c1a9690687823fdf6c31712bd09f"}, - {file = "usearch-2.13.2-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1cce3580f946d97b9b58278b6960632abcd4b62c2be566f0ea11dd78cc0252"}, - {file = "usearch-2.13.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:8c48a1e24f37c97e698471ecd25393ef5291a71f0e90887a1fe0001dfbe19aa5"}, - {file = "usearch-2.13.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bfbd43571f42af16cd30796d7132edfe5514088bafc96f5178caf4990e1efd14"}, - {file = "usearch-2.13.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:486134f647b3ddc5baae49f57ef014618bb7c9f0d2b8c6adc178ab793ad2191f"}, - {file = "usearch-2.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:a92a2fa400024a5bf0a09d0d49f86db6db787eb9d7de7b1f2f0249e796e9408c"}, - {file = "usearch-2.13.2-cp39-cp39-win_arm64.whl", hash = "sha256:bc39d38d8552325dd87ce2946ec94ab7f65e5895e8e681d5996d79197d8adfeb"}, -] - -[package.dependencies] -numpy = "*" -tqdm = "*" - -[[package]] -name = "uvicorn" -version = "0.30.6" -description = "The lightning-fast ASGI server." -optional = false -python-versions = ">=3.8" -files = [ - {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, - {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, -] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} -h11 = ">=0.8" -httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} -python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} -uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} -watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} - -[package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "uvloop" -version = "0.19.0" -description = "Fast implementation of asyncio event loop on top of libuv" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, - {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, - {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, - {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, - {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, - {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, - {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, - {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, - {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, - {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, - {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, - {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, - {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, - {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, - {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, - {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, - {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, - {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, - {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, - {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, - {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, - {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, - {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, - {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, - {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, - {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, - {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, - {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, - {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, - {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, - {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, -] - -[package.extras] -docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] - -[[package]] -name = "validators" -version = "0.33.0" -description = "Python Data Validation for Humans™" -optional = false -python-versions = ">=3.8" -files = [ - {file = "validators-0.33.0-py3-none-any.whl", hash = "sha256:134b586a98894f8139865953899fc2daeb3d0c35569552c5518f089ae43ed075"}, - {file = "validators-0.33.0.tar.gz", hash = "sha256:535867e9617f0100e676a1257ba1e206b9bfd847ddc171e4d44811f07ff0bfbf"}, -] - -[package.extras] -crypto-eth-addresses = ["eth-hash[pycryptodome] (>=0.7.0)"] - -[[package]] -name = "virtualenv" -version = "20.26.3" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, - {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "watchfiles" -version = "0.23.0" -description = "Simple, modern and high performance file watching and code reload in python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "watchfiles-0.23.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bee8ce357a05c20db04f46c22be2d1a2c6a8ed365b325d08af94358e0688eeb4"}, - {file = "watchfiles-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ccd3011cc7ee2f789af9ebe04745436371d36afe610028921cab9f24bb2987b"}, - {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb02d41c33be667e6135e6686f1bb76104c88a312a18faa0ef0262b5bf7f1a0f"}, - {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf12ac34c444362f3261fb3ff548f0037ddd4c5bb85f66c4be30d2936beb3c5"}, - {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0b2c25040a3c0ce0e66c7779cc045fdfbbb8d59e5aabfe033000b42fe44b53e"}, - {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf2be4b9eece4f3da8ba5f244b9e51932ebc441c0867bd6af46a3d97eb068d6"}, - {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40cb8fa00028908211eb9f8d47744dca21a4be6766672e1ff3280bee320436f1"}, - {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f48c917ffd36ff9a5212614c2d0d585fa8b064ca7e66206fb5c095015bc8207"}, - {file = "watchfiles-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9d183e3888ada88185ab17064079c0db8c17e32023f5c278d7bf8014713b1b5b"}, - {file = "watchfiles-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9837edf328b2805346f91209b7e660f65fb0e9ca18b7459d075d58db082bf981"}, - {file = "watchfiles-0.23.0-cp310-none-win32.whl", hash = "sha256:296e0b29ab0276ca59d82d2da22cbbdb39a23eed94cca69aed274595fb3dfe42"}, - {file = "watchfiles-0.23.0-cp310-none-win_amd64.whl", hash = "sha256:4ea756e425ab2dfc8ef2a0cb87af8aa7ef7dfc6fc46c6f89bcf382121d4fff75"}, - {file = "watchfiles-0.23.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e397b64f7aaf26915bf2ad0f1190f75c855d11eb111cc00f12f97430153c2eab"}, - {file = "watchfiles-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4ac73b02ca1824ec0a7351588241fd3953748d3774694aa7ddb5e8e46aef3e3"}, - {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130a896d53b48a1cecccfa903f37a1d87dbb74295305f865a3e816452f6e49e4"}, - {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5e7803a65eb2d563c73230e9d693c6539e3c975ccfe62526cadde69f3fda0cf"}, - {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1aa4cc85202956d1a65c88d18c7b687b8319dbe6b1aec8969784ef7a10e7d1a"}, - {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87f889f6e58849ddb7c5d2cb19e2e074917ed1c6e3ceca50405775166492cca8"}, - {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37fd826dac84c6441615aa3f04077adcc5cac7194a021c9f0d69af20fb9fa788"}, - {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee7db6e36e7a2c15923072e41ea24d9a0cf39658cb0637ecc9307b09d28827e1"}, - {file = "watchfiles-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2368c5371c17fdcb5a2ea71c5c9d49f9b128821bfee69503cc38eae00feb3220"}, - {file = "watchfiles-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:857af85d445b9ba9178db95658c219dbd77b71b8264e66836a6eba4fbf49c320"}, - {file = "watchfiles-0.23.0-cp311-none-win32.whl", hash = "sha256:1d636c8aeb28cdd04a4aa89030c4b48f8b2954d8483e5f989774fa441c0ed57b"}, - {file = "watchfiles-0.23.0-cp311-none-win_amd64.whl", hash = "sha256:46f1d8069a95885ca529645cdbb05aea5837d799965676e1b2b1f95a4206313e"}, - {file = "watchfiles-0.23.0-cp311-none-win_arm64.whl", hash = "sha256:e495ed2a7943503766c5d1ff05ae9212dc2ce1c0e30a80d4f0d84889298fa304"}, - {file = "watchfiles-0.23.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1db691bad0243aed27c8354b12d60e8e266b75216ae99d33e927ff5238d270b5"}, - {file = "watchfiles-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62d2b18cb1edaba311fbbfe83fb5e53a858ba37cacb01e69bc20553bb70911b8"}, - {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e087e8fdf1270d000913c12e6eca44edd02aad3559b3e6b8ef00f0ce76e0636f"}, - {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd41d5c72417b87c00b1b635738f3c283e737d75c5fa5c3e1c60cd03eac3af77"}, - {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5f3ca0ff47940ce0a389457b35d6df601c317c1e1a9615981c474452f98de1"}, - {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6991e3a78f642368b8b1b669327eb6751439f9f7eaaa625fae67dd6070ecfa0b"}, - {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f7252f52a09f8fa5435dc82b6af79483118ce6bd51eb74e6269f05ee22a7b9f"}, - {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e01bcb8d767c58865207a6c2f2792ad763a0fe1119fb0a430f444f5b02a5ea0"}, - {file = "watchfiles-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8e56fbcdd27fce061854ddec99e015dd779cae186eb36b14471fc9ae713b118c"}, - {file = "watchfiles-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bd3e2d64500a6cad28bcd710ee6269fbeb2e5320525acd0cfab5f269ade68581"}, - {file = "watchfiles-0.23.0-cp312-none-win32.whl", hash = "sha256:eb99c954291b2fad0eff98b490aa641e128fbc4a03b11c8a0086de8b7077fb75"}, - {file = "watchfiles-0.23.0-cp312-none-win_amd64.whl", hash = "sha256:dccc858372a56080332ea89b78cfb18efb945da858fabeb67f5a44fa0bcb4ebb"}, - {file = "watchfiles-0.23.0-cp312-none-win_arm64.whl", hash = "sha256:6c21a5467f35c61eafb4e394303720893066897fca937bade5b4f5877d350ff8"}, - {file = "watchfiles-0.23.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ba31c32f6b4dceeb2be04f717811565159617e28d61a60bb616b6442027fd4b9"}, - {file = "watchfiles-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85042ab91814fca99cec4678fc063fb46df4cbb57b4835a1cc2cb7a51e10250e"}, - {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24655e8c1c9c114005c3868a3d432c8aa595a786b8493500071e6a52f3d09217"}, - {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b1a950ab299a4a78fd6369a97b8763732bfb154fdb433356ec55a5bce9515c1"}, - {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8d3c5cd327dd6ce0edfc94374fb5883d254fe78a5e9d9dfc237a1897dc73cd1"}, - {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ff785af8bacdf0be863ec0c428e3288b817e82f3d0c1d652cd9c6d509020dd0"}, - {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02b7ba9d4557149410747353e7325010d48edcfe9d609a85cb450f17fd50dc3d"}, - {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a1b05c0afb2cd2f48c1ed2ae5487b116e34b93b13074ed3c22ad5c743109f0"}, - {file = "watchfiles-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:109a61763e7318d9f821b878589e71229f97366fa6a5c7720687d367f3ab9eef"}, - {file = "watchfiles-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9f8e6bb5ac007d4a4027b25f09827ed78cbbd5b9700fd6c54429278dacce05d1"}, - {file = "watchfiles-0.23.0-cp313-none-win32.whl", hash = "sha256:f46c6f0aec8d02a52d97a583782d9af38c19a29900747eb048af358a9c1d8e5b"}, - {file = "watchfiles-0.23.0-cp313-none-win_amd64.whl", hash = "sha256:f449afbb971df5c6faeb0a27bca0427d7b600dd8f4a068492faec18023f0dcff"}, - {file = "watchfiles-0.23.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:2dddc2487d33e92f8b6222b5fb74ae2cfde5e8e6c44e0248d24ec23befdc5366"}, - {file = "watchfiles-0.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e75695cc952e825fa3e0684a7f4a302f9128721f13eedd8dbd3af2ba450932b8"}, - {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2537ef60596511df79b91613a5bb499b63f46f01a11a81b0a2b0dedf645d0a9c"}, - {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20b423b58f5fdde704a226b598a2d78165fe29eb5621358fe57ea63f16f165c4"}, - {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b98732ec893975455708d6fc9a6daab527fc8bbe65be354a3861f8c450a632a4"}, - {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee1f5fcbf5bc33acc0be9dd31130bcba35d6d2302e4eceafafd7d9018c7755ab"}, - {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f195338a5a7b50a058522b39517c50238358d9ad8284fd92943643144c0c03"}, - {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524fcb8d59b0dbee2c9b32207084b67b2420f6431ed02c18bd191e6c575f5c48"}, - {file = "watchfiles-0.23.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0eff099a4df36afaa0eea7a913aa64dcf2cbd4e7a4f319a73012210af4d23810"}, - {file = "watchfiles-0.23.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a8323daae27ea290ba3350c70c836c0d2b0fb47897fa3b0ca6a5375b952b90d3"}, - {file = "watchfiles-0.23.0-cp38-none-win32.whl", hash = "sha256:aafea64a3ae698695975251f4254df2225e2624185a69534e7fe70581066bc1b"}, - {file = "watchfiles-0.23.0-cp38-none-win_amd64.whl", hash = "sha256:c846884b2e690ba62a51048a097acb6b5cd263d8bd91062cd6137e2880578472"}, - {file = "watchfiles-0.23.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a753993635eccf1ecb185dedcc69d220dab41804272f45e4aef0a67e790c3eb3"}, - {file = "watchfiles-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6bb91fa4d0b392f0f7e27c40981e46dda9eb0fbc84162c7fb478fe115944f491"}, - {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1f67312efa3902a8e8496bfa9824d3bec096ff83c4669ea555c6bdd213aa516"}, - {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ca6b71dcc50d320c88fb2d88ecd63924934a8abc1673683a242a7ca7d39e781"}, - {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aec5c29915caf08771d2507da3ac08e8de24a50f746eb1ed295584ba1820330"}, - {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1733b9bc2c8098c6bdb0ff7a3d7cb211753fecb7bd99bdd6df995621ee1a574b"}, - {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02ff5d7bd066c6a7673b17c8879cd8ee903078d184802a7ee851449c43521bdd"}, - {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18e2de19801b0eaa4c5292a223effb7cfb43904cb742c5317a0ac686ed604765"}, - {file = "watchfiles-0.23.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8ada449e22198c31fb013ae7e9add887e8d2bd2335401abd3cbc55f8c5083647"}, - {file = "watchfiles-0.23.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3af1b05361e1cc497bf1be654a664750ae61f5739e4bb094a2be86ec8c6db9b6"}, - {file = "watchfiles-0.23.0-cp39-none-win32.whl", hash = "sha256:486bda18be5d25ab5d932699ceed918f68eb91f45d018b0343e3502e52866e5e"}, - {file = "watchfiles-0.23.0-cp39-none-win_amd64.whl", hash = "sha256:d2d42254b189a346249424fb9bb39182a19289a2409051ee432fb2926bad966a"}, - {file = "watchfiles-0.23.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6a9265cf87a5b70147bfb2fec14770ed5b11a5bb83353f0eee1c25a81af5abfe"}, - {file = "watchfiles-0.23.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f02a259fcbbb5fcfe7a0805b1097ead5ba7a043e318eef1db59f93067f0b49b"}, - {file = "watchfiles-0.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebaebb53b34690da0936c256c1cdb0914f24fb0e03da76d185806df9328abed"}, - {file = "watchfiles-0.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd257f98cff9c6cb39eee1a83c7c3183970d8a8d23e8cf4f47d9a21329285cee"}, - {file = "watchfiles-0.23.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aba037c1310dd108411d27b3d5815998ef0e83573e47d4219f45753c710f969f"}, - {file = "watchfiles-0.23.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:a96ac14e184aa86dc43b8a22bb53854760a58b2966c2b41580de938e9bf26ed0"}, - {file = "watchfiles-0.23.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11698bb2ea5e991d10f1f4f83a39a02f91e44e4bd05f01b5c1ec04c9342bf63c"}, - {file = "watchfiles-0.23.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efadd40fca3a04063d40c4448c9303ce24dd6151dc162cfae4a2a060232ebdcb"}, - {file = "watchfiles-0.23.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:556347b0abb4224c5ec688fc58214162e92a500323f50182f994f3ad33385dcb"}, - {file = "watchfiles-0.23.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1cf7f486169986c4b9d34087f08ce56a35126600b6fef3028f19ca16d5889071"}, - {file = "watchfiles-0.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f18de0f82c62c4197bea5ecf4389288ac755896aac734bd2cc44004c56e4ac47"}, - {file = "watchfiles-0.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:532e1f2c491274d1333a814e4c5c2e8b92345d41b12dc806cf07aaff786beb66"}, - {file = "watchfiles-0.23.0.tar.gz", hash = "sha256:9338ade39ff24f8086bb005d16c29f8e9f19e55b18dcb04dfa26fcbc09da497b"}, -] - -[package.dependencies] -anyio = ">=3.0.0" - -[[package]] -name = "wcwidth" -version = "0.2.13" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = "*" -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - -[[package]] -name = "weaviate-client" -version = "4.7.1" -description = "A python native Weaviate client" -optional = false -python-versions = ">=3.8" -files = [ - {file = "weaviate_client-4.7.1-py3-none-any.whl", hash = "sha256:342f5c67b126cee4dc3a60467ad1ae74971cd5614e27af6fb13d687a345352c4"}, - {file = "weaviate_client-4.7.1.tar.gz", hash = "sha256:af99ac4e53613d2ff5b797372e95d004d0c8a1dd10a7f592068bcb423a30af30"}, -] - -[package.dependencies] -authlib = ">=1.2.1,<2.0.0" -grpcio = ">=1.57.0,<2.0.0" -grpcio-health-checking = ">=1.57.0,<2.0.0" -grpcio-tools = ">=1.57.0,<2.0.0" -httpx = ">=0.25.0,<=0.27.0" -pydantic = ">=2.5.0,<3.0.0" -requests = ">=2.30.0,<3.0.0" -validators = "0.33.0" - -[[package]] -name = "webencodings" -version = "0.5.1" -description = "Character encoding aliases for legacy web content" -optional = false -python-versions = "*" -files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] - -[[package]] -name = "websocket-client" -version = "1.8.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, - {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - -[[package]] -name = "websockets" -version = "12.0" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, - {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, - {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, - {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, - {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, - {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, - {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, - {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, - {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, - {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, - {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, - {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, - {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, - {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, - {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, - {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, - {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, - {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, - {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, - {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, - {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, - {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, - {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, - {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, - {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, -] - -[[package]] -name = "werkzeug" -version = "3.0.3" -description = "The comprehensive WSGI web application library." -optional = false -python-versions = ">=3.8" -files = [ - {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, - {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, -] - -[package.dependencies] -MarkupSafe = ">=2.1.1" - -[package.extras] -watchdog = ["watchdog (>=2.3)"] - -[[package]] -name = "wrapt" -version = "1.16.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.6" -files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, - {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, - {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, - {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, -] - -[[package]] -name = "yarl" -version = "1.9.4" -description = "Yet another URL library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, - {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, - {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, - {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, - {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, - {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, - {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, - {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, - {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, - {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, - {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, - {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, - {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, - {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, - {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, - {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" - -[[package]] -name = "zipp" -version = "3.20.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d"}, - {file = "zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31"}, -] - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - -[extras] -all = ["anthropic", "azure-ai-inference", "azure-core", "azure-cosmos", "azure-identity", "azure-search-documents", "chromadb", "ipykernel", "milvus", "mistralai", "motor", "ollama", "pinecone-client", "psycopg", "pyarrow", "pymilvus", "qdrant-client", "redis", "sentence-transformers", "torch", "transformers", "usearch", "weaviate-client"] -anthropic = ["anthropic"] -azure = ["azure-ai-inference", "azure-core", "azure-cosmos", "azure-identity", "azure-search-documents"] -chromadb = ["chromadb"] -google = ["google-cloud-aiplatform", "google-generativeai"] -hugging-face = ["sentence-transformers", "torch", "transformers"] -milvus = ["milvus", "pymilvus"] -mistralai = ["mistralai"] -mongo = ["motor"] -notebooks = ["ipykernel"] -ollama = ["ollama"] -pinecone = ["pinecone-client"] -postgres = ["psycopg"] -qdrant = ["qdrant-client"] -redis = ["redis", "types-redis"] -usearch = ["pyarrow", "usearch"] -weaviate = ["weaviate-client"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.10,<3.13" -content-hash = "45bffc6686e76fda8799c7a786d0618594cf8f8b7450bb8d805423882a0c20b3" diff --git a/python/pyproject.toml b/python/pyproject.toml index da2c21769037..c012607968bc 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,195 +1,138 @@ -[tool.poetry] +[project] name = "semantic-kernel" -version = "1.6.0" description = "Semantic Kernel Python SDK" -authors = ["Microsoft "] +authors = [{ name = "Microsoft", email = "SK-Support@microsoft.com"}] readme = "pip/README.md" +# Version read from __version__ field in __init__.py by Flit +dynamic = ["version"] packages = [{include = "semantic_kernel"}] -homepage = "https://learn.microsoft.com/en-us/semantic-kernel/overview/" -repository = "https://github.com/microsoft/semantic-kernel/" - -[tool.poetry.urls] -"Source Code" = "https://github.com/microsoft/semantic-kernel/tree/main/python" -"Release Notes" = "https://github.com/microsoft/semantic-kernel/releases?q=tag%3Apython-1&expanded=true" - -[tool.poetry.dependencies] -python = "^3.10,<3.13" - -# main dependencies -aiohttp = "^3.8" -pydantic = "^2" -pydantic-settings = "^2" -defusedxml = "^0.7.1" - -# embeddings -numpy = [ - { version = ">=1.25", python = "<3.12" }, - { version = ">=1.26", python = ">=3.12" }, +requires-python = ">=3.10,<3.13" +license = {file = "../LICENSE"} +urls.homepage = "https://learn.microsoft.com/en-us/semantic-kernel/overview/" +urls.source = "https://github.com/microsoft/semantic-kernel/tree/main/python" +urls.release_notes = "https://github.com/microsoft/semantic-kernel/releases?q=tag%3Apython-1&expanded=true" +urls.issues = "https://github.com/microsoft/semantic-kernel/issues" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Framework :: Pydantic :: 2", + "Typing :: Typed", ] +dependencies = [ + "aiohttp ~= 3.8", + "pydantic ~= 2.0", + "pydantic-settings ~= 2.0", + "defusedxml ~= 0.7", -# openai connector -openai = ">=1.0" + # embeddings + "numpy ~= 1.25.0; python_version < '3.12'", + "numpy ~= 1.26.0; python_version >= '3.12'", -# openapi and swagger -openapi_core = ">=0.18,<0.20" + # openai connector + "openai ~= 1.0", -# OpenTelemetry -opentelemetry-api = "^1.24.0" -opentelemetry-sdk = "^1.24.0" + # openapi and swagger + "openapi_core >= 0.18,<0.20", -prance = "^23.6.21.0" + # OpenTelemetry + "opentelemetry-api ~= 1.24", + "opentelemetry-sdk ~= 1.24", -# templating -pybars4 = "^0.9.13" -jinja2 = "^3.1.3" -nest-asyncio = "^1.6.0" + "prance ~= 23.6.21.0", + + # templating + "pybars4 ~= 0.9", + "jinja2 ~= 3.1", + "nest-asyncio ~= 1.6" +] ### Optional dependencies -# azure -azure-ai-inference = {version = "^1.0.0b1", allow-prereleases = true, optional = true} -azure-search-documents = {version = "11.6.0b4", allow-prereleases = true, optional = true} -azure-core = { version = "^1.28.0", optional = true} -azure-identity = { version = "^1.13.0", optional = true} -azure-cosmos = { version = "^4.7.0", optional = true} -# chroma -chromadb = { version = ">=0.4.13,<0.6.0", optional = true} -# google -google-cloud-aiplatform = { version = "^1.60.0", optional = true} -google-generativeai = { version = "^0.7.2", optional = true} -# hugging face -transformers = { version = "^4.28.1", extras=['torch'], optional = true} -sentence-transformers = { version = "^2.2.2", optional = true} -torch = {version = "2.2.2", optional = true} -# mongo -motor = { version = "^3.3.2", optional = true } -# notebooks -ipykernel = { version = "^6.21.1", optional = true} -# milvus -pymilvus = { version = ">=2.3,<2.4.6", optional = true} -milvus = { version = ">=2.3,<2.3.8", markers = 'sys_platform != "win32"', optional = true} -# mistralai -mistralai = { version = "^0.4.1", optional = true} -# ollama -ollama = { version = "^0.2.1", optional = true} -# anthropic -anthropic = { version = "^0.32.0", optional = true } -# pinecone -pinecone-client = { version = "^5.0.0", optional = true} -# postgres -psycopg = { version="^3.2.1", extras=["binary","pool"], optional = true} -# qdrant -qdrant-client = { version = '^1.9', optional = true} -# redis -redis = { version = "^5.0.7", extras=['hiredis'], optional = true} -types-redis = { version="^4.6.0.20240425", optional = true } -# usearch -usearch = { version = "^2.9", optional = true} -pyarrow = { version = ">=12.0.1,<18.0.0", optional = true} -weaviate-client = { version = ">=3.18,<5.0", optional = true} -pandas = {version = "^2.2.2", optional = true} - -[tool.poetry.group.dev.dependencies] -pre-commit = ">=3.7.1" -ipykernel = "^6.29.4" -nbconvert = "^7.16.4" -pytest = "^8.2.1" -pytest-xdist = { version="^3.6.1", extras=["psutil"]} -pytest-cov = ">=5.0.0" -pytest-asyncio = "^0.23.7" -snoop = "^0.4.3" -mypy = ">=1.10.0" -types-PyYAML = "^6.0.12.20240311" -ruff = "^0.5.2" - -[tool.poetry.group.unit-tests] -optional = true - -[tool.poetry.group.unit-tests.dependencies] -azure-ai-inference = {version = "^1.0.0b1", allow-prereleases = true} -azure-search-documents = {version = "11.6.0b4", allow-prereleases = true} -azure-core = "^1.28.0" -azure-cosmos = "^4.7.0" -mistralai = "^0.4.1" -ollama = "^0.2.1" -google-cloud-aiplatform = "^1.60.0" -anthropic = "^0.32.0" -google-generativeai = "^0.7.2" -transformers = { version = "^4.28.1", extras=['torch']} -sentence-transformers = { version = "^2.2.2"} -torch = {version = "2.2.2"} -# qdrant -qdrant-client = '^1.9' -# redis -redis = { version = "^5.0.7", extras=['hiredis']} -pandas = {version = "^2.2.2"} - -[tool.poetry.group.tests] -optional = true - -[tool.poetry.group.tests.dependencies] -# azure -azure-ai-inference = {version = "^1.0.0b1", allow-prereleases = true} -azure-search-documents = {version = "11.6.0b4", allow-prereleases = true} -azure-core = "^1.28.0" -azure-identity = "^1.13.0" -azure-cosmos = "^4.7.0" -msgraph-sdk = "^1.2.0" -# chroma -chromadb = ">=0.4.13,<0.6.0" -# google -google-cloud-aiplatform = "^1.60.0" -google-generativeai = "^0.7.2" -# hugging face -transformers = { version = "^4.28.1", extras=['torch']} -sentence-transformers = { version = "^2.2.2"} -torch = {version = "2.2.2"} -# milvus -pymilvus = ">=2.3,<2.4.6" -milvus = { version = ">=2.3,<2.3.8", markers = 'sys_platform != "win32"'} -# mistralai -mistralai = "^0.4.1" -# ollama -ollama = "^0.2.1" -# anthropic -anthropic = "^0.32.0" -# mongodb -motor = "^3.3.2" -# pinecone -pinecone-client = "^5.0.0" -# postgres -psycopg = { version="^3.1.9", extras=["binary","pool"]} -# qdrant -qdrant-client = '^1.9' -# redis -redis = { version="^5.0.7", extras=['hiredis']} -types-redis = { version="^4.6.0.20240425" } -# usearch -usearch = "^2.9" -pyarrow = ">=12.0.1,<18.0.0" -# weaviate -weaviate-client = ">=3.18,<5.0" -pandas = {version = "^2.2.2"} - -# Extras are exposed to pip, this allows a user to easily add the right dependencies to their environment -[tool.poetry.extras] -all = ["transformers", "sentence-transformers", "torch", "qdrant-client", "chromadb", "pymilvus", "milvus", "mistralai", "ollama", "anthropic", "google", "weaviate-client", "pinecone-client", "psycopg", "redis", "azure-ai-inference", "azure-search-documents", "azure-core", "azure-identity", "azure-cosmos", "usearch", "pyarrow", "ipykernel", "motor"] - -azure = ["azure-ai-inference", "azure-search-documents", "azure-core", "azure-identity", "azure-cosmos", "msgraph-sdk"] -chromadb = ["chromadb"] -google = ["google-cloud-aiplatform", "google-generativeai"] -hugging_face = ["transformers", "sentence-transformers", "torch"] -milvus = ["pymilvus", "milvus"] -mistralai = ["mistralai"] -ollama = ["ollama"] -anthropic = ["anthropic"] -mongo = ["motor"] -notebooks = ["ipykernel"] -pinecone = ["pinecone-client"] -postgres = ["psycopg"] -qdrant = ["qdrant-client"] -redis = ["redis", "types-redis"] -usearch = ["usearch", "pyarrow"] -weaviate = ["weaviate-client"] +[project.optional-dependencies] +azure = [ + "azure-ai-inference >= 1.0.0b3", + "azure-search-documents >= 11.6.0b4", + "azure-identity ~= 1.13", + "azure-cosmos ~= 4.7" +] +chroma = [ + "chromadb >= 0.4,<0.6" +] +google = [ + "google-cloud-aiplatform ~= 1.60", + "google-generativeai ~= 0.7" +] +hugging_face = [ + "transformers[torch] ~= 4.28", + "sentence-transformers ~= 2.2", + "torch == 2.2.2" +] +mongo = [ + "motor ~= 3.3.2" +] +notebooks = [ + "ipykernel ~= 6.29" +] +milvus = [ + "pymilvus >= 2.3,<2.4", + "milvus >= 2.3,<2.3.8; platform_system != 'Windows'" +] +mistralai = [ + "mistralai ~= 0.4" +] +ollama = [ + "ollama ~= 0.2" +] +anthropic = [ + "anthropic ~= 0.32" +] +pinecone = [ + "pinecone-client ~= 5.0" +] +postgres = [ + "psycopg[binary,pool] ~= 3.2" +] +qdrant = [ + "qdrant-client ~= 1.9" +] +redis = [ + "redis[hiredis] ~= 5.0", + "types-redis ~= 4.6.0.20240425" +] +usearch = [ + "usearch ~= 2.9", + "pyarrow >= 12.0,<18.0" +] +weaviate = [ + "weaviate-client >= 3.18,<5.0" +] +pandas = [ + "pandas ~= 2.2" +] + +[tool.uv] +prerelease = "if-necessary-or-explicit" +dev-dependencies = [ + "pre-commit ~= 3.7", + "ipykernel ~= 6.29", + "nbconvert ~= 7.16", + "pytest ~= 8.2", + "pytest-xdist[psutil] ~= 3.6", + "pytest-cov >= 5.0", + "pytest-asyncio ~= 0.23", + "snoop ~= 0.4", + "mypy >= 1.10", + "types-PyYAML ~= 6.0.12.20240311", + "ruff ~= 0.5" +] +environments = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'" +] [tool.pytest.ini_options] addopts = "-ra -q -r fEX" @@ -198,11 +141,11 @@ addopts = "-ra -q -r fEX" line-length = 120 target-version = "py310" include = ["*.py", "*.pyi", "**/pyproject.toml", "*.ipynb"] +preview = true [tool.ruff.lint] fixable = ["ALL"] unfixable = [] -preview = true select = [ "D", #pydocstyle checks "E", #error checks @@ -227,7 +170,6 @@ ignore = [ ] [tool.ruff.format] -preview = true docstring-code-format = true [tool.ruff.lint.pydocstyle] @@ -246,10 +188,10 @@ notice-rgx = "^# Copyright \\(c\\) Microsoft\\. All rights reserved\\." min-file-size = 1 [tool.bandit] -targets = ["python/semantic_kernel"] -exclude_dirs = ["python/tests"] +targets = ["semantic_kernel"] +exclude_dirs = ["tests"] [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["flit-core >= 3.8"] +build-backend = "flit_core.buildapi" diff --git a/python/samples/concepts/agents/chat_completion_function_termination.py b/python/samples/concepts/agents/chat_completion_function_termination.py new file mode 100644 index 000000000000..38ee6e76d832 --- /dev/null +++ b/python/samples/concepts/agents/chat_completion_function_termination.py @@ -0,0 +1,133 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +from typing import Annotated + +from semantic_kernel.agents import ChatCompletionAgent +from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior +from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion +from semantic_kernel.contents.chat_history import ChatHistory +from semantic_kernel.contents.chat_message_content import ChatMessageContent +from semantic_kernel.contents.function_call_content import FunctionCallContent +from semantic_kernel.contents.function_result_content import FunctionResultContent +from semantic_kernel.contents.utils.author_role import AuthorRole +from semantic_kernel.filters.auto_function_invocation.auto_function_invocation_context import ( + AutoFunctionInvocationContext, +) +from semantic_kernel.filters.filter_types import FilterTypes +from semantic_kernel.functions.kernel_function_decorator import kernel_function +from semantic_kernel.kernel import Kernel + +################################################################### +# The following sample demonstrates how to configure the auto # +# function invocation filter with use of a ChatCompletionAgent. # +################################################################### + + +# Define the agent name and instructions +HOST_NAME = "Host" +HOST_INSTRUCTIONS = "Answer questions about the menu." + + +# Define the auto function invocation filter that will be used by the kernel +async def auto_function_invocation_filter(context: AutoFunctionInvocationContext, next): + """A filter that will be called for each function call in the response.""" + # if we don't call next, it will skip this function, and go to the next one + await next(context) + if context.function.plugin_name == "menu": + context.terminate = True + + +# Define a sample plugin for the sample +class MenuPlugin: + """A sample Menu Plugin used for the concept sample.""" + + @kernel_function(description="Provides a list of specials from the menu.") + def get_specials(self) -> Annotated[str, "Returns the specials from the menu."]: + return """ + Special Soup: Clam Chowder + Special Salad: Cobb Salad + Special Drink: Chai Tea + """ + + @kernel_function(description="Provides the price of the requested menu item.") + def get_item_price( + self, menu_item: Annotated[str, "The name of the menu item."] + ) -> Annotated[str, "Returns the price of the menu item."]: + return "$9.99" + + +def _create_kernel_with_chat_completionand_filter(service_id: str) -> Kernel: + """A helper function to create a kernel with a chat completion service and a filter.""" + kernel = Kernel() + kernel.add_service(AzureChatCompletion(service_id=service_id)) + kernel.add_filter(FilterTypes.AUTO_FUNCTION_INVOCATION, auto_function_invocation_filter) + kernel.add_plugin(plugin=MenuPlugin(), plugin_name="menu") + return kernel + + +def _write_content(content: ChatMessageContent) -> None: + """Write the content to the console.""" + last_item_type = type(content.items[-1]).__name__ if content.items else "(empty)" + message_content = "" + if isinstance(last_item_type, FunctionCallContent): + message_content = f"tool request = {content.items[-1].function_name}" + elif isinstance(last_item_type, FunctionResultContent): + message_content = f"function result = {content.items[-1].result}" + else: + message_content = str(content.items[-1]) + print(f"[{last_item_type}] {content.role} : '{message_content}'") + + +# A helper method to invoke the agent with the user input +async def invoke_agent(agent: ChatCompletionAgent, input: str, chat_history: ChatHistory) -> None: + """Invoke the agent with the user input.""" + chat_history.add_user_message(input) + print(f"# {AuthorRole.USER}: '{input}'") + + async for content in agent.invoke(chat_history): + if not any(isinstance(item, (FunctionCallContent, FunctionResultContent)) for item in content.items): + chat_history.add_message(content) + _write_content(content) + + +async def main(): + service_id = "agent" + + # Create the kernel used by the chat completion agent + kernel = _create_kernel_with_chat_completionand_filter(service_id=service_id) + + settings = kernel.get_prompt_execution_settings_from_service_id(service_id=service_id) + + # Configure the function choice behavior to auto invoke kernel functions + settings.function_choice_behavior = FunctionChoiceBehavior.Auto() + + # Create the agent + agent = ChatCompletionAgent( + service_id=service_id, + kernel=kernel, + name=HOST_NAME, + instructions=HOST_INSTRUCTIONS, + execution_settings=settings, + ) + + # Define the chat history + chat = ChatHistory() + + # Respond to user input + await invoke_agent(agent=agent, input="Hello", chat_history=chat) + await invoke_agent(agent=agent, input="What is the special soup?", chat_history=chat) + await invoke_agent(agent=agent, input="What is the special drink?", chat_history=chat) + await invoke_agent(agent=agent, input="Thank you", chat_history=chat) + + print("================================") + print("CHAT HISTORY") + print("================================") + + # Print out the chat history to view the different types of messages + for message in chat.messages: + _write_content(message) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/concepts/agents/mixed_chat_agents_plugins.py b/python/samples/concepts/agents/mixed_chat_agents_plugins.py new file mode 100644 index 000000000000..6df7f88cac43 --- /dev/null +++ b/python/samples/concepts/agents/mixed_chat_agents_plugins.py @@ -0,0 +1,118 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +from typing import Annotated + +from semantic_kernel.agents import AgentGroupChat, ChatCompletionAgent +from semantic_kernel.agents.open_ai import OpenAIAssistantAgent +from semantic_kernel.agents.strategies.termination.termination_strategy import TerminationStrategy +from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior +from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion +from semantic_kernel.contents.chat_message_content import ChatMessageContent +from semantic_kernel.contents.utils.author_role import AuthorRole +from semantic_kernel.functions.kernel_function_decorator import kernel_function +from semantic_kernel.kernel import Kernel + +##################################################################### +# The following sample demonstrates how to create an OpenAI # +# assistant using either Azure OpenAI or OpenAI, a chat completion # +# agent and have them participate in a group chat to work towards # +# the user's requirement. The ChatCompletionAgent uses a plugin # +# that is part of the agent group chat. # +##################################################################### + + +class ApprovalTerminationStrategy(TerminationStrategy): + """A strategy for determining when an agent should terminate.""" + + async def should_agent_terminate(self, agent, history): + """Check if the agent should terminate.""" + return "approved" in history[-1].content.lower() + + +REVIEWER_NAME = "ArtDirector" +REVIEWER_INSTRUCTIONS = """ +You are an art director who has opinions about copywriting born of a love for David Ogilvy. +The goal is to determine if the given copy is acceptable to print. +If so, state that it is approved. Only include the word "approved" if it is so. +If not, provide insight on how to refine suggested copy without example. +You should always tie the conversation back to the food specials offered by the plugin. +""" + +COPYWRITER_NAME = "CopyWriter" +COPYWRITER_INSTRUCTIONS = """ +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. +""" + + +class MenuPlugin: + """A sample Menu Plugin used for the concept sample.""" + + @kernel_function(description="Provides a list of specials from the menu.") + def get_specials(self) -> Annotated[str, "Returns the specials from the menu."]: + return """ + Special Soup: Clam Chowder + Special Salad: Cobb Salad + Special Drink: Chai Tea + """ + + @kernel_function(description="Provides the price of the requested menu item.") + def get_item_price( + self, menu_item: Annotated[str, "The name of the menu item."] + ) -> Annotated[str, "Returns the price of the menu item."]: + return "$9.99" + + +def _create_kernel_with_chat_completion(service_id: str) -> Kernel: + kernel = Kernel() + kernel.add_service(AzureChatCompletion(service_id=service_id)) + kernel.add_plugin(plugin=MenuPlugin(), plugin_name="menu") + return kernel + + +async def main(): + try: + kernel = _create_kernel_with_chat_completion("artdirector") + settings = kernel.get_prompt_execution_settings_from_service_id(service_id="artdirector") + # Configure the function choice behavior to auto invoke kernel functions + settings.function_choice_behavior = FunctionChoiceBehavior.Auto() + agent_reviewer = ChatCompletionAgent( + service_id="artdirector", + kernel=kernel, + name=REVIEWER_NAME, + instructions=REVIEWER_INSTRUCTIONS, + execution_settings=settings, + ) + + agent_writer = await OpenAIAssistantAgent.create( + service_id="copywriter", + kernel=Kernel(), + name=COPYWRITER_NAME, + instructions=COPYWRITER_INSTRUCTIONS, + ) + + chat = AgentGroupChat( + agents=[agent_writer, agent_reviewer], + termination_strategy=ApprovalTerminationStrategy(agents=[agent_reviewer], maximum_iterations=10), + ) + + input = "Write copy based on the food specials." + + await chat.add_chat_message(ChatMessageContent(role=AuthorRole.USER, content=input)) + print(f"# {AuthorRole.USER}: '{input}'") + + async for content in chat.invoke(): + print(f"# {content.role} - {content.name or '*'}: '{content.content}'") + + print(f"# IS COMPLETE: {chat.is_complete}") + finally: + await agent_writer.delete() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/concepts/agents/mixed_chat_files.py b/python/samples/concepts/agents/mixed_chat_files.py index b97cce8dd593..b5d21c3fd09f 100644 --- a/python/samples/concepts/agents/mixed_chat_files.py +++ b/python/samples/concepts/agents/mixed_chat_files.py @@ -5,8 +5,8 @@ from semantic_kernel.agents import AgentGroupChat, ChatCompletionAgent from semantic_kernel.agents.open_ai import OpenAIAssistantAgent -from semantic_kernel.agents.strategies.termination.termination_strategy import TerminationStrategy from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion import AzureChatCompletion +from semantic_kernel.contents.annotation_content import AnnotationContent from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.kernel import Kernel @@ -19,21 +19,12 @@ ##################################################################### -class ApprovalTerminationStrategy(TerminationStrategy): - """A strategy for determining when an agent should terminate.""" - - async def should_agent_terminate(self, agent, history): - """Check if the agent should terminate.""" - return "approved" in history[-1].content.lower() - - SUMMARY_INSTRUCTIONS = "Summarize the entire conversation for the user in natural language." def _create_kernel_with_chat_completion(service_id: str) -> Kernel: kernel = Kernel() kernel.add_service(AzureChatCompletion(service_id=service_id)) - # kernel.add_service(OpenAIChatCompletion(service_id=service_id)) return kernel @@ -47,6 +38,12 @@ async def invoke_agent( async for content in chat.invoke(agent=agent): print(f"# {content.role} - {content.name or '*'}: '{content.content}'") + if len(content.items) > 0: + for item in content.items: + if isinstance(item, AnnotationContent): + print(f"\n`{item.quote}` => {item.file_id}") + response_content = await agent.client.files.content(item.file_id) + print(response_content.text) async def main(): diff --git a/python/samples/concepts/plugins/openapi/README.md b/python/samples/concepts/plugins/openapi/README.md index 4688b77be5f7..e93ebe0dca91 100644 --- a/python/samples/concepts/plugins/openapi/README.md +++ b/python/samples/concepts/plugins/openapi/README.md @@ -1,8 +1,10 @@ ### Running the OpenAPI syntax example +For more generic setup instructions, including how to install the `uv` tool, see the [main README](../../../../DEV_SETUP.md). + 1. In a terminal, navigate to `semantic_kernel/python/samples/kernel-syntax-examples/openapi_example`. -2. Run `poetry install` followed by `poetry shell` to enter poetry's virtual environment. +2. Run `uv sync` followed by `source .venv/bin/activate` to enter the virtual environment (depending on the os, the activate script may be in a different location). 3. Start the server by running `python openapi_server.py`. diff --git a/python/samples/concepts/setup/ALL_SETTINGS.md b/python/samples/concepts/setup/ALL_SETTINGS.md index 2a0e9b6fb80e..ea9e1db6ff74 100644 --- a/python/samples/concepts/setup/ALL_SETTINGS.md +++ b/python/samples/concepts/setup/ALL_SETTINGS.md @@ -14,6 +14,10 @@ OpenAI | [OpenAIChatCompletion](../../../semantic_kernel/connectors/ai/open_ai/s | | | ai_model_id | OPENAI_EMBEDDING_MODEL_ID | Yes | | | api_key | OPENAI_API_KEY | Yes | | | org_id | OPENAI_ORG_ID | No +| | [OpenAITextToImage](../../../semantic_kernel/connectors/ai/open_ai/services/open_ai_text_to_image.py) +| | | ai_model_id | OPENAI_TEXT_TO_IMAGE_MODEL_ID | Yes +| | | api_key | OPENAI_API_KEY | Yes +| | | org_id | OPENAI_ORG_ID | No Azure OpenAI | [AzureOpenAIChatCompletion](../../../semantic_kernel/connectors/ai/open_ai/services/azure_chat_completion.py) | | | | [AzureOpenAISettings](../../../semantic_kernel/connectors/ai/open_ai/settings/azure_open_ai_settings.py) | | | deployment_name | AZURE_OPENAI_CHAT_DEPLOYMENT_NAME | Yes | | | api_key | AZURE_OPENAI_API_KEY | Yes @@ -32,6 +36,10 @@ Azure OpenAI | [AzureOpenAIChatCompletion](../../../semantic_kernel/connectors/a | | | endpoint | AZURE_OPENAI_ENDPOINT | Yes | | | api_version | AZURE_OPENAI_API_VERSION | Yes | | | base_url | AZURE_OPENAI_BASE_URL | Yes +| | [AzureTextToImage](../../../semantic_kernel/connectors/ai/open_ai/services/azure_text_to_image.py) +| | | deployment_name | AZURE_OPENAI_TEXT_TO_IMAGE_DEPLOYMENT_NAME | Yes +| | | api_key | AZURE_OPENAI_API_KEY | Yes +| | | endpoint | AZURE_OPENAI_ENDPOINT | Yes ## Memory Service Settings used across SK: diff --git a/python/samples/demos/telemetry_with_application_insights/.env.example b/python/samples/demos/telemetry_with_application_insights/.env.example new file mode 100644 index 000000000000..3ee18ae9e6b0 --- /dev/null +++ b/python/samples/demos/telemetry_with_application_insights/.env.example @@ -0,0 +1 @@ +TELEMETRY_SAMPLE_CONNECTION_STRING="..." \ No newline at end of file diff --git a/python/samples/demos/telemetry_with_application_insights/README.md b/python/samples/demos/telemetry_with_application_insights/README.md new file mode 100644 index 000000000000..feadef29c0d0 --- /dev/null +++ b/python/samples/demos/telemetry_with_application_insights/README.md @@ -0,0 +1,49 @@ +# Semantic Kernel Python Telemetry with Application Insights + +This sample project shows how a Python application can be configured to send Semantic Kernel telemetry to Application Insights. + +> Note that it is also possible to use other Application Performance Management (APM) vendors. An example is [Prometheus](https://prometheus.io/docs/introduction/overview/). Please refer to this [link](https://opentelemetry.io/docs/languages/python/exporters/) to learn more about exporters. + + +For more information, please refer to the following resources: +1. [Azure Monitor OpenTelemetry Exporter](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry-exporter) +2. [Python Logging](https://docs.python.org/3/library/logging.html) +3. [Observability in Python](https://www.cncf.io/blog/2022/04/22/opentelemetry-and-python-a-complete-instrumentation-guide/) + +## What to expect + +The Semantic Kernel Python SDK is designed to efficiently generate comprehensive logs, traces, and metrics throughout the flow of function execution and model invocation. This allows you to effectively monitor your AI application's performance and accurately track token consumption. + +## Configuration + +### Required resources +1. [Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/create-workspace-resource) +2. OpenAI or [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?pivots=web-portal) + +### Dependencies +You will also need to install the following dependencies to your virtual environment to run this sample: +``` +pip install azure-monitor-opentelemetry-exporter==1.0.0b24 +``` + +## Running the sample + +1. Open a terminal and navigate to this folder: `python/samples/demos/telemetry_with_application_insights/`. This is necessary for the `.env` file to be read correctly. +2. Create a `.env` file if one doesn't already exist in this folder. Copy and paste your Application Insights connection string to the file. Please refer to the [example file](./.env.example). +3. Activate your python virtual environment, and then run `python main.py`. + +> This will output the Operation/Trace ID, which can be used later in Application Insights for searching the operation. + +## Application Insights/Azure Monitor + +### Logs and traces + +Go to your Application Insights instance, click on _Transaction search_ on the left menu. Use the operation id output by the program to search for the logs and traces associated with the operation. Click on any of the search result to view the end-to-end transaction details. Read more [here](https://learn.microsoft.com/en-us/azure/azure-monitor/app/transaction-search-and-diagnostics?tabs=transaction-search). + +### Metrics + +Running the application once will only generate one set of measurements (for each metrics). Run the application a couple times to generate more sets of measurements. + +> Note: Make sure not to run the program too frequently. Otherwise, you may get throttled. + +Please refer to here on how to analyze metrics in [Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/analyze-metrics). \ No newline at end of file diff --git a/python/samples/demos/telemetry_with_application_insights/main.py b/python/samples/demos/telemetry_with_application_insights/main.py new file mode 100644 index 000000000000..724737d2033f --- /dev/null +++ b/python/samples/demos/telemetry_with_application_insights/main.py @@ -0,0 +1,168 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import logging + +from azure.monitor.opentelemetry.exporter import ( + AzureMonitorLogExporter, + AzureMonitorMetricExporter, + AzureMonitorTraceExporter, +) +from opentelemetry import trace +from opentelemetry._logs import set_logger_provider +from opentelemetry.metrics import set_meter_provider +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader +from opentelemetry.sdk.metrics.view import DropAggregation, View +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.semconv.resource import ResourceAttributes +from opentelemetry.trace import set_tracer_provider + +from samples.demos.telemetry_with_application_insights.repo_utils import get_sample_plugin_path +from samples.demos.telemetry_with_application_insights.telemetry_sample_settings import TelemetrySampleSettings +from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_chat_completion import GoogleAIChatCompletion +from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.functions.kernel_arguments import KernelArguments +from semantic_kernel.kernel import Kernel + +# Load settings +settings = TelemetrySampleSettings.create() + +# Create a resource to represent the service/sample +resource = Resource.create({ResourceAttributes.SERVICE_NAME: "TelemetryExample"}) + + +def set_up_logging(): + log_exporter = AzureMonitorLogExporter(connection_string=settings.connection_string) + + # Create and set a global logger provider for the application. + logger_provider = LoggerProvider(resource=resource) + # Log processors are initialized with an exporter which is responsible + # for sending the telemetry data to a particular backend. + logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter)) + # Sets the global default logger provider + set_logger_provider(logger_provider) + + # Create a logging handler to write logging records, in OTLP format, to the exporter. + handler = LoggingHandler() + # Add a filter to the handler to only process records from semantic_kernel. + handler.addFilter(logging.Filter("semantic_kernel")) + # Attach the handler to the root logger. `getLogger()` with no arguments returns the root logger. + # Events from all child loggers will be processed by this handler. + logger = logging.getLogger() + logger.addHandler(handler) + # Set the logging level to NOTSET to allow all records to be processed by the handler. + logger.setLevel(logging.NOTSET) + + +def set_up_tracing(): + trace_exporter = AzureMonitorTraceExporter(connection_string=settings.connection_string) + + # Initialize a trace provider for the application. This is a factory for creating tracers. + tracer_provider = TracerProvider(resource=resource) + # Span processors are initialized with an exporter which is responsible + # for sending the telemetry data to a particular backend. + tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter)) + # Sets the global default tracer provider + set_tracer_provider(tracer_provider) + + +def set_up_metrics(): + metric_exporter = AzureMonitorMetricExporter(connection_string=settings.connection_string) + + # Initialize a metric provider for the application. This is a factory for creating meters. + metric_reader = PeriodicExportingMetricReader(metric_exporter, export_interval_millis=5000) + meter_provider = MeterProvider( + metric_readers=[metric_reader], + resource=resource, + views=[ + # Dropping all instrument names except for those starting with "semantic_kernel" + View(instrument_name="*", aggregation=DropAggregation()), + View(instrument_name="semantic_kernel*"), + ], + ) + # Sets the global default meter provider + set_meter_provider(meter_provider) + + +set_up_logging() +set_up_tracing() +set_up_metrics() + + +async def run_plugin(kernel: Kernel, plugin_name: str, service_id: str): + """Run a plugin with the given service ID.""" + plugin = kernel.get_plugin(plugin_name) + + poem = await kernel.invoke( + function=plugin["ShortPoem"], + arguments=KernelArguments( + input="Write a poem about John Doe.", + settings={ + service_id: PromptExecutionSettings(service_id=service_id), + }, + ), + ) + print(f"Poem:\n{poem}") + + print("\nTranslated poem:") + async for update in kernel.invoke_stream( + function=plugin["Translate"], + arguments=KernelArguments( + input=poem, + language="Italian", + settings={ + service_id: PromptExecutionSettings(service_id=service_id), + }, + ), + ): + print(update[0].content, end="") + print() + + +async def run_service(kernel: Kernel, plugin_name: str, service_id: str): + """Run a service with the given service ID.""" + print(f"================ Running service {service_id} ================") + + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span(service_id) as current_span: + try: + await run_plugin(kernel, plugin_name=plugin_name, service_id=service_id) + except Exception as e: + current_span.record_exception(e) + print(f"Error running service {service_id}: {e}") + + +async def main(): + # Initialize the kernel + kernel = Kernel() + kernel.add_service(OpenAIChatCompletion(service_id="open_ai")) + kernel.add_service(GoogleAIChatCompletion(service_id="google_ai")) + + # Add the sample plugin + if (sample_plugin_path := get_sample_plugin_path()) is None: + raise FileNotFoundError("Sample plugin path not found.") + print(f"Sample plugin path: {sample_plugin_path}") + plugin = kernel.add_plugin( + plugin_name="WriterPlugin", + parent_directory=sample_plugin_path, + ) + + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span("main") as current_span: + print(f"Trace ID: {current_span.get_span_context().trace_id}") + + # Run the OpenAI service + await run_service(kernel, plugin_name=plugin.name, service_id="open_ai") + + # Run the GoogleAI service + await run_service(kernel, plugin_name=plugin.name, service_id="google_ai") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/demos/telemetry_with_application_insights/repo_utils.py b/python/samples/demos/telemetry_with_application_insights/repo_utils.py new file mode 100644 index 000000000000..59a0b10efe99 --- /dev/null +++ b/python/samples/demos/telemetry_with_application_insights/repo_utils.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft. All rights reserved. + +import os + +SAMPLE_PLUGIN_FOLDER = "prompt_template_samples" + + +def get_sample_plugin_path(max_depth: int = 10) -> str | None: + """Find the path to the sample plugin folder. + + Args: + max_depth (int, optional): The maximum depth to search for the sample plugin folder. Defaults to 10. + Returns: + str | None: The path to the sample plugin folder or None if not found. + """ + curr_dir = os.path.dirname(os.path.abspath(__file__)) + + found = False + for _ in range(max_depth): + if SAMPLE_PLUGIN_FOLDER in os.listdir(curr_dir): + found = True + break + curr_dir = os.path.dirname(curr_dir) + + if found: + return os.path.join(curr_dir, SAMPLE_PLUGIN_FOLDER) + return None diff --git a/python/samples/demos/telemetry_with_application_insights/telemetry_sample_settings.py b/python/samples/demos/telemetry_with_application_insights/telemetry_sample_settings.py new file mode 100644 index 000000000000..d3d532d94964 --- /dev/null +++ b/python/samples/demos/telemetry_with_application_insights/telemetry_sample_settings.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import ClassVar + +from semantic_kernel.kernel_pydantic import KernelBaseSettings + + +class TelemetrySampleSettings(KernelBaseSettings): + """Settings for the telemetry sample application. + + Required settings for prefix 'TELEMETRY_SAMPLE_' are: + - connection_string: str - The connection string for the Application Insights resource. + This value can be found in the Overview section when examining + your resource from the Azure portal. + (Env var TELEMETRY_SAMPLE_CONNECTION_STRING) + """ + + env_prefix: ClassVar[str] = "TELEMETRY_SAMPLE_" + + connection_string: str diff --git a/python/samples/getting_started/00-getting-started.ipynb b/python/samples/getting_started/00-getting-started.ipynb index f2239967c7a3..950d9ce88a32 100644 --- a/python/samples/getting_started/00-getting-started.ipynb +++ b/python/samples/getting_started/00-getting-started.ipynb @@ -17,7 +17,7 @@ "outputs": [], "source": [ "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.6.0" + "%pip install semantic-kernel==1.8.0" ] }, { diff --git a/python/samples/getting_started/01-basic-loading-the-kernel.ipynb b/python/samples/getting_started/01-basic-loading-the-kernel.ipynb index 0b7991f02ae4..24cae548a047 100644 --- a/python/samples/getting_started/01-basic-loading-the-kernel.ipynb +++ b/python/samples/getting_started/01-basic-loading-the-kernel.ipynb @@ -24,7 +24,7 @@ "outputs": [], "source": [ "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.6.0" + "%pip install semantic-kernel==1.8.0" ] }, { diff --git a/python/samples/getting_started/02-running-prompts-from-file.ipynb b/python/samples/getting_started/02-running-prompts-from-file.ipynb index 12b2d658c068..4346ee3479d9 100644 --- a/python/samples/getting_started/02-running-prompts-from-file.ipynb +++ b/python/samples/getting_started/02-running-prompts-from-file.ipynb @@ -35,7 +35,7 @@ "outputs": [], "source": [ "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.6.0" + "%pip install semantic-kernel==1.8.0" ] }, { diff --git a/python/samples/getting_started/03-prompt-function-inline.ipynb b/python/samples/getting_started/03-prompt-function-inline.ipynb index a4ea1707f42e..1b2fd84dede3 100644 --- a/python/samples/getting_started/03-prompt-function-inline.ipynb +++ b/python/samples/getting_started/03-prompt-function-inline.ipynb @@ -25,7 +25,7 @@ "outputs": [], "source": [ "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.6.0" + "%pip install semantic-kernel==1.8.0" ] }, { diff --git a/python/samples/getting_started/04-kernel-arguments-chat.ipynb b/python/samples/getting_started/04-kernel-arguments-chat.ipynb index 01ed946274bb..bd41cf5be190 100644 --- a/python/samples/getting_started/04-kernel-arguments-chat.ipynb +++ b/python/samples/getting_started/04-kernel-arguments-chat.ipynb @@ -27,7 +27,7 @@ "outputs": [], "source": [ "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.6.0" + "%pip install semantic-kernel==1.8.0" ] }, { diff --git a/python/samples/getting_started/05-using-the-planner.ipynb b/python/samples/getting_started/05-using-the-planner.ipynb index e7a0f371f19f..07869285daa2 100644 --- a/python/samples/getting_started/05-using-the-planner.ipynb +++ b/python/samples/getting_started/05-using-the-planner.ipynb @@ -32,7 +32,7 @@ "outputs": [], "source": [ "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.6.0" + "%pip install semantic-kernel==1.8.0" ] }, { diff --git a/python/samples/getting_started/06-memory-and-embeddings.ipynb b/python/samples/getting_started/06-memory-and-embeddings.ipynb index fcece19a6223..637f0ce70496 100644 --- a/python/samples/getting_started/06-memory-and-embeddings.ipynb +++ b/python/samples/getting_started/06-memory-and-embeddings.ipynb @@ -37,9 +37,9 @@ "outputs": [], "source": [ "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.6.0\n", + "%pip install semantic-kernel==1.8.0\n", "%pip install azure-core==1.30.1\n", - "%pip install azure-search-documents==11.6.0b4" + "%pip install azure-search-documents==11.8.0b4" ] }, { diff --git a/python/samples/getting_started/07-hugging-face-for-plugins.ipynb b/python/samples/getting_started/07-hugging-face-for-plugins.ipynb index a99b0294cd18..c9f6a1a01263 100644 --- a/python/samples/getting_started/07-hugging-face-for-plugins.ipynb +++ b/python/samples/getting_started/07-hugging-face-for-plugins.ipynb @@ -21,7 +21,7 @@ "outputs": [], "source": [ "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel[hugging_face]==1.6.0" + "%pip install semantic-kernel[hugging_face]==1.8.0" ] }, { diff --git a/python/samples/getting_started/08-native-function-inline.ipynb b/python/samples/getting_started/08-native-function-inline.ipynb index f414a8424bfc..fbe2012bb967 100644 --- a/python/samples/getting_started/08-native-function-inline.ipynb +++ b/python/samples/getting_started/08-native-function-inline.ipynb @@ -55,7 +55,7 @@ "outputs": [], "source": [ "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.6.0" + "%pip install semantic-kernel==1.8.0" ] }, { diff --git a/python/samples/getting_started/09-groundedness-checking.ipynb b/python/samples/getting_started/09-groundedness-checking.ipynb index f603b615da19..367d1949d6a7 100644 --- a/python/samples/getting_started/09-groundedness-checking.ipynb +++ b/python/samples/getting_started/09-groundedness-checking.ipynb @@ -36,7 +36,7 @@ "outputs": [], "source": [ "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.6.0" + "%pip install semantic-kernel==1.8.0" ] }, { diff --git a/python/samples/getting_started/10-multiple-results-per-prompt.ipynb b/python/samples/getting_started/10-multiple-results-per-prompt.ipynb index f4accdc8a8cb..14d893a24b8b 100644 --- a/python/samples/getting_started/10-multiple-results-per-prompt.ipynb +++ b/python/samples/getting_started/10-multiple-results-per-prompt.ipynb @@ -34,7 +34,7 @@ "outputs": [], "source": [ "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.6.0" + "%pip install semantic-kernel==1.8.0" ] }, { diff --git a/python/samples/getting_started/11-streaming-completions.ipynb b/python/samples/getting_started/11-streaming-completions.ipynb index 50e2d8684ec4..25dff07897d1 100644 --- a/python/samples/getting_started/11-streaming-completions.ipynb +++ b/python/samples/getting_started/11-streaming-completions.ipynb @@ -27,7 +27,7 @@ "outputs": [], "source": [ "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.6.0" + "%pip install semantic-kernel==1.8.0" ] }, { diff --git a/python/samples/getting_started/third_party/weaviate-persistent-memory.ipynb b/python/samples/getting_started/third_party/weaviate-persistent-memory.ipynb index ed15f5ab82cc..3b7821605e7b 100644 --- a/python/samples/getting_started/third_party/weaviate-persistent-memory.ipynb +++ b/python/samples/getting_started/third_party/weaviate-persistent-memory.ipynb @@ -156,7 +156,7 @@ "metadata": {}, "outputs": [], "source": [ - "%pip install semantic-kernel[weaviate]==1.6.0" + "%pip install semantic-kernel[weaviate]==1.8.0" ] }, { diff --git a/python/semantic_kernel/__init__.py b/python/semantic_kernel/__init__.py index 8499f48aba31..08cd98c223d2 100644 --- a/python/semantic_kernel/__init__.py +++ b/python/semantic_kernel/__init__.py @@ -2,4 +2,5 @@ from semantic_kernel.kernel import Kernel -__all__ = ["Kernel"] +__version__ = "1.8.0" +__all__ = ["Kernel", "__version__"] diff --git a/python/semantic_kernel/agents/channels/open_ai_assistant_channel.py b/python/semantic_kernel/agents/channels/open_ai_assistant_channel.py index 1d37b68dcc5b..53a96c54fb69 100644 --- a/python/semantic_kernel/agents/channels/open_ai_assistant_channel.py +++ b/python/semantic_kernel/agents/channels/open_ai_assistant_channel.py @@ -4,6 +4,8 @@ from collections.abc import AsyncIterable from typing import TYPE_CHECKING, Any +from semantic_kernel.contents.function_call_content import FunctionCallContent + if sys.version_info >= (3, 12): from typing import override # pragma: no cover else: @@ -36,6 +38,8 @@ async def receive(self, history: list["ChatMessageContent"]) -> None: history: The conversation messages. """ for message in history: + if any(isinstance(item, FunctionCallContent) for item in message.items): + continue await create_chat_message(self.client, self.thread_id, message) @override diff --git a/python/semantic_kernel/agents/open_ai/assistant_content_generation.py b/python/semantic_kernel/agents/open_ai/assistant_content_generation.py index 858c17817ae0..f1ba76cd88d0 100644 --- a/python/semantic_kernel/agents/open_ai/assistant_content_generation.py +++ b/python/semantic_kernel/agents/open_ai/assistant_content_generation.py @@ -47,7 +47,7 @@ async def create_chat_message( Returns: Message: The message. """ - if message.role.value not in allowed_message_roles: + if message.role.value not in allowed_message_roles and message.role != AuthorRole.TOOL: raise AgentExecutionException( f"Invalid message role `{message.role.value}`. Allowed roles are {allowed_message_roles}." ) @@ -56,7 +56,7 @@ async def create_chat_message( return await client.beta.threads.messages.create( thread_id=thread_id, - role=message.role.value, # type: ignore + role="assistant" if message.role == AuthorRole.TOOL else message.role.value, # type: ignore content=message_contents, # type: ignore ) @@ -78,6 +78,8 @@ def get_message_contents(message: "ChatMessageContent") -> list[dict[str, Any]]: "type": "image_file", "image_file": {"file_id": content.file_id}, }) + elif isinstance(content, FunctionResultContent): + contents.append({"type": "text", "text": content.result}) return contents diff --git a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py index bcccd2c28d7f..92fe5bb2af71 100644 --- a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py @@ -34,7 +34,10 @@ from semantic_kernel.connectors.ai.azure_ai_inference.services.azure_ai_inference_base import AzureAIInferenceBase from semantic_kernel.connectors.ai.azure_ai_inference.services.utils import MESSAGE_CONVERTERS from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase -from semantic_kernel.connectors.ai.function_calling_utils import update_settings_from_function_call_configuration +from semantic_kernel.connectors.ai.function_calling_utils import ( + merge_function_results, + update_settings_from_function_call_configuration, +) from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.contents.chat_message_content import ITEM_TYPES, ChatMessageContent @@ -151,11 +154,12 @@ async def get_chat_message_contents( for request_index in range(settings.function_choice_behavior.maximum_auto_invoke_attempts): completions = await self._send_chat_request(chat_history, settings) - chat_history.add_message(message=completions[0]) - function_calls = [item for item in chat_history.messages[-1].items if isinstance(item, FunctionCallContent)] + function_calls = [item for item in completions[0].items if isinstance(item, FunctionCallContent)] if (fc_count := len(function_calls)) == 0: return completions + chat_history.add_message(message=completions[0]) + results = await self._invoke_function_calls( function_calls=function_calls, chat_history=chat_history, @@ -167,7 +171,7 @@ async def get_chat_message_contents( ) if any(result.terminate for result in results if result is not None): - return completions + return merge_function_results(chat_history.messages[-len(results) :]) else: # do a final call without auto function calling return await self._send_chat_request(chat_history, settings) @@ -317,7 +321,8 @@ async def _get_streaming_chat_message_contents_auto_invoke( ) if any(result.terminate for result in results if result is not None): - return + yield merge_function_results(chat_history.messages[-len(results) :]) # type: ignore + break async def _send_chat_streaming_request( self, chat_history: ChatHistory, settings: AzureAIInferenceChatPromptExecutionSettings diff --git a/python/semantic_kernel/connectors/ai/function_calling_utils.py b/python/semantic_kernel/connectors/ai/function_calling_utils.py index 70240b45710f..852ad52da58d 100644 --- a/python/semantic_kernel/connectors/ai/function_calling_utils.py +++ b/python/semantic_kernel/connectors/ai/function_calling_utils.py @@ -3,6 +3,9 @@ from collections import OrderedDict from typing import TYPE_CHECKING, Any +from semantic_kernel.contents.chat_message_content import ChatMessageContent +from semantic_kernel.contents.function_result_content import FunctionResultContent +from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError if TYPE_CHECKING: @@ -68,3 +71,22 @@ def _combine_filter_dicts(*dicts: dict[str, list[str]]) -> dict: combined_filters[key] = list(combined_functions.keys()) return combined_filters + + +def merge_function_results( + messages: list[ChatMessageContent], +) -> list[ChatMessageContent]: + """Combine multiple function result content types to one chat message content type. + + This method combines the FunctionResultContent items from separate ChatMessageContent messages, + and is used in the event that the `context.terminate = True` condition is met. + """ + items: list[Any] = [] + for message in messages: + items.extend([item for item in message.items if isinstance(item, FunctionResultContent)]) + return [ + ChatMessageContent( + role=AuthorRole.TOOL, + items=items, + ) + ] diff --git a/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_chat_completion.py b/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_chat_completion.py index 8b72915b1b82..2b65dc298111 100644 --- a/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_chat_completion.py @@ -13,6 +13,7 @@ from google.generativeai.types import AsyncGenerateContentResponse, GenerateContentResponse, GenerationConfig from pydantic import ValidationError +from semantic_kernel.connectors.ai.function_calling_utils import merge_function_results from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import ( GoogleAIChatPromptExecutionSettings, ) @@ -132,11 +133,12 @@ async def get_chat_message_contents( for request_index in range(settings.function_choice_behavior.maximum_auto_invoke_attempts): completions = await self._send_chat_request(chat_history, settings) - chat_history.add_message(message=completions[0]) - function_calls = [item for item in chat_history.messages[-1].items if isinstance(item, FunctionCallContent)] + function_calls = [item for item in completions[0].items if isinstance(item, FunctionCallContent)] if (fc_count := len(function_calls)) == 0: return completions + chat_history.add_message(message=completions[0]) + results = await invoke_function_calls( function_calls=function_calls, chat_history=chat_history, @@ -148,7 +150,7 @@ async def get_chat_message_contents( ) if any(result.terminate for result in results if result is not None): - return completions + return merge_function_results(chat_history.messages[-len(results) :]) else: # do a final call without auto function calling return await self._send_chat_request(chat_history, settings) @@ -294,7 +296,8 @@ async def _get_streaming_chat_message_contents_auto_invoke( ) if any(result.terminate for result in results if result is not None): - return + yield merge_function_results(chat_history.messages[-len(results) :]) # type: ignore + break async def _send_chat_streaming_request( self, diff --git a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_chat_completion.py b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_chat_completion.py index 245f9a434e45..b2519d1e5edc 100644 --- a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_chat_completion.py @@ -10,6 +10,7 @@ from pydantic import ValidationError from vertexai.generative_models import Candidate, GenerationResponse, GenerativeModel +from semantic_kernel.connectors.ai.function_calling_utils import merge_function_results from semantic_kernel.connectors.ai.google.shared_utils import ( configure_function_choice_behavior, filter_system_message, @@ -126,11 +127,12 @@ async def get_chat_message_contents( for request_index in range(settings.function_choice_behavior.maximum_auto_invoke_attempts): completions = await self._send_chat_request(chat_history, settings) - chat_history.add_message(message=completions[0]) - function_calls = [item for item in chat_history.messages[-1].items if isinstance(item, FunctionCallContent)] + function_calls = [item for item in completions[0].items if isinstance(item, FunctionCallContent)] if (fc_count := len(function_calls)) == 0: return completions + chat_history.add_message(message=completions[0]) + results = await invoke_function_calls( function_calls=function_calls, chat_history=chat_history, @@ -142,7 +144,7 @@ async def get_chat_message_contents( ) if any(result.terminate for result in results if result is not None): - return completions + return merge_function_results(chat_history.messages[-len(results) :]) else: # do a final call without auto function calling return await self._send_chat_request(chat_history, settings) @@ -287,7 +289,8 @@ async def _get_streaming_chat_message_contents_auto_invoke( ) if any(result.terminate for result in results if result is not None): - return + yield merge_function_results(chat_history.messages[-len(results) :]) # type: ignore + break async def _send_chat_streaming_request( self, diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py index f1d6099e8e7d..e23cd9799e61 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py @@ -7,8 +7,6 @@ from functools import reduce from typing import TYPE_CHECKING, Any, ClassVar, cast -from semantic_kernel.contents.function_result_content import FunctionResultContent - if sys.version_info >= (3, 12): from typing import override # pragma: no cover else: @@ -24,7 +22,10 @@ from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase from semantic_kernel.connectors.ai.function_call_behavior import FunctionCallBehavior -from semantic_kernel.connectors.ai.function_calling_utils import update_settings_from_function_call_configuration +from semantic_kernel.connectors.ai.function_calling_utils import ( + merge_function_results, + update_settings_from_function_call_configuration, +) from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import ( OpenAIChatPromptExecutionSettings, @@ -111,13 +112,15 @@ async def get_chat_message_contents( # loop for auto-invoke function calls for request_index in range(settings.function_choice_behavior.maximum_auto_invoke_attempts): completions = await self._send_chat_request(settings) - # there is only one chat message, this was checked earlier - chat_history.add_message(message=completions[0]) - # get the function call contents from the chat message - function_calls = [item for item in chat_history.messages[-1].items if isinstance(item, FunctionCallContent)] + # get the function call contents from the chat message, there is only one chat message + # this was checked earlier + function_calls = [item for item in completions[0].items if isinstance(item, FunctionCallContent)] if (fc_count := len(function_calls)) == 0: return completions + # Since we have a function call, add the assistant's tool call message to the history + chat_history.add_message(message=completions[0]) + logger.info(f"processing {fc_count} tool calls in parallel.") # this function either updates the chat history with the function call results @@ -139,7 +142,7 @@ async def get_chat_message_contents( ) if any(result.terminate for result in results if result is not None): - return self._create_filter_early_terminate_chat_message_content(chat_history.messages[-len(results) :]) + return merge_function_results(chat_history.messages[-len(results) :]) self._update_settings(settings, chat_history, kernel=kernel) else: @@ -239,7 +242,7 @@ async def get_streaming_chat_message_contents( ], ) if any(result.terminate for result in results if result is not None): - yield self._create_filter_early_terminate_chat_message_content(chat_history.messages[-len(results) :]) # type: ignore + yield merge_function_results(chat_history.messages[-len(results) :]) # type: ignore break self._update_settings(settings, chat_history, kernel=kernel) @@ -318,25 +321,6 @@ def _create_streaming_chat_message_content( items=items, ) - def _create_filter_early_terminate_chat_message_content( - self, - messages: list[ChatMessageContent], - ) -> list[ChatMessageContent]: - """Add an early termination message to the chat messages. - - This method combines the FunctionResultContent items from separate ChatMessageContent messages, - and is used in the event that the `context.terminate = True` condition is met. - """ - items: list[Any] = [] - for message in messages: - items.extend([item for item in message.items if isinstance(item, FunctionResultContent)]) - return [ - ChatMessageContent( - role=AuthorRole.TOOL, - items=items, - ) - ] - def _get_metadata_from_chat_response(self, response: ChatCompletion) -> dict[str, Any]: """Get metadata from a chat response.""" return { diff --git a/python/semantic_kernel/connectors/memory/chroma/chroma_memory_store.py b/python/semantic_kernel/connectors/memory/chroma/chroma_memory_store.py index b0d8734449df..f6c1853b2cce 100644 --- a/python/semantic_kernel/connectors/memory/chroma/chroma_memory_store.py +++ b/python/semantic_kernel/connectors/memory/chroma/chroma_memory_store.py @@ -181,7 +181,7 @@ async def upsert_batch(self, collection_name: str, records: list[MemoryRecord]) # upsert is checking collection existence return [await self.upsert(collection_name, record) for record in records] - async def get(self, collection_name: str, key: str, with_embedding: bool) -> MemoryRecord: + async def get(self, collection_name: str, key: str, with_embedding: bool = False) -> MemoryRecord: """Gets a record. Args: @@ -200,7 +200,12 @@ async def get(self, collection_name: str, key: str, with_embedding: bool) -> Mem f"Record with key '{key}' does not exist in collection '{collection_name}'" ) from exc - async def get_batch(self, collection_name: str, keys: list[str], with_embeddings: bool) -> list[MemoryRecord]: + async def get_batch( + self, + collection_name: str, + keys: list[str], + with_embeddings: bool = False + ) -> list[MemoryRecord]: """Gets a batch of records. Args: diff --git a/python/semantic_kernel/contents/utils/data_uri.py b/python/semantic_kernel/contents/utils/data_uri.py index 3cf080af8577..a4407ff2237b 100644 --- a/python/semantic_kernel/contents/utils/data_uri.py +++ b/python/semantic_kernel/contents/utils/data_uri.py @@ -10,7 +10,7 @@ if sys.version < "3.11": from typing_extensions import Self # pragma: no cover else: - from typing import Self # pragma: no cover + from typing import Self # type: ignore # pragma: no cover from pydantic import Field, ValidationError, field_validator, model_validator from pydantic_core import Url diff --git a/python/semantic_kernel/core_plugins/sessions_python_tool/README.md b/python/semantic_kernel/core_plugins/sessions_python_tool/README.md index 472789bb5e33..8bcaa78c693d 100644 --- a/python/semantic_kernel/core_plugins/sessions_python_tool/README.md +++ b/python/semantic_kernel/core_plugins/sessions_python_tool/README.md @@ -6,7 +6,7 @@ Please follow the [Azure Container Apps Documentation](https://learn.microsoft.c ## Configuring the Python Plugin -To successfully use the Python Plugin in Semantic Kernel, you must install the Poetry `azure` extras by running `poetry install -E azure`. +To successfully use the Python Plugin in Semantic Kernel, you must install the `azure` extras by running `uv sync --extra azure` or `pip install semantic-kernel[azure]`. Next, as an environment variable or in the .env file, add the `poolManagementEndpoint` value from above to the variable `ACA_POOL_MANAGEMENT_ENDPOINT`. The `poolManagementEndpoint` should look something like: diff --git a/python/semantic_kernel/functions/kernel_function.py b/python/semantic_kernel/functions/kernel_function.py index 9b7f2a1eb317..b5d541a758ad 100644 --- a/python/semantic_kernel/functions/kernel_function.py +++ b/python/semantic_kernel/functions/kernel_function.py @@ -1,17 +1,22 @@ # Copyright (c) Microsoft. All rights reserved. import logging +import time from abc import abstractmethod from collections.abc import AsyncGenerator, Callable from copy import copy, deepcopy from inspect import isasyncgen, isgenerator from typing import TYPE_CHECKING, Any +from opentelemetry import metrics, trace +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE + from semantic_kernel.filters.filter_types import FilterTypes from semantic_kernel.filters.functions.function_invocation_context import FunctionInvocationContext from semantic_kernel.filters.kernel_filters_extension import _rebuild_function_invocation_context from semantic_kernel.functions.function_result import FunctionResult from semantic_kernel.functions.kernel_arguments import KernelArguments +from semantic_kernel.functions.kernel_function_log_messages import KernelFunctionLogMessages from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata from semantic_kernel.kernel_pydantic import KernelBaseModel @@ -34,8 +39,11 @@ from semantic_kernel.prompt_template.prompt_template_base import PromptTemplateBase from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig +# Logger, tracer and meter for observability logger: logging.Logger = logging.getLogger(__name__) - +tracer: trace.Tracer = trace.get_tracer(__name__) +meter: metrics.Meter = metrics.get_meter_provider().get_meter(__name__) +MEASUREMENT_FUNCTION_TAG_NAME: str = "semantic_kernel.function.name" TEMPLATE_FORMAT_MAP = { KERNEL_TEMPLATE_FORMAT_NAME: KernelPromptTemplate, @@ -67,6 +75,17 @@ class KernelFunction(KernelBaseModel): metadata: KernelFunctionMetadata + invocation_duration_histogram: metrics.Histogram = meter.create_histogram( + "semantic_kernel.function.invocation.duration", + unit="s", + description="Measures the duration of a function's execution", + ) + streaming_duration_histogram: metrics.Histogram = meter.create_histogram( + "semantic_kernel.function.streaming.duration", + unit="s", + description="Measures the duration of a function's streaming execution", + ) + @classmethod def from_prompt( cls, @@ -204,13 +223,30 @@ async def invoke( _rebuild_function_invocation_context() function_context = FunctionInvocationContext(function=self, kernel=kernel, arguments=arguments) - stack = kernel.construct_call_stack( - filter_type=FilterTypes.FUNCTION_INVOCATION, - inner_function=self._invoke_internal, - ) - await stack(function_context) - - return function_context.result + with tracer.start_as_current_span(self.fully_qualified_name) as current_span: + KernelFunctionLogMessages.log_function_invoking(logger, self.fully_qualified_name) + KernelFunctionLogMessages.log_function_arguments(logger, arguments) + + attributes = {MEASUREMENT_FUNCTION_TAG_NAME: self.fully_qualified_name} + starting_time_stamp = time.perf_counter() + try: + stack = kernel.construct_call_stack( + filter_type=FilterTypes.FUNCTION_INVOCATION, + inner_function=self._invoke_internal, + ) + await stack(function_context) + + KernelFunctionLogMessages.log_function_invoked_success(logger, self.fully_qualified_name) + KernelFunctionLogMessages.log_function_result_value(logger, function_context.result) + + return function_context.result + except Exception as e: + self._handle_exception(current_span, e, attributes) + raise e + finally: + duration = time.perf_counter() - starting_time_stamp + self.invocation_duration_histogram.record(duration, attributes) + KernelFunctionLogMessages.log_function_completed(logger, duration) @abstractmethod async def _invoke_internal_stream(self, context: FunctionInvocationContext) -> None: @@ -247,21 +283,35 @@ async def invoke_stream( _rebuild_function_invocation_context() function_context = FunctionInvocationContext(function=self, kernel=kernel, arguments=arguments) - stack = kernel.construct_call_stack( - filter_type=FilterTypes.FUNCTION_INVOCATION, - inner_function=self._invoke_internal_stream, - ) - await stack(function_context) - - if function_context.result is not None: - if isasyncgen(function_context.result.value): - async for partial in function_context.result.value: - yield partial - elif isgenerator(function_context.result.value): - for partial in function_context.result.value: - yield partial - else: - yield function_context.result + with tracer.start_as_current_span(self.fully_qualified_name) as current_span: + KernelFunctionLogMessages.log_function_streaming_invoking(logger, self.fully_qualified_name) + KernelFunctionLogMessages.log_function_arguments(logger, arguments) + + attributes = {MEASUREMENT_FUNCTION_TAG_NAME: self.fully_qualified_name} + starting_time_stamp = time.perf_counter() + try: + stack = kernel.construct_call_stack( + filter_type=FilterTypes.FUNCTION_INVOCATION, + inner_function=self._invoke_internal_stream, + ) + await stack(function_context) + + if function_context.result is not None: + if isasyncgen(function_context.result.value): + async for partial in function_context.result.value: + yield partial + elif isgenerator(function_context.result.value): + for partial in function_context.result.value: + yield partial + else: + yield function_context.result + except Exception as e: + self._handle_exception(current_span, e, attributes) + raise e + finally: + duration = time.perf_counter() - starting_time_stamp + self.streaming_duration_histogram.record(duration, attributes) + KernelFunctionLogMessages.log_function_streaming_completed(logger, duration) def function_copy(self, plugin_name: str | None = None) -> "KernelFunction": """Copy the function, can also override the plugin_name. @@ -277,3 +327,19 @@ def function_copy(self, plugin_name: str | None = None) -> "KernelFunction": if plugin_name: cop.metadata.plugin_name = plugin_name return cop + + def _handle_exception(self, current_span: trace.Span, exception: Exception, attributes: dict[str, str]) -> None: + """Handle the exception. + + Args: + current_span (trace.Span): The current span. + exception (Exception): The exception. + attributes (Attributes): The attributes to be modified. + """ + attributes[ERROR_TYPE] = type(exception).__name__ + + current_span.record_exception(exception) + current_span.set_attribute(ERROR_TYPE, type(exception).__name__) + current_span.set_status(trace.StatusCode.ERROR, description=str(exception)) + + KernelFunctionLogMessages.log_function_error(logger, exception) diff --git a/python/semantic_kernel/functions/kernel_function_log_messages.py b/python/semantic_kernel/functions/kernel_function_log_messages.py new file mode 100644 index 000000000000..a41e7f9adcc4 --- /dev/null +++ b/python/semantic_kernel/functions/kernel_function_log_messages.py @@ -0,0 +1,63 @@ +# Copyright (c) Microsoft. All rights reserved. + +import logging +from logging import Logger + +from semantic_kernel.functions.function_result import FunctionResult +from semantic_kernel.functions.kernel_arguments import KernelArguments + + +class KernelFunctionLogMessages: + """Kernel function log messages. + + This class contains static methods to log messages related to kernel functions. + """ + + @staticmethod + def log_function_invoking(logger: Logger, kernel_function_name: str): + """Log message when a kernel function is invoked.""" + logger.info("Function %s invoking.", kernel_function_name) + + @staticmethod + def log_function_arguments(logger: Logger, arguments: KernelArguments): + """Log message when a kernel function is invoked.""" + logger.debug("Function arguments: %s", arguments) + + @staticmethod + def log_function_invoked_success(logger: Logger, kernel_function_name: str): + """Log message when a kernel function is invoked successfully.""" + logger.info("Function %s succeeded.", kernel_function_name) + + @staticmethod + def log_function_result_value(logger: Logger, function_result: FunctionResult | None): + """Log message when a kernel function result is returned.""" + if not logger.isEnabledFor(logging.DEBUG): + return + + if function_result is not None: + try: + logger.debug("Function result: %s", function_result) + except Exception: + logger.error("Function result: Failed to convert result value to string") + else: + logger.debug("Function result: None") + + @staticmethod + def log_function_error(logger: Logger, error: Exception): + """Log message when a kernel function fails.""" + logger.error("Function failed. Error: %s", error) + + @staticmethod + def log_function_completed(logger: Logger, duration: float): + """Log message when a kernel function is completed.""" + logger.info("Function completed. Duration: %fs", duration) + + @staticmethod + def log_function_streaming_invoking(logger: Logger, kernel_function_name: str): + """Log message when a kernel function is invoked via streaming.""" + logger.info("Function %s streaming.", kernel_function_name) + + @staticmethod + def log_function_streaming_completed(logger: Logger, duration: float): + """Log message when a kernel function is completed via streaming.""" + logger.info("Function streaming completed. Duration: %fs", duration) diff --git a/python/setup_dev.sh b/python/setup_dev.sh deleted file mode 100644 index 98a642d3953b..000000000000 --- a/python/setup_dev.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -# this assumes Poetry is installed and in the Path, see https://python-poetry.org/docs/#installing-with-the-official-installer -# on macos run with `source ./setup_dev.sh` -poetry install -poetry run pre-commit install -poetry run pre-commit autoupdate diff --git a/python/tests/integration/completions/test_chat_completion_with_function_calling.py b/python/tests/integration/completions/test_chat_completion_with_function_calling.py index 47cdfaa7294f..bd1a047fe527 100644 --- a/python/tests/integration/completions/test_chat_completion_with_function_calling.py +++ b/python/tests/integration/completions/test_chat_completion_with_function_calling.py @@ -543,12 +543,9 @@ async def _test_helper( retries=5, ) - if test_type != FunctionChoiceTestTypes.AUTO or stream: - # Need to add the last response (the response from the model after it sees the tool call result) - # to the chat history. - # When not streaming: responses from within the auto invoke loop will be added to the history. - # When streaming, responses will not add the message to the history if the response doesn't - # contain a FunctionCallContent - history.add_message(cmc) + # We need to add the latest message to the history because the connector is + # not responsible for updating the history, unless it is related to auto function + # calling, when the history is updated after the function calls are invoked. + history.add_message(cmc) self.evaluate(history, inputs=inputs, test_type=test_type) diff --git a/python/tests/unit/agents/test_open_ai_assistant_base.py b/python/tests/unit/agents/test_open_ai_assistant_base.py index 815ca2b2208e..6ff2dfb5ac7a 100644 --- a/python/tests/unit/agents/test_open_ai_assistant_base.py +++ b/python/tests/unit/agents/test_open_ai_assistant_base.py @@ -810,9 +810,9 @@ async def test_add_chat_message( async def test_add_chat_message_invalid_role( azure_openai_assistant_agent, mock_chat_message_content, openai_unit_test_env ): - mock_chat_message_content.role = AuthorRole.TOOL + mock_chat_message_content.role = AuthorRole.SYSTEM - with pytest.raises(AgentExecutionException, match="Invalid message role `tool`"): + with pytest.raises(AgentExecutionException, match="Invalid message role `system`"): await azure_openai_assistant_agent.add_chat_message("test_thread_id", mock_chat_message_content) diff --git a/python/tests/unit/connectors/google/google_ai/services/test_google_ai_chat_completion.py b/python/tests/unit/connectors/google/google_ai/services/test_google_ai_chat_completion.py index bc24e98d43c0..f60640b9472c 100644 --- a/python/tests/unit/connectors/google/google_ai/services/test_google_ai_chat_completion.py +++ b/python/tests/unit/connectors/google/google_ai/services/test_google_ai_chat_completion.py @@ -189,10 +189,6 @@ async def test_google_ai_chat_completion_with_function_choice_behavior_no_tool_c kernel=kernel, ) - # Remove the latest message since the response from the model will be added to the chat history - # even when the model doesn't return a tool call - chat_history.remove_message(chat_history[-1]) - mock_google_ai_model_generate_content_async.assert_awaited_once_with( contents=google_ai_chat_completion._prepare_chat_history_for_request(chat_history), generation_config=GenerationConfig(**settings.prepare_settings_dict()), diff --git a/python/tests/unit/connectors/google/vertex_ai/services/test_vertex_ai_chat_completion.py b/python/tests/unit/connectors/google/vertex_ai/services/test_vertex_ai_chat_completion.py index 7bed2ae9e776..99ce39a455f7 100644 --- a/python/tests/unit/connectors/google/vertex_ai/services/test_vertex_ai_chat_completion.py +++ b/python/tests/unit/connectors/google/vertex_ai/services/test_vertex_ai_chat_completion.py @@ -189,10 +189,6 @@ async def test_vertex_ai_chat_completion_with_function_choice_behavior_no_tool_c kernel=kernel, ) - # Remove the latest message since the response from the model will be added to the chat history - # even when the model doesn't return a tool call - chat_history.remove_message(chat_history[-1]) - mock_vertex_ai_model_generate_content_async.assert_awaited_once_with( contents=vertex_ai_chat_completion._prepare_chat_history_for_request(chat_history), generation_config=settings.prepare_settings_dict(), diff --git a/python/uv.lock b/python/uv.lock new file mode 100644 index 000000000000..9d9d143261fc --- /dev/null +++ b/python/uv.lock @@ -0,0 +1,5347 @@ +version = 1 +requires-python = ">=3.10, <3.13" +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'win32'", +] +supported-markers = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'", +] + +[[package]] +name = "accelerate" +version = "0.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "safetensors", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "torch", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/24/5e813a41495ec7fdbc6a0f08e38c099caccf49147b8cd84053f4c3007c35/accelerate-0.33.0.tar.gz", hash = "sha256:11ba481ed6ea09191775df55ce464aeeba67a024bd0261a44b77b30fb439e26a", size = 314567 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/33/b6b4ad5efa8b9f4275d4ed17ff8a44c97276171341ba565fdffb0e3dc5e8/accelerate-0.33.0-py3-none-any.whl", hash = "sha256:0a7f33d60ba09afabd028d4f0856dd19c5a734b7a596d637d9dd6e3d0eadbaf3", size = 315131 }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f7/22bba300a16fd1cad99da1a23793fe43963ee326d012fdf852d0b4035955/aiohappyeyeballs-2.4.0.tar.gz", hash = "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2", size = 16786 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/b6/58ea188899950d759a837f9a58b2aee1d1a380ea4d6211ce9b1823748851/aiohappyeyeballs-2.4.0-py3-none-any.whl", hash = "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd", size = 12155 }, +] + +[[package]] +name = "aiohttp" +version = "3.10.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "aiosignal", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "async-timeout", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, + { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "frozenlist", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "multidict", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "yarl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/28/ca549838018140b92a19001a8628578b0f2a3b38c16826212cc6f706e6d4/aiohttp-3.10.5.tar.gz", hash = "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691", size = 7524360 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/4a/b27dd9b88fe22dde88742b341fd10251746a6ffcfe1c0b8b15b4a8cbd7c1/aiohttp-3.10.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3", size = 587010 }, + { url = "https://files.pythonhosted.org/packages/de/a9/0f7e2b71549c9d641086c423526ae7a10de3b88d03ba104a3df153574d0d/aiohttp-3.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6", size = 397698 }, + { url = "https://files.pythonhosted.org/packages/3b/52/26baa486e811c25b0cd16a494038260795459055568713f841e78f016481/aiohttp-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699", size = 389052 }, + { url = "https://files.pythonhosted.org/packages/33/df/71ba374a3e925539cb2f6e6d4f5326e7b6b200fabbe1b3cc5e6368f07ce7/aiohttp-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6", size = 1248615 }, + { url = "https://files.pythonhosted.org/packages/67/02/bb89c1eba08a27fc844933bee505d63d480caf8e2816c06961d2941cd128/aiohttp-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1", size = 1282930 }, + { url = "https://files.pythonhosted.org/packages/db/36/07d8cfcc37f39c039f93a4210cc71dadacca003609946c63af23659ba656/aiohttp-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f", size = 1317250 }, + { url = "https://files.pythonhosted.org/packages/9a/44/cabeac994bef8ba521b552ae996928afc6ee1975a411385a07409811b01f/aiohttp-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb", size = 1243212 }, + { url = "https://files.pythonhosted.org/packages/5a/11/23f1e31f5885ac72be52fd205981951dd2e4c87c5b1487cf82fde5bbd46c/aiohttp-3.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91", size = 1213401 }, + { url = "https://files.pythonhosted.org/packages/3f/e7/6e69a0b0d896fbaf1192d492db4c21688e6c0d327486da610b0e8195bcc9/aiohttp-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f", size = 1212450 }, + { url = "https://files.pythonhosted.org/packages/a9/7f/a42f51074c723ea848254946aec118f1e59914a639dc8ba20b0c9247c195/aiohttp-3.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c", size = 1211324 }, + { url = "https://files.pythonhosted.org/packages/d5/43/c2f9d2f588ccef8f028f0a0c999b5ceafecbda50b943313faee7e91f3e03/aiohttp-3.10.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69", size = 1266838 }, + { url = "https://files.pythonhosted.org/packages/c1/a7/ff9f067ecb06896d859e4f2661667aee4bd9c616689599ff034b63cbd9d7/aiohttp-3.10.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3", size = 1285301 }, + { url = "https://files.pythonhosted.org/packages/9a/e3/dd56bb4c67d216046ce61d98dec0f3023043f1de48f561df1bf93dd47aea/aiohttp-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683", size = 1235806 }, + { url = "https://files.pythonhosted.org/packages/a7/64/90dcd42ac21927a49ba4140b2e4d50e1847379427ef6c43eb338ef9960e3/aiohttp-3.10.5-cp310-cp310-win32.whl", hash = "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef", size = 360162 }, + { url = "https://files.pythonhosted.org/packages/f3/45/145d8b4853fc92c0c8509277642767e7726a085e390ce04353dc68b0f5b5/aiohttp-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088", size = 379173 }, + { url = "https://files.pythonhosted.org/packages/f1/90/54ccb1e4eadfb6c95deff695582453f6208584431d69bf572782e9ae542b/aiohttp-3.10.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2", size = 586455 }, + { url = "https://files.pythonhosted.org/packages/c3/7a/95e88c02756e7e718f054e1bb3ec6ad5d0ee4a2ca2bb1768c5844b3de30a/aiohttp-3.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf", size = 397255 }, + { url = "https://files.pythonhosted.org/packages/07/4f/767387b39990e1ee9aba8ce642abcc286d84d06e068dc167dab983898f18/aiohttp-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e", size = 388973 }, + { url = "https://files.pythonhosted.org/packages/61/46/0df41170a4d228c07b661b1ba9d87101d99a79339dc93b8b1183d8b20545/aiohttp-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77", size = 1326126 }, + { url = "https://files.pythonhosted.org/packages/af/20/da0d65e07ce49d79173fed41598f487a0a722e87cfbaa8bb7e078a7c1d39/aiohttp-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061", size = 1364538 }, + { url = "https://files.pythonhosted.org/packages/aa/20/b59728405114e57541ba9d5b96033e69d004e811ded299537f74237629ca/aiohttp-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697", size = 1399896 }, + { url = "https://files.pythonhosted.org/packages/2a/92/006690c31b830acbae09d2618e41308fe4c81c0679b3b33a3af859e0b7bf/aiohttp-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7", size = 1312914 }, + { url = "https://files.pythonhosted.org/packages/d4/71/1a253ca215b6c867adbd503f1e142117527ea8775e65962bc09b2fad1d2c/aiohttp-3.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0", size = 1271301 }, + { url = "https://files.pythonhosted.org/packages/0a/ab/5d1d9ff9ce6cce8fa54774d0364e64a0f3cd50e512ff09082ced8e5217a1/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5", size = 1291652 }, + { url = "https://files.pythonhosted.org/packages/75/5f/f90510ea954b9ae6e7a53d2995b97a3e5c181110fdcf469bc9238445871d/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e", size = 1286289 }, + { url = "https://files.pythonhosted.org/packages/be/9e/1f523414237798660921817c82b9225a363af436458caf584d2fa6a2eb4a/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1", size = 1341848 }, + { url = "https://files.pythonhosted.org/packages/f6/36/443472ddaa85d7d80321fda541d9535b23ecefe0bf5792cc3955ea635190/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277", size = 1361619 }, + { url = "https://files.pythonhosted.org/packages/19/f6/3ecbac0bc4359c7d7ba9e85c6b10f57e20edaf1f97751ad2f892db231ad0/aiohttp-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058", size = 1320869 }, + { url = "https://files.pythonhosted.org/packages/34/7e/ed74ffb36e3a0cdec1b05d8fbaa29cb532371d5a20058b3a8052fc90fe7c/aiohttp-3.10.5-cp311-cp311-win32.whl", hash = "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072", size = 359271 }, + { url = "https://files.pythonhosted.org/packages/98/1b/718901f04bc8c886a742be9e83babb7b93facabf7c475cc95e2b3ab80b4d/aiohttp-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff", size = 379143 }, + { url = "https://files.pythonhosted.org/packages/d9/1c/74f9dad4a2fc4107e73456896283d915937f48177b99867b63381fadac6e/aiohttp-3.10.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487", size = 583468 }, + { url = "https://files.pythonhosted.org/packages/12/29/68d090551f2b58ce76c2b436ced8dd2dfd32115d41299bf0b0c308a5483c/aiohttp-3.10.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a", size = 394066 }, + { url = "https://files.pythonhosted.org/packages/8f/f7/971f88b4cdcaaa4622925ba7d86de47b48ec02a9040a143514b382f78da4/aiohttp-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d", size = 389098 }, + { url = "https://files.pythonhosted.org/packages/f1/5a/fe3742efdce551667b2ddf1158b27c5b8eb1edc13d5e14e996e52e301025/aiohttp-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75", size = 1332742 }, + { url = "https://files.pythonhosted.org/packages/1a/52/a25c0334a1845eb4967dff279151b67ca32a948145a5812ed660ed900868/aiohttp-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178", size = 1372134 }, + { url = "https://files.pythonhosted.org/packages/96/3d/33c1d8efc2d8ec36bff9a8eca2df9fdf8a45269c6e24a88e74f2aa4f16bd/aiohttp-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e", size = 1414413 }, + { url = "https://files.pythonhosted.org/packages/64/74/0f1ddaa5f0caba1d946f0dd0c31f5744116e4a029beec454ec3726d3311f/aiohttp-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f", size = 1328107 }, + { url = "https://files.pythonhosted.org/packages/0a/32/c10118f0ad50e4093227234f71fd0abec6982c29367f65f32ee74ed652c4/aiohttp-3.10.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73", size = 1280126 }, + { url = "https://files.pythonhosted.org/packages/c6/c9/77e3d648d97c03a42acfe843d03e97be3c5ef1b4d9de52e5bd2d28eed8e7/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf", size = 1292660 }, + { url = "https://files.pythonhosted.org/packages/7e/5d/99c71f8e5c8b64295be421b4c42d472766b263a1fe32e91b64bf77005bf2/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820", size = 1300988 }, + { url = "https://files.pythonhosted.org/packages/8f/2c/76d2377dd947f52fbe8afb19b18a3b816d66c7966755c04030f93b1f7b2d/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca", size = 1339268 }, + { url = "https://files.pythonhosted.org/packages/fd/e6/3d9d935cc705d57ed524d82ec5d6b678a53ac1552720ae41282caa273584/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91", size = 1366993 }, + { url = "https://files.pythonhosted.org/packages/fe/c2/f7eed4d602f3f224600d03ab2e1a7734999b0901b1c49b94dc5891340433/aiohttp-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6", size = 1329459 }, + { url = "https://files.pythonhosted.org/packages/ce/8f/27f205b76531fc592abe29e1ad265a16bf934a9f609509c02d765e6a8055/aiohttp-3.10.5-cp312-cp312-win32.whl", hash = "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12", size = 356968 }, + { url = "https://files.pythonhosted.org/packages/39/8c/4f6c0b2b3629f6be6c81ab84d9d577590f74f01d4412bfc4067958eaa1e1/aiohttp-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc", size = 377650 }, + { url = "https://files.pythonhosted.org/packages/7b/b9/03b4327897a5b5d29338fa9b514f1c2f66a3e4fc88a4e40fad478739314d/aiohttp-3.10.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092", size = 576994 }, + { url = "https://files.pythonhosted.org/packages/67/1b/20c2e159cd07b8ed6dde71c2258233902fdf415b2fe6174bd2364ba63107/aiohttp-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77", size = 390684 }, + { url = "https://files.pythonhosted.org/packages/4d/6b/ff83b34f157e370431d8081c5d1741963f4fb12f9aaddb2cacbf50305225/aiohttp-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385", size = 386176 }, + { url = "https://files.pythonhosted.org/packages/4d/a1/6e92817eb657de287560962df4959b7ddd22859c4b23a0309e2d3de12538/aiohttp-3.10.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972", size = 1303310 }, + { url = "https://files.pythonhosted.org/packages/04/29/200518dc7a39c30ae6d5bc232d7207446536e93d3d9299b8e95db6e79c54/aiohttp-3.10.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16", size = 1340445 }, + { url = "https://files.pythonhosted.org/packages/8e/20/53f7bba841ba7b5bb5dea580fea01c65524879ba39cb917d08c845524717/aiohttp-3.10.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6", size = 1385121 }, + { url = "https://files.pythonhosted.org/packages/f1/b4/d99354ad614c48dd38fb1ee880a1a54bd9ab2c3bcad3013048d4a1797d3a/aiohttp-3.10.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa", size = 1299669 }, + { url = "https://files.pythonhosted.org/packages/51/39/ca1de675f2a5729c71c327e52ac6344e63f036bd37281686ae5c3fb13bfb/aiohttp-3.10.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689", size = 1252638 }, + { url = "https://files.pythonhosted.org/packages/54/cf/a3ae7ff43138422d477348e309ef8275779701bf305ff6054831ef98b782/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57", size = 1266889 }, + { url = "https://files.pythonhosted.org/packages/6e/7a/c6027ad70d9fb23cf254a26144de2723821dade1a624446aa22cd0b6d012/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f", size = 1266249 }, + { url = "https://files.pythonhosted.org/packages/64/fd/ed136d46bc2c7e3342fed24662b4827771d55ceb5a7687847aae977bfc17/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599", size = 1311036 }, + { url = "https://files.pythonhosted.org/packages/76/9a/43eeb0166f1119256d6f43468f900db1aed7fbe32069d2a71c82f987db4d/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5", size = 1338756 }, + { url = "https://files.pythonhosted.org/packages/d5/bc/d01ff0810b3f5e26896f76d44225ed78b088ddd33079b85cd1a23514318b/aiohttp-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987", size = 1299976 }, + { url = "https://files.pythonhosted.org/packages/3e/c9/50a297c4f7ab57a949f4add2d3eafe5f3e68bb42f739e933f8b32a092bda/aiohttp-3.10.5-cp313-cp313-win32.whl", hash = "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04", size = 355609 }, + { url = "https://files.pythonhosted.org/packages/65/28/aee9d04fb0b3b1f90622c338a08e54af5198e704a910e20947c473298fd0/aiohttp-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022", size = 375697 }, +] + +[[package]] +name = "aiosignal" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anthropic" +version = "0.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "distro", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jiter", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "sniffio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/e2/98ff733ff75c1d371c029fb27eb9308f9c8e694749cea70382338a8e7e88/anthropic-0.34.1.tar.gz", hash = "sha256:69e822bd7a31ec11c2edb85f2147e8f0ee0cfd3288fea70b0ca8808b2f9bf91d", size = 901462 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/1c/1ce9edec76885badebacb4e31d42acffbdfd30dbaa839d5c378d57ac9aa9/anthropic-0.34.1-py3-none-any.whl", hash = "sha256:2fa26710809d0960d970f26cd0be3686437250a481edb95c33d837aa5fa24158", size = 891537 }, +] + +[[package]] +name = "anyio" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "sniffio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/e3/c4c8d473d6780ef1853d630d581f70d655b4f8d7553c6997958c283039a2/anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", size = 163930 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/a2/10639a79341f6c019dedc95bd48a4928eed9f1d1197f4c04f546fc7ae0ff/anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7", size = 86780 }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321 }, +] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/fa/57ec2c6d16ecd2ba0cf15f3c7d1c3c2e7b5fcb83555ff56d7ab10888ec8f/argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08", size = 42798 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea", size = 15124 }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658 }, + { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583 }, + { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168 }, + { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709 }, + { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613 }, + { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583 }, + { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475 }, + { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698 }, + { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817 }, + { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104 }, +] + +[[package]] +name = "asgiref" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, +] + +[[package]] +name = "asttokens" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/1d/f03bcb60c4a3212e15f99a56085d93093a497718adf828d050b9d675da81/asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0", size = 62284 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", size = 27764 }, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 }, +] + +[[package]] +name = "attrs" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, +] + +[[package]] +name = "authlib" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/47/df70ecd34fbf86d69833fe4e25bb9ecbaab995c8e49df726dd416f6bb822/authlib-1.3.1.tar.gz", hash = "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917", size = 146074 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/1f/bc95e43ffb57c05b8efcc376dd55a0240bf58f47ddf5a0f92452b6457b75/Authlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377", size = 223827 }, +] + +[[package]] +name = "azure-ai-inference" +version = "1.0.0b3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/89/5ddefdc2ce920b68bc741e3be6c2a0ca58702eeca778546a356c3ae7fe24/azure-ai-inference-1.0.0b3.tar.gz", hash = "sha256:1e99dc74c3b335a457500311bbbadb348f54dc4c12252a93cb8ab78d6d217ff0", size = 104451 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/79/9f84eb6c03c6e18b36317b587b29b3244037ab2f65acd48666d9e62fdfdc/azure_ai_inference-1.0.0b3-py3-none-any.whl", hash = "sha256:6734ca7334c809a170beb767f1f1455724ab3f006cb60045e42a833c0e764403", size = 85951 }, +] + +[[package]] +name = "azure-common" +version = "1.1.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/71/f6f71a276e2e69264a97ad39ef850dca0a04fce67b12570730cb38d0ccac/azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3", size = 20914 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/55/7f118b9c1b23ec15ca05d15a578d8207aa1706bc6f7c87218efffbbf875d/azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad", size = 14462 }, +] + +[[package]] +name = "azure-core" +version = "1.30.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/d4/1f469fa246f554b86fb5cebc30eef1b2a38b7af7a2c2791bce0a4c6e4604/azure-core-1.30.2.tar.gz", hash = "sha256:a14dc210efcd608821aa472d9fb8e8d035d29b68993819147bc290a8ac224472", size = 271104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/d7/69d53f37733f8cb844862781767aef432ff3152bc9b9864dc98c7e286ce9/azure_core-1.30.2-py3-none-any.whl", hash = "sha256:cf019c1ca832e96274ae85abd3d9f752397194d9fea3b41487290562ac8abe4a", size = 194253 }, +] + +[[package]] +name = "azure-cosmos" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/5a/1ae2e5e58da70ffcb2ee50fadece728b4941489cc8febbbf46d5522f6fff/azure-cosmos-4.7.0.tar.gz", hash = "sha256:72d714033134656302a2e8957c4b93590673bd288b0ca60cb123e348ae99a241", size = 381958 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/d4/38929bb3504bb9a2cb21ede7a1c652ebe7487b65da8885ea4039647bba86/azure_cosmos-4.7.0-py3-none-any.whl", hash = "sha256:03d8c7740ddc2906fb16e07b136acc0fe6a6a02656db46c5dd6f1b127b58cc96", size = 252084 }, +] + +[[package]] +name = "azure-identity" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "msal", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "msal-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/c9/f7e3926686a89670ce641b360bd2da9a2d7a12b3e532403462d99f81e9d5/azure-identity-1.17.1.tar.gz", hash = "sha256:32ecc67cc73f4bd0595e4f64b1ca65cd05186f4fe6f98ed2ae9f1aa32646efea", size = 246652 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/83/a777861351e7b99e7c84ff3b36bab35e87b6e5d36e50b6905e148c696515/azure_identity-1.17.1-py3-none-any.whl", hash = "sha256:db8d59c183b680e763722bfe8ebc45930e6c57df510620985939f7f3191e0382", size = 173229 }, +] + +[[package]] +name = "azure-search-documents" +version = "11.6.0b4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/8b/1253b0694d54fe55edf8a18ee29af04247d1113b870c40ea95cf4fdb8176/azure-search-documents-11.6.0b4.tar.gz", hash = "sha256:b09fc3fa2813e83e7177874b352c84462fb86934d9f4299775361e1dfccc3f8f", size = 324608 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/9c/4f1cd68cdedc014d7d157d86671c648b2983184e098fc659c27c9a5ccd60/azure_search_documents-11.6.0b4-py3-none-any.whl", hash = "sha256:9590392464f882762ce6bad03613c822d4423f09f311c275b833de25398c00c1", size = 325310 }, +] + +[[package]] +name = "azure-storage-blob" +version = "12.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/de/9cea85c0d5fc21f99bcf9f060fc2287cb95236b70431fa63cb69890a121e/azure-storage-blob-12.22.0.tar.gz", hash = "sha256:b3804bb4fe8ab1c32771fa464053da772a682c2737b19da438a3f4e5e3b3736e", size = 564873 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/52/b578c94048469fbf9f6378e2b2a46a2d0ccba3d59a7845dbed22ebf61601/azure_storage_blob-12.22.0-py3-none-any.whl", hash = "sha256:bb7d2d824ce3f11f14a27ee7d9281289f7e072ac8311c52e3652672455b7d5e8", size = 404892 }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 }, +] + +[[package]] +name = "bcrypt" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/7e/d95e7d96d4828e965891af92e43b52a4cd3395dc1c1ef4ee62748d0471d0/bcrypt-4.2.0.tar.gz", hash = "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221", size = 24294 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/81/4e8f5bc0cd947e91fb720e1737371922854da47a94bc9630454e7b2845f8/bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb", size = 471568 }, + { url = "https://files.pythonhosted.org/packages/05/d2/1be1e16aedec04bcf8d0156e01b987d16a2063d38e64c3f28030a3427d61/bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00", size = 277372 }, + { url = "https://files.pythonhosted.org/packages/e3/96/7a654027638ad9b7589effb6db77eb63eba64319dfeaf9c0f4ca953e5f76/bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d", size = 273488 }, + { url = "https://files.pythonhosted.org/packages/46/54/dc7b58abeb4a3d95bab653405935e27ba32f21b812d8ff38f271fb6f7f55/bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291", size = 277759 }, + { url = "https://files.pythonhosted.org/packages/ac/be/da233c5f11fce3f8adec05e8e532b299b64833cc962f49331cdd0e614fa9/bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328", size = 273796 }, + { url = "https://files.pythonhosted.org/packages/b0/b8/8b4add88d55a263cf1c6b8cf66c735280954a04223fcd2880120cc767ac3/bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7", size = 311082 }, + { url = "https://files.pythonhosted.org/packages/7b/76/2aa660679abbdc7f8ee961552e4bb6415a81b303e55e9374533f22770203/bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399", size = 305912 }, + { url = "https://files.pythonhosted.org/packages/00/03/2af7c45034aba6002d4f2b728c1a385676b4eab7d764410e34fd768009f2/bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060", size = 325185 }, + { url = "https://files.pythonhosted.org/packages/dc/5d/6843443ce4ab3af40bddb6c7c085ed4a8418b3396f7a17e60e6d9888416c/bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7", size = 335188 }, + { url = "https://files.pythonhosted.org/packages/cb/4c/ff8ca83d816052fba36def1d24e97d9a85739b9bbf428c0d0ecd296a07c8/bcrypt-4.2.0-cp37-abi3-win32.whl", hash = "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458", size = 156481 }, + { url = "https://files.pythonhosted.org/packages/65/f1/e09626c88a56cda488810fb29d5035f1662873777ed337880856b9d204ae/bcrypt-4.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5", size = 151336 }, + { url = "https://files.pythonhosted.org/packages/96/86/8c6a84daed4dd878fbab094400c9174c43d9b838ace077a2f8ee8bc3ae12/bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841", size = 472414 }, + { url = "https://files.pythonhosted.org/packages/f6/05/e394515f4e23c17662e5aeb4d1859b11dc651be01a3bd03c2e919a155901/bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68", size = 277599 }, + { url = "https://files.pythonhosted.org/packages/4b/3b/ad784eac415937c53da48983756105d267b91e56aa53ba8a1b2014b8d930/bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe", size = 273491 }, + { url = "https://files.pythonhosted.org/packages/cc/14/b9ff8e0218bee95e517b70e91130effb4511e8827ac1ab00b4e30943a3f6/bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2", size = 277934 }, + { url = "https://files.pythonhosted.org/packages/3e/d0/31938bb697600a04864246acde4918c4190a938f891fd11883eaaf41327a/bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c", size = 273804 }, + { url = "https://files.pythonhosted.org/packages/e7/c3/dae866739989e3f04ae304e1201932571708cb292a28b2f1b93283e2dcd8/bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae", size = 311275 }, + { url = "https://files.pythonhosted.org/packages/5d/2c/019bc2c63c6125ddf0483ee7d914a405860327767d437913942b476e9c9b/bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d", size = 306355 }, + { url = "https://files.pythonhosted.org/packages/75/fe/9e137727f122bbe29771d56afbf4e0dbc85968caa8957806f86404a5bfe1/bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e", size = 325381 }, + { url = "https://files.pythonhosted.org/packages/1a/d4/586b9c18a327561ea4cd336ff4586cca1a7aa0f5ee04e23a8a8bb9ca64f1/bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8", size = 335685 }, + { url = "https://files.pythonhosted.org/packages/24/55/1a7127faf4576138bb278b91e9c75307490178979d69c8e6e273f74b974f/bcrypt-4.2.0-cp39-abi3-win32.whl", hash = "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34", size = 155857 }, + { url = "https://files.pythonhosted.org/packages/1c/2a/c74052e54162ec639266d91539cca7cbf3d1d3b8b36afbfeaee0ea6a1702/bcrypt-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9", size = 151717 }, + { url = "https://files.pythonhosted.org/packages/09/97/01026e7b1b7f8aeb41514408eca1137c0f8aef9938335e3bc713f82c282e/bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a", size = 275924 }, + { url = "https://files.pythonhosted.org/packages/ca/46/03eb26ea3e9c12ca18d1f3bf06199f7d72ce52e68f2a1ebcfd8acff9c472/bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db", size = 272242 }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, +] + +[[package]] +name = "bleach" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "webencodings", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/10/77f32b088738f40d4f5be801daa5f327879eadd4562f36a2b5ab975ae571/bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe", size = 202119 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/63/da7237f805089ecc28a3f36bca6a21c31fcbc2eb380f3b8f1be3312abd14/bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6", size = 162750 }, +] + +[[package]] +name = "build" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "(os_name == 'nt' and sys_platform == 'darwin') or (os_name == 'nt' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform == 'win32')" }, + { name = "importlib-metadata", marker = "(python_full_version < '3.10.2' and sys_platform == 'darwin') or (python_full_version < '3.10.2' and sys_platform == 'linux') or (python_full_version < '3.10.2' and sys_platform == 'win32')" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyproject-hooks", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/9e/2d725d2f7729c6e79ca62aeb926492abbc06e25910dd30139d60a68bcb19/build-1.2.1.tar.gz", hash = "sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d", size = 44781 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/03/f3c8ba0a6b6e30d7d18c40faab90807c9bb5e9a1e3b2fe2008af624a9c97/build-1.2.1-py3-none-any.whl", hash = "sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4", size = 21911 }, +] + +[[package]] +name = "cachetools" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/38/a0f315319737ecf45b4319a8cd1f3a908e29d9277b46942263292115eee7/cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a", size = 27661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/07/14f8ad37f2d12a5ce41206c21820d8cb6561b728e51fad4530dff0552a67/cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", size = 9524 }, +] + +[[package]] +name = "certifi" +version = "2024.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/02/a95f2b11e207f68bc64d7aae9666fed2e2b3f307748d5123dffb72a1bbea/certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", size = 164065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/d5/c84e1a17bf61d4df64ca866a1c9a913874b4e9bdc131ec689a0ad013fb36/certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90", size = 162960 }, +] + +[[package]] +name = "cffi" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/bf/82c351342972702867359cfeba5693927efe0a8dd568165490144f554b18/cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76", size = 516073 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2a/9071bf1e20bf9f695643b6c3e0f838f340b95ee29de0d1bb7968772409be/cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb", size = 181841 }, + { url = "https://files.pythonhosted.org/packages/4b/42/60116f10466d692b64aef32ac40fd79b11344ab6ef889ff8e3d047f2fcb2/cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a", size = 178242 }, + { url = "https://files.pythonhosted.org/packages/26/8e/a53f844454595c6e9215e56cda123db3427f8592f2c7b5ef1be782f620d6/cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42", size = 425676 }, + { url = "https://files.pythonhosted.org/packages/60/ac/6402563fb40b64c7ccbea87836d9c9498b374629af3449f3d8ff34df187d/cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d", size = 447842 }, + { url = "https://files.pythonhosted.org/packages/b2/e7/e2ffdb8de59f48f17b196813e9c717fbed2364e39b10bdb3836504e89486/cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2", size = 455224 }, + { url = "https://files.pythonhosted.org/packages/59/55/3e8968e92fe35c1c368959a070a1276c10cae29cdad0fd0daa36c69e237e/cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab", size = 436341 }, + { url = "https://files.pythonhosted.org/packages/7f/df/700aaf009dfbfa04acb1ed487586c03c788c6a312f0361ad5f298c5f5a7d/cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b", size = 445861 }, + { url = "https://files.pythonhosted.org/packages/5a/70/637f070aae533ea11ab77708a820f3935c0edb4fbcef9393b788e6f426a5/cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206", size = 460982 }, + { url = "https://files.pythonhosted.org/packages/f7/1a/7d4740fa1ccc4fcc888963fc3165d69ef1a2c8d42c8911c946703ff5d4a5/cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa", size = 438434 }, + { url = "https://files.pythonhosted.org/packages/d0/d9/c48cc38aaf6f53a8b5d2dbf6fe788410fcbab33b15a69c56c01d2b08f6a2/cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f", size = 461219 }, + { url = "https://files.pythonhosted.org/packages/26/ec/b6a7f660a7f27bd2bb53fe99a2ccafa279088395ec8639b25b8950985b2d/cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc", size = 171406 }, + { url = "https://files.pythonhosted.org/packages/08/42/8c00824787e6f5ec55194f5cd30c4ba4b9d9d5bb0d4d0007b1bb948d4ad4/cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2", size = 180809 }, + { url = "https://files.pythonhosted.org/packages/53/cc/9298fb6235522e00e47d78d6aa7f395332ef4e5f6fe124f9a03aa60600f7/cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720", size = 181912 }, + { url = "https://files.pythonhosted.org/packages/e7/79/dc5334fbe60635d0846c56597a8d2af078a543ff22bc48d36551a0de62c2/cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9", size = 178297 }, + { url = "https://files.pythonhosted.org/packages/39/d7/ef1b6b16b51ccbabaced90ff0d821c6c23567fc4b2e4a445aea25d3ceb92/cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb", size = 444909 }, + { url = "https://files.pythonhosted.org/packages/29/b8/6e3c61885537d985c78ef7dd779b68109ba256263d74a2f615c40f44548d/cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424", size = 468854 }, + { url = "https://files.pythonhosted.org/packages/0b/49/adad1228e19b931e523c2731e6984717d5f9e33a2f9971794ab42815b29b/cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d", size = 476890 }, + { url = "https://files.pythonhosted.org/packages/76/54/c00f075c3e7fd14d9011713bcdb5b4f105ad044c5ad948db7b1a0a7e4e78/cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8", size = 459374 }, + { url = "https://files.pythonhosted.org/packages/f3/b9/f163bb3fa4fbc636ee1f2a6a4598c096cdef279823ddfaa5734e556dd206/cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6", size = 466891 }, + { url = "https://files.pythonhosted.org/packages/31/52/72bbc95f6d06ff2e88a6fa13786be4043e542cb24748e1351aba864cb0a7/cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91", size = 477658 }, + { url = "https://files.pythonhosted.org/packages/67/20/d694811457eeae0c7663fa1a7ca201ce495533b646c1180d4ac25684c69c/cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8", size = 453890 }, + { url = "https://files.pythonhosted.org/packages/dc/79/40cbf5739eb4f694833db5a27ce7f63e30a9b25b4a836c4f25fb7272aacc/cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb", size = 478254 }, + { url = "https://files.pythonhosted.org/packages/e9/eb/2c384c385cca5cae67ca10ac4ef685277680b8c552b99aedecf4ea23ff7e/cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9", size = 171285 }, + { url = "https://files.pythonhosted.org/packages/ca/42/74cb1e0f1b79cb64672f3cb46245b506239c1297a20c0d9c3aeb3929cb0c/cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0", size = 180842 }, + { url = "https://files.pythonhosted.org/packages/1a/1f/7862231350cc959a3138889d2c8d33da7042b22e923457dfd4cd487d772a/cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc", size = 182826 }, + { url = "https://files.pythonhosted.org/packages/8b/8c/26119bf8b79e05a1c39812064e1ee7981e1f8a5372205ba5698ea4dd958d/cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59", size = 178494 }, + { url = "https://files.pythonhosted.org/packages/61/94/4882c47d3ad396d91f0eda6ef16d45be3d752a332663b7361933039ed66a/cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb", size = 454459 }, + { url = "https://files.pythonhosted.org/packages/0f/7c/a6beb119ad515058c5ee1829742d96b25b2b9204ff920746f6e13bf574eb/cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195", size = 478502 }, + { url = "https://files.pythonhosted.org/packages/61/8a/2575cd01a90e1eca96a30aec4b1ac101a6fae06c49d490ac2704fa9bc8ba/cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e", size = 485381 }, + { url = "https://files.pythonhosted.org/packages/cd/66/85899f5a9f152db49646e0c77427173e1b77a1046de0191ab3b0b9a5e6e3/cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828", size = 470907 }, + { url = "https://files.pythonhosted.org/packages/00/13/150924609bf377140abe6e934ce0a57f3fc48f1fd956ec1f578ce97a4624/cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150", size = 479074 }, + { url = "https://files.pythonhosted.org/packages/17/fd/7d73d7110155c036303b0a6462c56250e9bc2f4119d7591d27417329b4d1/cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a", size = 484225 }, + { url = "https://files.pythonhosted.org/packages/fc/83/8353e5c9b01bb46332dac3dfb18e6c597a04ceb085c19c814c2f78a8c0d0/cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885", size = 488388 }, + { url = "https://files.pythonhosted.org/packages/73/0c/f9d5ca9a095b1fc88ef77d1f8b85d11151c374144e4606da33874e17b65b/cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492", size = 172096 }, + { url = "https://files.pythonhosted.org/packages/72/21/8c5d285fe20a6e31d29325f1287bb0e55f7d93630a5a44cafdafb5922495/cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2", size = 181478 }, + { url = "https://files.pythonhosted.org/packages/17/8f/581f2f3c3464d5f7cf87c2f7a5ba9acc6976253e02d73804240964243ec2/cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118", size = 182638 }, + { url = "https://files.pythonhosted.org/packages/8d/1c/c9afa66684b7039f48018eb11b229b659dfb32b7a16b88251bac106dd1ff/cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7", size = 178453 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/1a134d479d3a5a1ff2fabbee551d1d3f1dd70f453e081b5f70d604aae4c0/cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377", size = 454441 }, + { url = "https://files.pythonhosted.org/packages/b1/b4/e1569475d63aad8042b0935dbf62ae2a54d1e9142424e2b0e924d2d4a529/cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb", size = 478543 }, + { url = "https://files.pythonhosted.org/packages/d2/40/a9ad03fbd64309dec5bb70bc803a9a6772602de0ee164d7b9a6ca5a89249/cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555", size = 485463 }, + { url = "https://files.pythonhosted.org/packages/a6/1a/f10be60e006dd9242a24bcc2b1cd55c34c578380100f742d8c610f7a5d26/cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204", size = 470854 }, + { url = "https://files.pythonhosted.org/packages/cc/b3/c035ed21aa3d39432bd749fe331ee90e4bc83ea2dbed1f71c4bc26c41084/cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f", size = 479096 }, + { url = "https://files.pythonhosted.org/packages/00/cb/6f7edde01131de9382c89430b8e253b8c8754d66b63a62059663ceafeab2/cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0", size = 484013 }, + { url = "https://files.pythonhosted.org/packages/b9/83/8e4e8c211ea940210d293e951bf06b1bfb90f2eeee590e9778e99b4a8676/cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4", size = 488119 }, + { url = "https://files.pythonhosted.org/packages/5e/52/3f7cfbc4f444cb4f73ff17b28690d12436dde665f67d68f1e1687908ab6c/cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a", size = 172122 }, + { url = "https://files.pythonhosted.org/packages/94/19/cf5baa07ee0f0e55eab7382459fbddaba0fdb0ba45973dd92556ae0d02db/cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7", size = 181504 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219 }, + { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521 }, + { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383 }, + { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223 }, + { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101 }, + { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699 }, + { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065 }, + { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505 }, + { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425 }, + { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287 }, + { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929 }, + { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605 }, + { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646 }, + { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846 }, + { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343 }, + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, +] + +[[package]] +name = "cheap-repr" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/30/f0e9d5bfe80b8287ea8a9263eb3c71c5fdf44b6f7a781a7c96f83172ccad/cheap_repr-0.5.2.tar.gz", hash = "sha256:001a5cf8adb0305c7ad3152c5f776040ac2a559d97f85770cebcb28c6ca5a30f", size = 20232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/52/fec0262af470a157a557e46be1d52ecdaf1695cefd80bb62bb6a07cc4ea9/cheap_repr-0.5.2-py2.py3-none-any.whl", hash = "sha256:537ec1991bfee885c13c6d473afd110a408e039cde26882e95bf92761556ab6e", size = 12228 }, +] + +[[package]] +name = "chroma-hnswlib" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/09/10d57569e399ce9cbc5eee2134996581c957f63a9addfa6ca657daf006b8/chroma_hnswlib-0.7.6.tar.gz", hash = "sha256:4dce282543039681160259d29fcde6151cc9106c6461e0485f57cdccd83059b7", size = 32256 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/74/b9dde05ea8685d2f8c4681b517e61c7887e974f6272bb24ebc8f2105875b/chroma_hnswlib-0.7.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f35192fbbeadc8c0633f0a69c3d3e9f1a4eab3a46b65458bbcbcabdd9e895c36", size = 195821 }, + { url = "https://files.pythonhosted.org/packages/fd/58/101bfa6bc41bc6cc55fbb5103c75462a7bf882e1704256eb4934df85b6a8/chroma_hnswlib-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f007b608c96362b8f0c8b6b2ac94f67f83fcbabd857c378ae82007ec92f4d82", size = 183854 }, + { url = "https://files.pythonhosted.org/packages/17/ff/95d49bb5ce134f10d6aa08d5f3bec624eaff945f0b17d8c3fce888b9a54a/chroma_hnswlib-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:456fd88fa0d14e6b385358515aef69fc89b3c2191706fd9aee62087b62aad09c", size = 2358774 }, + { url = "https://files.pythonhosted.org/packages/3a/6d/27826180a54df80dbba8a4f338b022ba21c0c8af96fd08ff8510626dee8f/chroma_hnswlib-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dfaae825499c2beaa3b75a12d7ec713b64226df72a5c4097203e3ed532680da", size = 2392739 }, + { url = "https://files.pythonhosted.org/packages/d6/63/ee3e8b7a8f931918755faacf783093b61f32f59042769d9db615999c3de0/chroma_hnswlib-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:2487201982241fb1581be26524145092c95902cb09fc2646ccfbc407de3328ec", size = 150955 }, + { url = "https://files.pythonhosted.org/packages/f5/af/d15fdfed2a204c0f9467ad35084fbac894c755820b203e62f5dcba2d41f1/chroma_hnswlib-0.7.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81181d54a2b1e4727369486a631f977ffc53c5533d26e3d366dda243fb0998ca", size = 196911 }, + { url = "https://files.pythonhosted.org/packages/0d/19/aa6f2139f1ff7ad23a690ebf2a511b2594ab359915d7979f76f3213e46c4/chroma_hnswlib-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4b4ab4e11f1083dd0a11ee4f0e0b183ca9f0f2ed63ededba1935b13ce2b3606f", size = 185000 }, + { url = "https://files.pythonhosted.org/packages/79/b1/1b269c750e985ec7d40b9bbe7d66d0a890e420525187786718e7f6b07913/chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53db45cd9173d95b4b0bdccb4dbff4c54a42b51420599c32267f3abbeb795170", size = 2377289 }, + { url = "https://files.pythonhosted.org/packages/c7/2d/d5663e134436e5933bc63516a20b5edc08b4c1b1588b9680908a5f1afd04/chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c093f07a010b499c00a15bc9376036ee4800d335360570b14f7fe92badcdcf9", size = 2411755 }, + { url = "https://files.pythonhosted.org/packages/3e/79/1bce519cf186112d6d5ce2985392a89528c6e1e9332d680bf752694a4cdf/chroma_hnswlib-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:0540b0ac96e47d0aa39e88ea4714358ae05d64bbe6bf33c52f316c664190a6a3", size = 151888 }, + { url = "https://files.pythonhosted.org/packages/93/ac/782b8d72de1c57b64fdf5cb94711540db99a92768d93d973174c62d45eb8/chroma_hnswlib-0.7.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e87e9b616c281bfbe748d01705817c71211613c3b063021f7ed5e47173556cb7", size = 197804 }, + { url = "https://files.pythonhosted.org/packages/32/4e/fd9ce0764228e9a98f6ff46af05e92804090b5557035968c5b4198bc7af9/chroma_hnswlib-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec5ca25bc7b66d2ecbf14502b5729cde25f70945d22f2aaf523c2d747ea68912", size = 185421 }, + { url = "https://files.pythonhosted.org/packages/d9/3d/b59a8dedebd82545d873235ef2d06f95be244dfece7ee4a1a6044f080b18/chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305ae491de9d5f3c51e8bd52d84fdf2545a4a2bc7af49765cda286b7bb30b1d4", size = 2389672 }, + { url = "https://files.pythonhosted.org/packages/74/1e/80a033ea4466338824974a34f418e7b034a7748bf906f56466f5caa434b0/chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:822ede968d25a2c88823ca078a58f92c9b5c4142e38c7c8b4c48178894a0a3c5", size = 2436986 }, +] + +[[package]] +name = "chromadb" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "build", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "chroma-hnswlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "importlib-resources", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "kubernetes", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "mmh3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, + { name = "onnxruntime", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "orjson", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "overrides", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "posthog", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pypika", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tenacity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "uvicorn", extra = ["standard"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/31/7659067b51ac8b2ec355a100a77fb4d6d823aeb3ff111b6de87dfd18ace1/chromadb-0.5.5.tar.gz", hash = "sha256:84f4bfee320fb4912cbeb4d738f01690891e9894f0ba81f39ee02867102a1c4d", size = 31282293 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/4c/ee62b19a8daeed51e3c88c84b7da6047a74b786e598be3592b67a286d419/chromadb-0.5.5-py3-none-any.whl", hash = "sha256:2a5a4b84cb0fc32b380e193be68cdbadf3d9f77dbbf141649be9886e42910ddd", size = 584312 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "(platform_system == 'Windows' and sys_platform == 'darwin') or (platform_system == 'Windows' and sys_platform == 'linux') or (platform_system == 'Windows' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 }, +] + +[[package]] +name = "comm" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/a8/fb783cb0abe2b5fded9f55e5703015cdf1c9c85b3669087c538dd15a6a86/comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e", size = 6210 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180 }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690 }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127 }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654 }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598 }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732 }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816 }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325 }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418 }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343 }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136 }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796 }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244 }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279 }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859 }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549 }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477 }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134 }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910 }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "(python_full_version == '3.11' and sys_platform == 'darwin') or (python_full_version == '3.11' and sys_platform == 'linux') or (python_full_version == '3.11' and sys_platform == 'win32')" }, +] + +[[package]] +name = "cryptography" +version = "43.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "(platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (platform_python_implementation != 'PyPy' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/ec/9fb9dcf4f91f0e5e76de597256c43eedefd8423aa59be95c70c4c3db426a/cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e", size = 686873 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/46/dcd2eb6840b9452e7fbc52720f3dc54a85eb41e68414733379e8f98e3275/cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74", size = 6239718 }, + { url = "https://files.pythonhosted.org/packages/e8/23/b0713319edff1d8633775b354f8b34a476e4dd5f4cd4b91e488baec3361a/cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895", size = 3808466 }, + { url = "https://files.pythonhosted.org/packages/77/9d/0b98c73cebfd41e4fb0439fe9ce08022e8d059f51caa7afc8934fc1edcd9/cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22", size = 3998060 }, + { url = "https://files.pythonhosted.org/packages/ae/71/e073795d0d1624847f323481f7d84855f699172a632aa37646464b0e1712/cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47", size = 3792596 }, + { url = "https://files.pythonhosted.org/packages/83/25/439a8ddd8058e7f898b7d27c36f94b66c8c8a2d60e1855d725845f4be0bc/cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf", size = 4008355 }, + { url = "https://files.pythonhosted.org/packages/c7/a2/1607f1295eb2c30fcf2c07d7fd0c3772d21dcdb827de2b2730b02df0af51/cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55", size = 3899133 }, + { url = "https://files.pythonhosted.org/packages/5e/64/f41f42ddc9c583737c9df0093affb92c61de7d5b0d299bf644524afe31c1/cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431", size = 4096946 }, + { url = "https://files.pythonhosted.org/packages/cd/cd/d165adcf3e707d6a049d44ade6ca89973549bed0ab3686fa49efdeefea53/cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc", size = 2616826 }, + { url = "https://files.pythonhosted.org/packages/f9/b7/38924229e84c41b0e88d7a5eed8a29d05a44364f85fbb9ddb3984b746fd2/cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778", size = 3078700 }, + { url = "https://files.pythonhosted.org/packages/66/d7/397515233e6a861f921bd0365b162b38e0cc513fcf4f1bdd9cc7bc5a3384/cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66", size = 6242814 }, + { url = "https://files.pythonhosted.org/packages/58/aa/99b2c00a4f54c60d210d6d1759c720ecf28305aa32d6fb1bb1853f415be6/cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5", size = 3809467 }, + { url = "https://files.pythonhosted.org/packages/76/eb/ab783b47b3b9b55371b4361c7ec695144bde1a3343ff2b7a8c1d8fe617bb/cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e", size = 3998617 }, + { url = "https://files.pythonhosted.org/packages/a3/62/62770f34290ebb1b6542bd3f13b3b102875b90aed4804e296f8d2a5ac6d7/cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5", size = 3794003 }, + { url = "https://files.pythonhosted.org/packages/0f/6c/b42660b3075ff543065b2c1c5a3d9bedaadcff8ebce2ee981be2babc2934/cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f", size = 4008774 }, + { url = "https://files.pythonhosted.org/packages/f7/74/028cea86db9315ba3f991e307adabf9f0aa15067011137c38b2fb2aa16eb/cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0", size = 3900098 }, + { url = "https://files.pythonhosted.org/packages/bd/f6/e4387edb55563e2546028ba4c634522fe727693d3cdd9ec0ecacedc75411/cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b", size = 4096867 }, + { url = "https://files.pythonhosted.org/packages/ce/61/55560405e75432bdd9f6cf72fa516cab623b83a3f6d230791bc8fc4afeee/cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf", size = 2616481 }, + { url = "https://files.pythonhosted.org/packages/e6/3d/696e7a0f04555c58a2813d47aaa78cb5ba863c1f453c74a4f45ae772b054/cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709", size = 3081462 }, + { url = "https://files.pythonhosted.org/packages/c6/3a/9c7d864bbcca2df77a601366a6ae3937cd78d0f21ad98441f3424592aea7/cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70", size = 3156882 }, + { url = "https://files.pythonhosted.org/packages/17/cd/d43859b09d726a905d882b6e464ccf02aa2dca2c3e76c44a0c5b169f0144/cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66", size = 3722095 }, + { url = "https://files.pythonhosted.org/packages/2e/ce/c7b912d95f0ded80ad3b50a0a6b31de813c25d9ffadbe1b26bf22d2c4518/cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f", size = 3928750 }, + { url = "https://files.pythonhosted.org/packages/ca/25/7b53082e4c373127c1fb190f70c5aca7bf7a03ac11f67ba15473bc6d9a0e/cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f", size = 3002487 }, +] + +[[package]] +name = "debugpy" +version = "1.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/f9/61c325a10ded8dc3ddc3e7cd2ed58c0b15b2ef4bf8b4bf2930ee98ed59ee/debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0", size = 4612118 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/36/0b423f94097cc86555f9a2c8717511863b2a680c9b44b5419d8ac1ff7bf2/debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7", size = 1711184 }, + { url = "https://files.pythonhosted.org/packages/57/0c/c2ec581541923a4d36cee4fd2419c1211c986849fc61097f87aa81fc6ad3/debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a", size = 2997629 }, + { url = "https://files.pythonhosted.org/packages/a8/46/3072c2cd3b20f435968275d316f6aea7ddbb760386324e6578278bc2eb99/debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed", size = 4764678 }, + { url = "https://files.pythonhosted.org/packages/38/25/e738d6f782beba924c0e10dfde2061152f1ea3608dff0e5a5bfb30c311e9/debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e", size = 4788002 }, + { url = "https://files.pythonhosted.org/packages/ad/72/fd138a10dda16775607316d60dd440fcd23e7560e9276da53c597b5917e9/debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a", size = 1786504 }, + { url = "https://files.pythonhosted.org/packages/e2/0e/d0e6af2d7bbf5ace847e4d3bd41f8f9d4a0764fcd8058f07a1c51618cbf2/debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b", size = 2642077 }, + { url = "https://files.pythonhosted.org/packages/f6/55/2a1dc192894ba9b368cdcce15315761a00f2d4cd7de4402179648840e480/debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408", size = 4702081 }, + { url = "https://files.pythonhosted.org/packages/7f/7f/942b23d64f4896e9f8776cf306dfd00feadc950a38d56398610a079b28b1/debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3", size = 4715571 }, + { url = "https://files.pythonhosted.org/packages/9a/82/7d9e1f75fb23c876ab379008c7cf484a1cfa5ed47ccaac8ba37c75e6814e/debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156", size = 1436398 }, + { url = "https://files.pythonhosted.org/packages/fd/b6/ee71d5e73712daf8307a9e85f5e39301abc8b66d13acd04dfff1702e672e/debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb", size = 1437465 }, + { url = "https://files.pythonhosted.org/packages/6c/d8/8e32bf1f2e0142f7e8a2c354338b493e87f2c44e77e233b3a140fb5efa03/debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7", size = 4581313 }, + { url = "https://files.pythonhosted.org/packages/f7/be/2fbaffecb063de228b2b3b6a1750b0b745e5dc645eddd52be8b329933c0b/debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c", size = 4581209 }, + { url = "https://files.pythonhosted.org/packages/02/49/b595c34d7bc690e8d225a6641618a5c111c7e13db5d9e2b756c15ce8f8c6/debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44", size = 4824118 }, +] + +[[package]] +name = "decorator" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, +] + +[[package]] +name = "deprecated" +version = "1.2.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/14/1e41f504a246fc224d2ac264c227975427a85caf37c3979979edb9b1b232/Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3", size = 2974416 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/8d/778b7d51b981a96554f29136cd59ca7880bf58094338085bcf2a979a0e6a/Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c", size = 9561 }, +] + +[[package]] +name = "distlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + +[[package]] +name = "dnspython" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/c871f55054e403fdfd6b8f65fd6d1c4e147ed100d3e9f9ba1fe695403939/dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc", size = 332727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/a1/8c5287991ddb8d3e4662f71356d9656d91ab3a36618c3dd11b280df0d255/dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50", size = 307696 }, +] + +[[package]] +name = "docstring-parser" +version = "0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533 }, +] + +[[package]] +name = "environs" +version = "9.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/e3/c3c6c76f3dbe3e019e9a451b35bf9f44690026a5bb1232f7b77097b72ff5/environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9", size = 20795 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/5e/f0f217dc393372681bfe05c50f06a212e78d0a3fee907a74ab451ec1dcdb/environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124", size = 12548 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "execnet" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 }, +] + +[[package]] +name = "executing" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/41/85d2d28466fca93737592b7f3cc456d1cfd6bcd401beceeba17e8e792b50/executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147", size = 836501 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/03/6ea8b1b2a5ab40a7a60dc464d3daa7aa546e0a74d74a9f8ff551ea7905db/executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc", size = 24922 }, +] + +[[package]] +name = "fastapi" +version = "0.112.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/09/71a961740a1121d7cc90c99036cc3fbb507bf0c69860d08d4388f842196b/fastapi-0.112.1.tar.gz", hash = "sha256:b2537146f8c23389a7faa8b03d0bd38d4986e6983874557d95eed2acc46448ef", size = 291025 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/b0/0981f9eb5884245ed6678af234f2cbcd40f44570718caddc0360bdb4015d/fastapi-0.112.1-py3-none-any.whl", hash = "sha256:bcbd45817fc2a1cd5da09af66815b84ec0d3d634eb173d1ab468ae3103e183e4", size = 93163 }, +] + +[[package]] +name = "fastjsonschema" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/3f/3ad5e7be13b4b8b55f4477141885ab2364f65d5f6ad5f7a9daffd634d066/fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23", size = 373056 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/ca/086311cdfc017ec964b2436fe0c98c1f4efcb7e4c328956a22456e497655/fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a", size = 23543 }, +] + +[[package]] +name = "filelock" +version = "3.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/dd/49e06f09b6645156550fb9aee9cc1e59aba7efbc972d665a1bd6ae0435d4/filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb", size = 18007 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/f0/48285f0262fe47103a4a45972ed2f9b93e4c80b8fd609fa98da78b2a5706/filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7", size = 16159 }, +] + +[[package]] +name = "flatbuffers" +version = "24.3.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/74/2df95ef84b214d2bee0886d572775a6f38793f5ca6d7630c3239c91104ac/flatbuffers-24.3.25.tar.gz", hash = "sha256:de2ec5b203f21441716617f38443e0a8ebf3d25bf0d9c0bb0ce68fa00ad546a4", size = 22139 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/f0/7e988a019bc54b2dbd0ad4182ef2d53488bb02e58694cd79d61369e85900/flatbuffers-24.3.25-py2.py3-none-any.whl", hash = "sha256:8dbdec58f935f3765e4f7f3cf635ac3a77f83568138d6a2311f524ec96364812", size = 26784 }, +] + +[[package]] +name = "frozenlist" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/3d/2102257e7acad73efc4a0c306ad3953f68c504c16982bbdfee3ad75d8085/frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", size = 37820 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/35/1328c7b0f780d34f8afc1d87ebdc2bb065a123b24766a0b475f0d67da637/frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", size = 94315 }, + { url = "https://files.pythonhosted.org/packages/f4/d6/ca016b0adcf8327714ccef969740688808c86e0287bf3a639ff582f24e82/frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", size = 53805 }, + { url = "https://files.pythonhosted.org/packages/ae/83/bcdaa437a9bd693ba658a0310f8cdccff26bd78e45fccf8e49897904a5cd/frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", size = 52163 }, + { url = "https://files.pythonhosted.org/packages/d4/e9/759043ab7d169b74fe05ebfbfa9ee5c881c303ebc838e308346204309cd0/frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", size = 238595 }, + { url = "https://files.pythonhosted.org/packages/f8/ce/b9de7dc61e753dc318cf0de862181b484178210c5361eae6eaf06792264d/frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", size = 262428 }, + { url = "https://files.pythonhosted.org/packages/36/ce/dc6f29e0352fa34ebe45421960c8e7352ca63b31630a576e8ffb381e9c08/frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", size = 258867 }, + { url = "https://files.pythonhosted.org/packages/51/47/159ac53faf8a11ae5ee8bb9db10327575557504e549cfd76f447b969aa91/frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", size = 229412 }, + { url = "https://files.pythonhosted.org/packages/ec/25/0c87df2e53c0c5d90f7517ca0ff7aca78d050a8ec4d32c4278e8c0e52e51/frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", size = 239539 }, + { url = "https://files.pythonhosted.org/packages/97/94/a1305fa4716726ae0abf3b1069c2d922fcfd442538cb850f1be543f58766/frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", size = 253379 }, + { url = "https://files.pythonhosted.org/packages/53/82/274e19f122e124aee6d113188615f63b0736b4242a875f482a81f91e07e2/frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", size = 245901 }, + { url = "https://files.pythonhosted.org/packages/b8/28/899931015b8cffbe155392fe9ca663f981a17e1adc69589ee0e1e7cdc9a2/frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", size = 263797 }, + { url = "https://files.pythonhosted.org/packages/6e/4f/b8a5a2f10c4a58c52a52a40cf6cf1ffcdbf3a3b64f276f41dab989bf3ab5/frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", size = 264415 }, + { url = "https://files.pythonhosted.org/packages/b0/2c/7be3bdc59dbae444864dbd9cde82790314390ec54636baf6b9ce212627ad/frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", size = 253964 }, + { url = "https://files.pythonhosted.org/packages/2e/ec/4fb5a88f6b9a352aed45ab824dd7ce4801b7bcd379adcb927c17a8f0a1a8/frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", size = 44559 }, + { url = "https://files.pythonhosted.org/packages/61/15/2b5d644d81282f00b61e54f7b00a96f9c40224107282efe4cd9d2bf1433a/frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", size = 50434 }, + { url = "https://files.pythonhosted.org/packages/01/bc/8d33f2d84b9368da83e69e42720cff01c5e199b5a868ba4486189a4d8fa9/frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", size = 97060 }, + { url = "https://files.pythonhosted.org/packages/af/b2/904500d6a162b98a70e510e743e7ea992241b4f9add2c8063bf666ca21df/frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", size = 55347 }, + { url = "https://files.pythonhosted.org/packages/5b/9c/f12b69997d3891ddc0d7895999a00b0c6a67f66f79498c0e30f27876435d/frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", size = 53374 }, + { url = "https://files.pythonhosted.org/packages/ac/6e/e0322317b7c600ba21dec224498c0c5959b2bce3865277a7c0badae340a9/frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", size = 273288 }, + { url = "https://files.pythonhosted.org/packages/a7/76/180ee1b021568dad5b35b7678616c24519af130ed3fa1e0f1ed4014e0f93/frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", size = 284737 }, + { url = "https://files.pythonhosted.org/packages/05/08/40159d706a6ed983c8aca51922a93fc69f3c27909e82c537dd4054032674/frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", size = 280267 }, + { url = "https://files.pythonhosted.org/packages/e0/18/9f09f84934c2b2aa37d539a322267939770362d5495f37783440ca9c1b74/frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", size = 258778 }, + { url = "https://files.pythonhosted.org/packages/b3/c9/0bc5ee7e1f5cc7358ab67da0b7dfe60fbd05c254cea5c6108e7d1ae28c63/frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", size = 272276 }, + { url = "https://files.pythonhosted.org/packages/12/5d/147556b73a53ad4df6da8bbb50715a66ac75c491fdedac3eca8b0b915345/frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", size = 272424 }, + { url = "https://files.pythonhosted.org/packages/83/61/2087bbf24070b66090c0af922685f1d0596c24bb3f3b5223625bdeaf03ca/frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", size = 260881 }, + { url = "https://files.pythonhosted.org/packages/a8/be/a235bc937dd803258a370fe21b5aa2dd3e7bfe0287a186a4bec30c6cccd6/frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", size = 282327 }, + { url = "https://files.pythonhosted.org/packages/5d/e7/b2469e71f082948066b9382c7b908c22552cc705b960363c390d2e23f587/frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74", size = 281502 }, + { url = "https://files.pythonhosted.org/packages/db/1b/6a5b970e55dffc1a7d0bb54f57b184b2a2a2ad0b7bca16a97ca26d73c5b5/frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", size = 272292 }, + { url = "https://files.pythonhosted.org/packages/1a/05/ebad68130e6b6eb9b287dacad08ea357c33849c74550c015b355b75cc714/frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", size = 44446 }, + { url = "https://files.pythonhosted.org/packages/b3/21/c5aaffac47fd305d69df46cfbf118768cdf049a92ee6b0b5cb029d449dcf/frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", size = 50459 }, + { url = "https://files.pythonhosted.org/packages/b4/db/4cf37556a735bcdb2582f2c3fa286aefde2322f92d3141e087b8aeb27177/frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", size = 93937 }, + { url = "https://files.pythonhosted.org/packages/46/03/69eb64642ca8c05f30aa5931d6c55e50b43d0cd13256fdd01510a1f85221/frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", size = 53656 }, + { url = "https://files.pythonhosted.org/packages/3f/ab/c543c13824a615955f57e082c8a5ee122d2d5368e80084f2834e6f4feced/frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", size = 51868 }, + { url = "https://files.pythonhosted.org/packages/a9/b8/438cfd92be2a124da8259b13409224d9b19ef8f5a5b2507174fc7e7ea18f/frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", size = 280652 }, + { url = "https://files.pythonhosted.org/packages/54/72/716a955521b97a25d48315c6c3653f981041ce7a17ff79f701298195bca3/frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", size = 286739 }, + { url = "https://files.pythonhosted.org/packages/65/d8/934c08103637567084568e4d5b4219c1016c60b4d29353b1a5b3587827d6/frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", size = 289447 }, + { url = "https://files.pythonhosted.org/packages/70/bb/d3b98d83ec6ef88f9bd63d77104a305d68a146fd63a683569ea44c3085f6/frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", size = 265466 }, + { url = "https://files.pythonhosted.org/packages/0b/f2/b8158a0f06faefec33f4dff6345a575c18095a44e52d4f10c678c137d0e0/frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", size = 281530 }, + { url = "https://files.pythonhosted.org/packages/ea/a2/20882c251e61be653764038ece62029bfb34bd5b842724fff32a5b7a2894/frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", size = 281295 }, + { url = "https://files.pythonhosted.org/packages/4c/f9/8894c05dc927af2a09663bdf31914d4fb5501653f240a5bbaf1e88cab1d3/frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", size = 268054 }, + { url = "https://files.pythonhosted.org/packages/37/ff/a613e58452b60166507d731812f3be253eb1229808e59980f0405d1eafbf/frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", size = 286904 }, + { url = "https://files.pythonhosted.org/packages/cc/6e/0091d785187f4c2020d5245796d04213f2261ad097e0c1cf35c44317d517/frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", size = 290754 }, + { url = "https://files.pythonhosted.org/packages/a5/c2/e42ad54bae8bcffee22d1e12a8ee6c7717f7d5b5019261a8c861854f4776/frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", size = 282602 }, + { url = "https://files.pythonhosted.org/packages/b6/61/56bad8cb94f0357c4bc134acc30822e90e203b5cb8ff82179947de90c17f/frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", size = 44063 }, + { url = "https://files.pythonhosted.org/packages/3e/dc/96647994a013bc72f3d453abab18340b7f5e222b7b7291e3697ca1fcfbd5/frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", size = 50452 }, + { url = "https://files.pythonhosted.org/packages/83/10/466fe96dae1bff622021ee687f68e5524d6392b0a2f80d05001cd3a451ba/frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", size = 11552 }, +] + +[[package]] +name = "fsspec" +version = "2024.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/b6/eba5024a9889fcfff396db543a34bef0ab9d002278f163129f9f01005960/fsspec-2024.6.1.tar.gz", hash = "sha256:fad7d7e209dd4c1208e3bbfda706620e0da5142bebbd9c384afb95b07e798e49", size = 284584 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/44/73bea497ac69bafde2ee4269292fa3b41f1198f4bb7bbaaabde30ad29d4a/fsspec-2024.6.1-py3-none-any.whl", hash = "sha256:3cb443f8bcd2efb31295a5b9fdb02aee81d8452c80d28f97a6d0959e6cee101e", size = 177561 }, +] + +[[package]] +name = "google-ai-generativelanguage" +version = "0.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "proto-plus", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/38/3d717e70a0020cde7bef8ec998ef3c605f208cc77ba93d22450e09f4d4ee/google-ai-generativelanguage-0.6.6.tar.gz", hash = "sha256:1739f035caeeeca5c28f887405eec8690f3372daf79fecf26454a97a4f1733a8", size = 758303 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/64/bac34c331e8103a0c32df8298823520787e6ff32ea736785c46b1322d62e/google_ai_generativelanguage-0.6.6-py3-none-any.whl", hash = "sha256:59297737931f073d55ce1268dcc6d95111ee62850349d2b6cde942b16a4fca5c", size = 718256 }, +] + +[[package]] +name = "google-api-core" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "googleapis-common-protos", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "proto-plus", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/41/42a127bf163d9bf1f21540a3bf41c69b231b88707d8d753680b8878201a6/google-api-core-2.19.1.tar.gz", hash = "sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd", size = 148925 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/99/daa3541e8ecd7d8b7907b714ba92126097a976b5b3dbabdb5febdcf08554/google_api_core-2.19.1-py3-none-any.whl", hash = "sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125", size = 139384 }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "grpcio-status", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "google-api-python-client" +version = "2.142.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-auth-httplib2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httplib2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "uritemplate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/d2/1dc1b95e9fef7bec1df1e04941d9556b6e384691d2ba520777c68429230f/google_api_python_client-2.142.0.tar.gz", hash = "sha256:a1101ac9e24356557ca22f07ff48b7f61fa5d4b4e7feeef3bda16e5dcb86350e", size = 11680160 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/41/957e29b392728ba94d1df652e2f3ce59022a6d7bb0164575c016ad204a52/google_api_python_client-2.142.0-py2.py3-none-any.whl", hash = "sha256:266799082bb8301f423ec204dffbffb470b502abbf29efd1f83e644d36eb5a8f", size = 12186205 }, +] + +[[package]] +name = "google-auth" +version = "2.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyasn1-modules", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "rsa", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/ae/634dafb151366d91eb848a25846a780dbce4326906ef005d199723fbbca0/google_auth-2.34.0.tar.gz", hash = "sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc", size = 257875 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fb/9af9e3f2996677bdda72734482934fe85a3abde174e5f0783ac2f817ba98/google_auth-2.34.0-py2.py3-none-any.whl", hash = "sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65", size = 200870 }, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httplib2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/be/217a598a818567b28e859ff087f347475c807a5649296fb5a817c58dacef/google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", size = 10842 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/8a/fe34d2f3f9470a27b01c9e76226965863f153d5fbe276f83608562e49c04/google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d", size = 9253 }, +] + +[[package]] +name = "google-cloud-aiplatform" +version = "1.63.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docstring-parser", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-api-core", extra = ["grpc"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-cloud-bigquery", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-cloud-resource-manager", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-cloud-storage", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "proto-plus", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "shapely", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/f8/f3dd468562b2ec9bee62bb52f79dbf32a754f807a29d1e82e2d9eabea314/google-cloud-aiplatform-1.63.0.tar.gz", hash = "sha256:4eb2398bed02a60ad23656b4a442b5d6efa181d11653f8c31f0a5f642c09f913", size = 6250057 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/ac/29c1caeb449a1957b6a3fb09c5e58d09dabf3c9b7d0644f74c16ae286dad/google_cloud_aiplatform-1.63.0-py2.py3-none-any.whl", hash = "sha256:857abe09d1f3f49f62000dbd2302bc653c9a4cdce67ccf65bfd5878fcc81760d", size = 5224293 }, +] + +[[package]] +name = "google-cloud-bigquery" +version = "3.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-cloud-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-resumable-media", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/07/d6f8c55f68d796a6a045cbb3c1783ed1c77ec641acbf9e6ff78b38b127a4/google-cloud-bigquery-3.25.0.tar.gz", hash = "sha256:5b2aff3205a854481117436836ae1403f11f2594e6810a98886afd57eda28509", size = 455186 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/98/2f931388614ea894640f84c1874d72d84d890c093e334a3990e363ff689e/google_cloud_bigquery-3.25.0-py2.py3-none-any.whl", hash = "sha256:7f0c371bc74d2a7fb74dacbc00ac0f90c8c2bec2289b51dd6685a275873b1ce9", size = 239012 }, +] + +[[package]] +name = "google-cloud-core" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/1f/9d1e0ba6919668608570418a9a51e47070ac15aeff64261fb092d8be94c0/google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073", size = 35587 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/0f/2e2061e3fbcb9d535d5da3f58cc8de4947df1786fe6a1355960feb05a681/google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61", size = 29233 }, +] + +[[package]] +name = "google-cloud-resource-manager" +version = "1.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "grpc-google-iam-v1", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "proto-plus", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/32/14d345dee1f290a26bd639da8edbca30958865b7cc7207961e10d2f32282/google_cloud_resource_manager-1.12.5.tar.gz", hash = "sha256:b7af4254401ed4efa3aba3a929cb3ddb803fa6baf91a78485e45583597de5891", size = 394678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/ab/63ab13fb060714b9d1708ca32e0ee41f9ffe42a62e524e7429cde45cfe61/google_cloud_resource_manager-1.12.5-py2.py3-none-any.whl", hash = "sha256:2708a718b45c79464b7b21559c701b5c92e6b0b1ab2146d0a256277a623dc175", size = 341861 }, +] + +[[package]] +name = "google-cloud-storage" +version = "2.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-cloud-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-crc32c", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-resumable-media", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/b7/1554cdeb55d9626a4b8720746cba8119af35527b12e1780164f9ba0f659a/google_cloud_storage-2.18.2.tar.gz", hash = "sha256:aaf7acd70cdad9f274d29332673fcab98708d0e1f4dceb5a5356aaef06af4d99", size = 5532864 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/da/95db7bd4f0bd1644378ac1702c565c0210b004754d925a74f526a710c087/google_cloud_storage-2.18.2-py2.py3-none-any.whl", hash = "sha256:97a4d45c368b7d401ed48c4fdfe86e1e1cb96401c9e199e419d289e2c0370166", size = 130466 }, +] + +[[package]] +name = "google-crc32c" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/a5/4bb58448fffd36ede39684044df93a396c13d1ea3516f585767f9f960352/google-crc32c-1.5.0.tar.gz", hash = "sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7", size = 12689 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/9a/a9bc2603a17d4fda1827d7ab0bb18d1eb5b9df80b9e11955ed9f727ace09/google_crc32c-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13", size = 32090 }, + { url = "https://files.pythonhosted.org/packages/f8/b3/59b49d9c5f15172a35f5560b67048eae02a54927e60c370f3b91743b79f6/google_crc32c-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346", size = 30073 }, + { url = "https://files.pythonhosted.org/packages/34/c6/27be6fc6cbfebff08f63c2017fe885932b3387b45a0013b772f9beac7c01/google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65", size = 32681 }, + { url = "https://files.pythonhosted.org/packages/b7/53/0170614ccaf34ac602c877929998dbca4923f0c401f0bea6f0d5a38a3e57/google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b", size = 30022 }, + { url = "https://files.pythonhosted.org/packages/a9/d0/04f2846f0af1c683eb3b664c9de9543da1e66a791397456a65073b6054a2/google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02", size = 32288 }, + { url = "https://files.pythonhosted.org/packages/f9/c2/eb43b40e799a9f85a43b358f2b4a2b4d60f8c22a7867aca5d6eb1b88b565/google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4", size = 569180 }, + { url = "https://files.pythonhosted.org/packages/b9/14/e9ba87ccc931323d79574924bf582633cc467e196bb63a49bc5a75c1dd58/google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e", size = 35236 }, + { url = "https://files.pythonhosted.org/packages/3f/a7/d9709429d1eae1c4907b3b9aab866de26acc5ca42c4237d216acf0b7033a/google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c", size = 581671 }, + { url = "https://files.pythonhosted.org/packages/5a/6b/882314bb535e44bb5578d60859497c5b9d82103960f3b6ecdaf42d3fab34/google_crc32c-1.5.0-cp310-cp310-win32.whl", hash = "sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee", size = 23927 }, + { url = "https://files.pythonhosted.org/packages/1f/6b/fcd4744a020fa7bfb1a451b0be22b3e5a4cb28bafaaf01467d2e9402b96b/google_crc32c-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289", size = 27318 }, + { url = "https://files.pythonhosted.org/packages/69/0f/7f89ae2b22c55273110a44a7ed55a2948bc213fb58983093fbefcdfd2d13/google_crc32c-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273", size = 32093 }, + { url = "https://files.pythonhosted.org/packages/41/3f/8141b03ad127fc569c3efda2bfe31d64665e02e2b8b7fbf7b25ea914c27a/google_crc32c-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298", size = 30071 }, + { url = "https://files.pythonhosted.org/packages/fc/76/3ef124b893aa280e45e95d2346160f1d1d5c0ffc89d3f6e446c83116fb91/google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57", size = 32702 }, + { url = "https://files.pythonhosted.org/packages/fd/71/299a368347aeab3c89896cdfb67703161becbf5afbc1748a1850094828dc/google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438", size = 30041 }, + { url = "https://files.pythonhosted.org/packages/72/92/2a2fa23db7d0b0382accbdf09768c28f7c07fc8c354cdcf2f44a47f4314e/google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906", size = 32317 }, + { url = "https://files.pythonhosted.org/packages/0f/99/e7e288f1b50baf4964ff39fa79d9259d004ae44db35c8280ff4ffea362d5/google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183", size = 570024 }, + { url = "https://files.pythonhosted.org/packages/88/ea/e53fbafcd0be2349d9c2a6912646cdfc47cfc5c22be9a8a5156552e33821/google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd", size = 36047 }, + { url = "https://files.pythonhosted.org/packages/02/94/d2ea867760d5a27b3e9eb40ff31faf7f03f949e51d4e3b3ae24f759b5963/google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c", size = 582541 }, + { url = "https://files.pythonhosted.org/packages/b7/09/768d2ca0c10a0765f83c6d06a5e40f3083cb75b8e7718ac22edff997aefc/google_crc32c-1.5.0-cp311-cp311-win32.whl", hash = "sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709", size = 23928 }, + { url = "https://files.pythonhosted.org/packages/ce/8b/02bf4765c487901c8660290ade9929d65a6151c367ba32e75d136ef2d0eb/google_crc32c-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968", size = 27318 }, +] + +[[package]] +name = "google-generativeai" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-ai-generativelanguage", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-api-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-api-python-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/92/766c19a6ccdc3e5272ecb831e131672e290d1ca4ec5cd6a4040a78454707/google_generativeai-0.7.2-py3-none-any.whl", hash = "sha256:3117d1ebc92ee77710d4bc25ab4763492fddce9b6332eb25d124cf5d8b78b339", size = 164212 }, +] + +[[package]] +name = "google-resumable-media" +version = "2.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/5a/0efdc02665dca14e0837b62c8a1a93132c264bd02054a15abb2218afe0ae/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0", size = 2163099 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/35/b8d3baf8c46695858cb9d8835a53baa1eeb9906ddaf2f728a5f5b640fd1e/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", size = 81251 }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.63.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/41723ae380fa9c561cbe7b61c4eef9091d5fe95486465ccfc84845877331/googleapis-common-protos-1.63.2.tar.gz", hash = "sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87", size = 112890 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/48/87422ff1bddcae677fb6f58c97f5cfc613304a5e8ce2c3662760199c0a84/googleapis_common_protos-1.63.2-py2.py3-none-any.whl", hash = "sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945", size = 220001 }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/41/f01bf46bac4034b4750575fe87c80c5a43a8912847307955e22f2125b60c/grpc-google-iam-v1-0.13.1.tar.gz", hash = "sha256:3ff4b2fd9d990965e410965253c0da6f66205d5a8291c4c31c6ebecca18a9001", size = 17664 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/7d/da3875b7728bc700eeb28b513754ce237c04ac7cbf8559d76b0464ee01cb/grpc_google_iam_v1-0.13.1-py2.py3-none-any.whl", hash = "sha256:c3e86151a981811f30d5e7330f271cee53e73bb87755e88cc3b6f0c7b5fe374e", size = 24866 }, +] + +[[package]] +name = "grpcio" +version = "1.60.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/38/c615b5c2be690fb31871f294cc08a96e598b085b8d07c5967a5018e0b90c/grpcio-1.60.0.tar.gz", hash = "sha256:2199165a1affb666aa24adf0c97436686d0a61bc5fc113c037701fb7c7fceb96", size = 24766390 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/88/f5a1e1441180a57a409ccb26a7db20ec5686973698f8b6119412dedb7368/grpcio-1.60.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:d020cfa595d1f8f5c6b343530cd3ca16ae5aefdd1e832b777f9f0eb105f5b139", size = 99933161 }, + { url = "https://files.pythonhosted.org/packages/e6/54/58c17c86f3410fdfc843dddbbafa2d71a61f96b7a3832b6ad299d4359833/grpcio-1.60.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b98f43fcdb16172dec5f4b49f2fece4b16a99fd284d81c6bbac1b3b69fcbe0ff", size = 9627835 }, + { url = "https://files.pythonhosted.org/packages/d8/86/b082d195d1c0ac885a9bec7ced2e6811856bef745efef4c604fe97e72614/grpcio-1.60.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:20e7a4f7ded59097c84059d28230907cd97130fa74f4a8bfd1d8e5ba18c81491", size = 5115762 }, + { url = "https://files.pythonhosted.org/packages/37/11/a360319387e90b911dc0458eacbd90c615660e4ed415cb0a81eb18685c10/grpcio-1.60.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452ca5b4afed30e7274445dd9b441a35ece656ec1600b77fff8c216fdf07df43", size = 5621840 }, + { url = "https://files.pythonhosted.org/packages/ed/bd/4dbe2ae13ffba7eef2a3bd2dcebbc2255da18d1a972a89952d55e8ad3d4b/grpcio-1.60.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43e636dc2ce9ece583b3e2ca41df5c983f4302eabc6d5f9cd04f0562ee8ec1ae", size = 5356182 }, + { url = "https://files.pythonhosted.org/packages/09/43/98b53f2fccc2389adfc60720a514d029a728d028641a4289788aa22c3981/grpcio-1.60.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e306b97966369b889985a562ede9d99180def39ad42c8014628dd3cc343f508", size = 5904203 }, + { url = "https://files.pythonhosted.org/packages/73/3c/d7bd58d4784b04813d21ac8c9bd99d36c9c4dd911c3fb5d683b59ccbc7af/grpcio-1.60.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f897c3b127532e6befdcf961c415c97f320d45614daf84deba0a54e64ea2457b", size = 5605970 }, + { url = "https://files.pythonhosted.org/packages/7e/f0/01938fee8517de7c41fb8dbc84a8aafd309ae4c4e3ae1e652a66b4e76af9/grpcio-1.60.0-cp310-cp310-win32.whl", hash = "sha256:b87efe4a380887425bb15f220079aa8336276398dc33fce38c64d278164f963d", size = 3131685 }, + { url = "https://files.pythonhosted.org/packages/9e/7f/adf4bc4c2d54e496eca16a856ddfdace57e7ace01ac9ffcd2abf888c47e6/grpcio-1.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:a9c7b71211f066908e518a2ef7a5e211670761651039f0d6a80d8d40054047df", size = 3702013 }, + { url = "https://files.pythonhosted.org/packages/28/98/1c5218ed23e4c5ba58058e52d39206871feba4e1d17bddfb4da48e441101/grpcio-1.60.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:fb464479934778d7cc5baf463d959d361954d6533ad34c3a4f1d267e86ee25fd", size = 100133810 }, + { url = "https://files.pythonhosted.org/packages/5c/45/8708497bc482cc7bf3779df9cf00c8e9efe1df5cd29b77e3eb060c141f84/grpcio-1.60.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:4b44d7e39964e808b071714666a812049765b26b3ea48c4434a3b317bac82f14", size = 9650227 }, + { url = "https://files.pythonhosted.org/packages/56/0a/5320d3ba32ac3ba98a18606bedcec89b571c40d31f62302196ceac835e91/grpcio-1.60.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:90bdd76b3f04bdb21de5398b8a7c629676c81dfac290f5f19883857e9371d28c", size = 5120380 }, + { url = "https://files.pythonhosted.org/packages/3e/7c/fd25f2e5247383d994b90a2d9522090bbc9e609547504613ea351928d2c7/grpcio-1.60.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91229d7203f1ef0ab420c9b53fe2ca5c1fbeb34f69b3bc1b5089466237a4a134", size = 5625772 }, + { url = "https://files.pythonhosted.org/packages/de/01/a8d9bcc59526f22b8fef29c234cc63434f05dae1154d979222c02b31a557/grpcio-1.60.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b36a2c6d4920ba88fa98075fdd58ff94ebeb8acc1215ae07d01a418af4c0253", size = 5354817 }, + { url = "https://files.pythonhosted.org/packages/13/4c/9d6ffdfcaa22f380dfd2b459b9761249ad61cfde65a927d832b3800d139b/grpcio-1.60.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:297eef542156d6b15174a1231c2493ea9ea54af8d016b8ca7d5d9cc65cfcc444", size = 5908054 }, + { url = "https://files.pythonhosted.org/packages/7c/1e/f7b9c72ae6560d92027aac51f90a827051c3766ea961bc2d1b78c3657437/grpcio-1.60.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:87c9224acba0ad8bacddf427a1c2772e17ce50b3042a789547af27099c5f751d", size = 5604825 }, + { url = "https://files.pythonhosted.org/packages/2d/2f/fd5ff4cf5a307dae7ba6b72962c904bcb26f08ea3df139019fdf5c40b298/grpcio-1.60.0-cp311-cp311-win32.whl", hash = "sha256:95ae3e8e2c1b9bf671817f86f155c5da7d49a2289c5cf27a319458c3e025c320", size = 3127779 }, + { url = "https://files.pythonhosted.org/packages/6a/b9/f94bea4c6f0e322a239f7ba66ba3b0ce766d1c6a2d50055f7c8acf0fba38/grpcio-1.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:467a7d31554892eed2aa6c2d47ded1079fc40ea0b9601d9f79204afa8902274b", size = 3699392 }, + { url = "https://files.pythonhosted.org/packages/61/f9/e3c4b4a879096fe608d75e2a5b4b3790baa91137c5d5da259f98128d2f86/grpcio-1.60.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:a7152fa6e597c20cb97923407cf0934e14224af42c2b8d915f48bc3ad2d9ac18", size = 100617931 }, + { url = "https://files.pythonhosted.org/packages/dd/7d/5005318879231a879be0d33c588400941aee08ea8b5b45d3a9061d6bf0fb/grpcio-1.60.0-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:7db16dd4ea1b05ada504f08d0dca1cd9b926bed3770f50e715d087c6f00ad748", size = 9612074 }, + { url = "https://files.pythonhosted.org/packages/f1/b5/93ea03649a8315fe00b11871bb7fa807e1ee22d14f5c4de2fbc288c6cd37/grpcio-1.60.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:b0571a5aef36ba9177e262dc88a9240c866d903a62799e44fd4aae3f9a2ec17e", size = 5061795 }, + { url = "https://files.pythonhosted.org/packages/c9/b8/91b5b56f7812372bd51342126f0184a1a604723b0f58466ac20c2dcef63a/grpcio-1.60.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fd9584bf1bccdfff1512719316efa77be235469e1e3295dce64538c4773840b", size = 5566289 }, + { url = "https://files.pythonhosted.org/packages/d7/2e/3337baee24c902d9e82f1eac00bc9dca106934763c4cd0faf819ef01b96b/grpcio-1.60.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6a478581b1a1a8fdf3318ecb5f4d0cda41cacdffe2b527c23707c9c1b8fdb55", size = 5300194 }, + { url = "https://files.pythonhosted.org/packages/8c/ea/b1229842677f5b712f72760d1633cf36813ec121c986454d6eba6de22093/grpcio-1.60.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:77c8a317f0fd5a0a2be8ed5cbe5341537d5c00bb79b3bb27ba7c5378ba77dbca", size = 5852832 }, + { url = "https://files.pythonhosted.org/packages/05/dc/c641498f09246a61ebe7a721888edf772e2ecdfd524e25ac61e27352d9d3/grpcio-1.60.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1c30bb23a41df95109db130a6cc1b974844300ae2e5d68dd4947aacba5985aa5", size = 5555224 }, + { url = "https://files.pythonhosted.org/packages/4d/a3/0f07d9fdb9dddce85bbcc671bf49ed3c73301dfc3108ed4ab3212d55ef13/grpcio-1.60.0-cp312-cp312-win32.whl", hash = "sha256:2aef56e85901c2397bd557c5ba514f84de1f0ae5dd132f5d5fed042858115951", size = 3111209 }, + { url = "https://files.pythonhosted.org/packages/73/99/a7b768c6a9873b6f450476bfa389eeef877f152aeb443bec2bd91d9fb5a2/grpcio-1.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:e381fe0c2aa6c03b056ad8f52f8efca7be29fb4d9ae2f8873520843b6039612a", size = 3691893 }, +] + +[[package]] +name = "grpcio-health-checking" +version = "1.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/24/d58e2855bedfe4150718e03babcadb68d3dd69803cfdb45d27195bafcd20/grpcio-health-checking-1.60.0.tar.gz", hash = "sha256:478b5300778120fed9f6d134d72b157a59f9c06689789218cbff47fafca2f119", size = 16324 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/d7/98a877cabb6e0e1dd514f16d77b45036a8add1b0457a6e92c695baed9ded/grpcio_health_checking-1.60.0-py3-none-any.whl", hash = "sha256:13caf28bc93795bd6bdb580b21832ebdd1aa3f5b648ea47ed17362d85bed96d3", size = 18545 }, +] + +[[package]] +name = "grpcio-status" +version = "1.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/38/0cd65d29f8fe0b5efaef60a0664885b5457a566b1a531d3e6b76a8bb0f21/grpcio-status-1.60.0.tar.gz", hash = "sha256:f10e0b6db3adc0fdc244b71962814ee982996ef06186446b5695b9fa635aa1ab", size = 13546 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/bd/f46d6511088f314cfedc880721fd32d387b8513b22da01cf4771d7439a2b/grpcio_status-1.60.0-py3-none-any.whl", hash = "sha256:7d383fa36e59c1e61d380d91350badd4d12ac56e4de2c2b831b050362c3c572e", size = 14448 }, +] + +[[package]] +name = "grpcio-tools" +version = "1.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/8f/1861529938e4a27f8d9b736a4ba58846ab1ccf63b6d7610a86a0329ffc46/grpcio-tools-1.60.0.tar.gz", hash = "sha256:ed30499340228d733ff69fcf4a66590ed7921f94eb5a2bf692258b1280b9dac7", size = 4611505 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/fe/3552c6e900d86fa21ec7b18ce93e912fbf8d79ee5ea4b41a0cb5bbf75b1a/grpcio_tools-1.60.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:6807b7a3f3e6e594566100bd7fe04a2c42ce6d5792652677f1aaf5aa5adaef3d", size = 63932019 }, + { url = "https://files.pythonhosted.org/packages/c3/7f/44bb9eba5797e1cfebaa28bf9cb61f0b337d407953ccc377a66e0777501b/grpcio_tools-1.60.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:857c5351e9dc33a019700e171163f94fcc7e3ae0f6d2b026b10fda1e3c008ef1", size = 5120499 }, + { url = "https://files.pythonhosted.org/packages/1e/1f/670010f510a0f28f912e5080ebfa02bc8c809e6aaef8394ebfbe12593de9/grpcio_tools-1.60.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:ec0e401e9a43d927d216d5169b03c61163fb52b665c5af2fed851357b15aef88", size = 2707837 }, + { url = "https://files.pythonhosted.org/packages/fd/c1/bb2198f3480d3acb7683708e729732b7f12ccbc4db0cb70b59a257928f88/grpcio_tools-1.60.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e68dc4474f30cad11a965f0eb5d37720a032b4720afa0ec19dbcea2de73b5aae", size = 3060242 }, + { url = "https://files.pythonhosted.org/packages/3c/7d/00a156dba65c9965e6e94988ab518c4ea88f95e1b70c2b61b34dd65124b5/grpcio_tools-1.60.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbf0ed772d2ae7e8e5d7281fcc00123923ab130b94f7a843eee9af405918f924", size = 2795167 }, + { url = "https://files.pythonhosted.org/packages/6d/6a/a4980794503537474ca27d13ffedc200610a631c8cf047c0b311d19fb015/grpcio_tools-1.60.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c771b19dce2bfe06899247168c077d7ab4e273f6655d8174834f9a6034415096", size = 3673788 }, + { url = "https://files.pythonhosted.org/packages/fc/3e/809c98c5423ac8374a55aa90e9d222a5da542aa13fd18b8181cfd01bb6cd/grpcio_tools-1.60.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5614cf0960456d21d8a0f4902e3e5e3bcacc4e400bf22f196e5dd8aabb978b7", size = 3281881 }, + { url = "https://files.pythonhosted.org/packages/f1/ff/282a802232e8de69221cd41c5045950d4253fe7d5d2e24574e5637c8184c/grpcio_tools-1.60.0-cp310-cp310-win32.whl", hash = "sha256:87cf439178f3eb45c1a889b2e4a17cbb4c450230d92c18d9c57e11271e239c55", size = 921927 }, + { url = "https://files.pythonhosted.org/packages/fe/b7/79ec64ad16b9159458ab29b485511a7dc7cf9c9f1cc9ba6e1bbc91f61646/grpcio_tools-1.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:687f576d7ff6ce483bc9a196d1ceac45144e8733b953620a026daed8e450bc38", size = 1068077 }, + { url = "https://files.pythonhosted.org/packages/7b/3c/233eb8db31c08f29ea84f690f6f25e2fd02477c1986ba13096e24b828878/grpcio_tools-1.60.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2a8a758701f3ac07ed85f5a4284c6a9ddefcab7913a8e552497f919349e72438", size = 63937622 }, + { url = "https://files.pythonhosted.org/packages/7e/7f/47d8b35172b7f94b93c8ea4b7229f40a19d6da13bca976b6e85bbe7ef010/grpcio_tools-1.60.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:7c1cde49631732356cb916ee1710507967f19913565ed5f9991e6c9cb37e3887", size = 5147769 }, + { url = "https://files.pythonhosted.org/packages/c9/4d/b601d7bc72f453a1e9f9962be5a4ee81b5cae70b08bac5339e876cec355a/grpcio_tools-1.60.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:d941749bd8dc3f8be58fe37183143412a27bec3df8482d5abd6b4ec3f1ac2924", size = 2708357 }, + { url = "https://files.pythonhosted.org/packages/90/ec/bc2902d5a753b59920082ba4e6b9b7adb8b3c076c327639494a32b51a953/grpcio_tools-1.60.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ee35234f1da8fba7ddbc544856ff588243f1128ea778d7a1da3039be829a134", size = 3060323 }, + { url = "https://files.pythonhosted.org/packages/29/0f/fdfa88aff42abc0caa29f74cfa47e77ea1d6385c073c082fef582ac0ec9f/grpcio_tools-1.60.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f7a5094adb49e85db13ea3df5d99a976c2bdfd83b0ba26af20ebb742ac6786", size = 2795700 }, + { url = "https://files.pythonhosted.org/packages/33/20/36584dff9564d1237f8fb90dc151d76dac8d00ac86dbd53bc99cc25767e1/grpcio_tools-1.60.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:24c4ead4a03037beaeb8ef2c90d13d70101e35c9fae057337ed1a9144ef10b53", size = 3674639 }, + { url = "https://files.pythonhosted.org/packages/da/99/c08d1160f08089e7b422e6b97351cf17843a5b4bebc8ac5d98c8af8db7da/grpcio_tools-1.60.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:811abb9c4fb6679e0058dfa123fb065d97b158b71959c0e048e7972bbb82ba0f", size = 3282610 }, + { url = "https://files.pythonhosted.org/packages/56/a7/378ccd3e8ec1e57fa62f9d60e7da6afece565b105f86d4393a8eabbccba4/grpcio_tools-1.60.0-cp311-cp311-win32.whl", hash = "sha256:bd2a17b0193fbe4793c215d63ce1e01ae00a8183d81d7c04e77e1dfafc4b2b8a", size = 922153 }, + { url = "https://files.pythonhosted.org/packages/61/19/528588f68effc32be1f5803f11d5dd66833e53a99384c0e1e4c53b78d42b/grpcio_tools-1.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:b22b1299b666eebd5752ba7719da536075eae3053abcf2898b65f763c314d9da", size = 1067992 }, + { url = "https://files.pythonhosted.org/packages/50/09/16b77ffe4f0e3f03c98407a82485e8c9c15bc433334965fbd31a9dfa127b/grpcio_tools-1.60.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:74025fdd6d1cb7ba4b5d087995339e9a09f0c16cf15dfe56368b23e41ffeaf7a", size = 63964335 }, + { url = "https://files.pythonhosted.org/packages/21/2f/3b4f50a810bc9892ac094b29c5c66e575a56813cb4e73fc9a4c7d2dccd3c/grpcio_tools-1.60.0-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:5a907a4f1ffba86501b2cdb8682346249ea032b922fc69a92f082ba045cca548", size = 5147864 }, + { url = "https://files.pythonhosted.org/packages/7c/28/f3baa87c8e53b7694761ea69d5d9c3f635b54ff7c09761e3593ca59344b3/grpcio_tools-1.60.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:1fbb9554466d560472f07d906bfc8dcaf52f365c2a407015185993e30372a886", size = 2709526 }, + { url = "https://files.pythonhosted.org/packages/9d/07/87e5c0c70dfa0aefc130a6e9116a54866d72449706b35902fbbf3f57f37e/grpcio_tools-1.60.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f10ef47460ce3c6fd400f05fe757b90df63486c9b84d1ecad42dcc5f80c8ac14", size = 3061068 }, + { url = "https://files.pythonhosted.org/packages/b4/cb/e8ad1dd2caac2de9e3a0e6627024ffca3bf30c9911e691f88b7dca4e5097/grpcio_tools-1.60.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:321b18f42a70813545e416ddcb8bf20defa407a8114906711c9710a69596ceda", size = 2797033 }, + { url = "https://files.pythonhosted.org/packages/ba/1d/8c8048c00c194aa8d5648aba853df4076be6d70e9a00a1f25d4830b6dee8/grpcio_tools-1.60.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:081336d8258f1a56542aa8a7a5dec99a2b38d902e19fbdd744594783301b0210", size = 3674987 }, + { url = "https://files.pythonhosted.org/packages/a4/48/dae5740b16b9fdd937fa3bf4f29b6c95b8e0d2dc06a5e82a59e2aa67f07b/grpcio_tools-1.60.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:addc9b23d6ff729d9f83d4a2846292d4c84f5eb2ec38f08489a6a0d66ac2b91e", size = 3283144 }, + { url = "https://files.pythonhosted.org/packages/9b/b6/87d859bf481a2e5629c1ea14a741faa90d533b756af0c514cbff06b00c71/grpcio_tools-1.60.0-cp312-cp312-win32.whl", hash = "sha256:e87cabac7969bdde309575edc2456357667a1b28262b2c1f12580ef48315b19d", size = 922614 }, + { url = "https://files.pythonhosted.org/packages/a8/0a/d6fea138f949f307f2e6958fbf6a3cd94a2d6a51ba3a6333a36b02e24459/grpcio_tools-1.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:e70d867c120d9849093b0ac24d861e378bc88af2552e743d83b9f642d2caa7c2", size = 1068418 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "h2" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "hyperframe", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/32/fec683ddd10629ea4ea46d206752a95a2d8a48c22521edd70b142488efe1/h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb", size = 2145593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e5/db6d438da759efbb488c4f3fbdab7764492ff3c3f953132efa6b9f0e9e53/h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d", size = 57488 }, +] + +[[package]] +name = "hiredis" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/80/740fb0dfa7a42416ce8376490f41dcdb1e5deed9c3739dfe4200fad865a9/hiredis-3.0.0.tar.gz", hash = "sha256:fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441", size = 87581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/cc/41521d38c77f404c31e08a0118f369f37dc6a9e19cf315dbbc8b0b8afaba/hiredis-3.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:4b182791c41c5eb1d9ed736f0ff81694b06937ca14b0d4dadde5dadba7ff6dae", size = 81483 }, + { url = "https://files.pythonhosted.org/packages/99/35/0138fe68b0da01ea91ad67910577905b7f4a34b5c11e2f665d44067c52df/hiredis-3.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:13c275b483a052dd645eb2cb60d6380f1f5215e4c22d6207e17b86be6dd87ffa", size = 44763 }, + { url = "https://files.pythonhosted.org/packages/45/53/64fa74d43c17a406c2dc3cb4f1a3729ac00c5451f31f5940ca577b24afa9/hiredis-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1018cc7f12824506f165027eabb302735b49e63af73eb4d5450c66c88f47026", size = 42452 }, + { url = "https://files.pythonhosted.org/packages/af/b8/40c58b7db70e3850adeac85d5fca67e2fce6bf15c2705ca6af9c8bb32b5d/hiredis-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83a29cc7b21b746cb6a480189e49f49b2072812c445e66a9e38d2004d496b81c", size = 165712 }, + { url = "https://files.pythonhosted.org/packages/ff/8e/7afd36941d58cb0a7f0142ba3a043a5b3743dfff60596e98b355fb048113/hiredis-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e241fab6332e8fb5f14af00a4a9c6aefa22f19a336c069b7ddbf28ef8341e8d6", size = 176842 }, + { url = "https://files.pythonhosted.org/packages/ff/39/482970200e65cdcea037a595083e145fc089b8368312f6f2b0d3c5a7c266/hiredis-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fb8de899f0145d6c4d5d4bd0ee88a78eb980a7ffabd51e9889251b8f58f1785", size = 166127 }, + { url = "https://files.pythonhosted.org/packages/3a/2b/655e8b4b54ff28c88e2ac536d4aa24c9119c6160169c043351a91db69bca/hiredis-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b23291951959141173eec10f8573538e9349fa27f47a0c34323d1970bf891ee5", size = 165983 }, + { url = "https://files.pythonhosted.org/packages/81/d8/bc917412f95da9904a83a04263aa2760051c118d0199eac7250623bfcf17/hiredis-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e421ac9e4b5efc11705a0d5149e641d4defdc07077f748667f359e60dc904420", size = 162249 }, + { url = "https://files.pythonhosted.org/packages/77/93/d6585264bb50f9f79537429fa90f4a2a5c29fd5e70d57dec7705ff161a7c/hiredis-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77c8006c12154c37691b24ff293c077300c22944018c3ff70094a33e10c1d795", size = 160013 }, + { url = "https://files.pythonhosted.org/packages/48/a5/302868a60e963c1b768bd5622f125f5b38a3ea084bdcb374c9251dcc7c02/hiredis-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:41afc0d3c18b59eb50970479a9c0e5544fb4b95e3a79cf2fbaece6ddefb926fe", size = 159315 }, + { url = "https://files.pythonhosted.org/packages/82/77/c02d516ab8f31d85378916055dbf980ef7ca431d93ba1f7ac11ac4304863/hiredis-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:04ccae6dcd9647eae6025425ab64edb4d79fde8b9e6e115ebfabc6830170e3b2", size = 171008 }, + { url = "https://files.pythonhosted.org/packages/e1/28/c080805a340b418b1d022fa58465e365636c0ed201837e0fe70cc7beb0d3/hiredis-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fe91d62b0594db5ea7d23fc2192182b1a7b6973f628a9b8b2e0a42a2be721ac6", size = 163290 }, + { url = "https://files.pythonhosted.org/packages/6a/f9/caacca69987de597487360565e34dfd191ab23ce147144c13df1f2db6c8d/hiredis-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99516d99316062824a24d145d694f5b0d030c80da693ea6f8c4ecf71a251d8bb", size = 161037 }, + { url = "https://files.pythonhosted.org/packages/88/3a/0d560473ca21facc1de5ba538f655aeae71303afd71f2a5e35fadee0c698/hiredis-3.0.0-cp310-cp310-win32.whl", hash = "sha256:562eaf820de045eb487afaa37e6293fe7eceb5b25e158b5a1974b7e40bf04543", size = 20034 }, + { url = "https://files.pythonhosted.org/packages/9c/af/23c2ce80faffb0ceb1775fe4581829c229400d6faacc0e2567ae179e8bc2/hiredis-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1c81c89ed765198da27412aa21478f30d54ef69bf5e4480089d9c3f77b8f882", size = 21863 }, + { url = "https://files.pythonhosted.org/packages/42/3e/502e2ce2487673214fbb4cc733b1a279bc71309a689803d9ba8ad6f2fa8f/hiredis-3.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:4664dedcd5933364756d7251a7ea86d60246ccf73a2e00912872dacbfcef8978", size = 81442 }, + { url = "https://files.pythonhosted.org/packages/18/0b/171d85b2ee0ac51f94e993a323beffdb6b273b838a4f86d9abaaca22e2f7/hiredis-3.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:47de0bbccf4c8a9f99d82d225f7672b9dd690d8fd872007b933ef51a302c9fa6", size = 44742 }, + { url = "https://files.pythonhosted.org/packages/6a/67/466e0b16caff07bc8df8f3ff8b0b279f81066e0fb6a201b0ec66288fe5a4/hiredis-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e43679eca508ba8240d016d8cca9d27342d70184773c15bea78a23c87a1922f1", size = 42424 }, + { url = "https://files.pythonhosted.org/packages/01/50/e1f21e1cc9426bdf62e9ca8106294fbc3e5d27ddbae2e85e47fb9f251d1b/hiredis-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13c345e7278c210317e77e1934b27b61394fee0dec2e8bd47e71570900f75823", size = 166331 }, + { url = "https://files.pythonhosted.org/packages/98/40/8d8e4e15045ce066570f82f49604c6273b186eda1e5c9b93b450dd25d7b9/hiredis-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00018f22f38530768b73ea86c11f47e8d4df65facd4e562bd78773bd1baef35e", size = 177350 }, + { url = "https://files.pythonhosted.org/packages/5d/9c/f7b6d7afa2bd9c6671de853069222d9d874725e387100dfb0f1a22aab122/hiredis-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ea3a86405baa8eb0d3639ced6926ad03e07113de54cb00fd7510cb0db76a89d", size = 166794 }, + { url = "https://files.pythonhosted.org/packages/53/0c/1076e0c045412081ec44dc81969373cda15c093a0692e10f2941e154e583/hiredis-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c073848d2b1d5561f3903879ccf4e1a70c9b1e7566c7bdcc98d082fa3e7f0a1d", size = 166566 }, + { url = "https://files.pythonhosted.org/packages/05/69/e081b023f86b0128fcf9f76c8ed5a5f9426895ad86de234b0332c18a57b8/hiredis-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a8dffb5f5b3415a4669d25de48b617fd9d44b0bccfc4c2ab24b06406ecc9ecb", size = 162561 }, + { url = "https://files.pythonhosted.org/packages/96/e0/7f957fb2158c6f6800b6faa2f90bedcc485ca038a2d42166761d400683a3/hiredis-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22c17c96143c2a62dfd61b13803bc5de2ac526b8768d2141c018b965d0333b66", size = 160472 }, + { url = "https://files.pythonhosted.org/packages/5c/31/d68020aa6276bd1a7436ece96d540ad17c204d97285639e0757ef1c3d430/hiredis-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3ece960008dab66c6b8bb3a1350764677ee7c74ccd6270aaf1b1caf9ccebb46", size = 159705 }, + { url = "https://files.pythonhosted.org/packages/f7/68/5d101f8ffd764a96c2b959815adebb1e4b7e06db68122f9d3dbbc19b81eb/hiredis-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f75999ae00a920f7dce6ecae76fa5e8674a3110e5a75f12c7a2c75ae1af53396", size = 171498 }, + { url = "https://files.pythonhosted.org/packages/83/86/66131743a2012f668f84aa2eddc07e7b2462b4a07a753b27125f14e4b8bc/hiredis-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e069967cbd5e1900aafc4b5943888f6d34937fc59bf8918a1a546cb729b4b1e4", size = 163951 }, + { url = "https://files.pythonhosted.org/packages/a5/ea/58976d9c21086975a90c7fa2337591ea3903eeb55083e366b5ea36b99ca5/hiredis-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0aacc0a78e1d94d843a6d191f224a35893e6bdfeb77a4a89264155015c65f126", size = 161566 }, + { url = "https://files.pythonhosted.org/packages/39/69/cdb255e3d37f82f31f4b7b2db5bbd8500eae8d22c0d7992fe474fd02babd/hiredis-3.0.0-cp311-cp311-win32.whl", hash = "sha256:719c32147ba29528cb451f037bf837dcdda4ff3ddb6cdb12c4216b0973174718", size = 20037 }, + { url = "https://files.pythonhosted.org/packages/9d/cf/40d209e0458ac28a26973d1449df2922c7b8259f7f88d7738d11c87f9ff6/hiredis-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:bdc144d56333c52c853c31b4e2e52cfbdb22d3da4374c00f5f3d67c42158970f", size = 21862 }, + { url = "https://files.pythonhosted.org/packages/ae/09/0a3eace00115d8c82a8e7d8e58e60aacec10334f4f1512f09ffbac3252e3/hiredis-3.0.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a", size = 81540 }, + { url = "https://files.pythonhosted.org/packages/1c/e8/1a7a5ded4fb11e91aafc5ba5518392f22883d54e79c4b47f188fb712ea46/hiredis-3.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1", size = 44814 }, + { url = "https://files.pythonhosted.org/packages/3b/f5/4e055dc9b55484644afb18063f28649cdbd19be4f15bc152bd633dccd6f7/hiredis-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1", size = 42478 }, + { url = "https://files.pythonhosted.org/packages/65/7b/e06f55b9dcdf10cb6b3f08d7917d3080096cd83deaef1bd4927720fbb280/hiredis-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd", size = 168303 }, + { url = "https://files.pythonhosted.org/packages/f4/16/081e90137bb896acd9dc2e1e68480cc84d652af4d959e75e52d6ce9dd602/hiredis-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa", size = 179151 }, + { url = "https://files.pythonhosted.org/packages/1e/0f/f5aba1c82977f4b639e5b450c0d8685333f1200cd1972647eb3f4d972e55/hiredis-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c", size = 168580 }, + { url = "https://files.pythonhosted.org/packages/60/86/aa24c20f6d3038bf244bc60a2fe8cde61fb3c0d6a82e2bed30b08d55f96c/hiredis-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9", size = 169147 }, + { url = "https://files.pythonhosted.org/packages/6e/03/a4c7a28b6320ef3e36062c1c51e9d66e889c9e09ee7d7ae38b8a2ffdb365/hiredis-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5", size = 164722 }, + { url = "https://files.pythonhosted.org/packages/cd/66/d60106b56ba0ddd9789656d204a577591ff0cd91ab94178bb96c84d0d918/hiredis-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4", size = 162561 }, + { url = "https://files.pythonhosted.org/packages/6a/30/f33f2b782096efe9fe6b24c67a4df13b5055d9c859f615a74fb4f18cce41/hiredis-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9", size = 161388 }, + { url = "https://files.pythonhosted.org/packages/45/02/34d9b151f9ea4655bfe00e0230f7db8fd8a52c7b7bd728efdf1c17655860/hiredis-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717", size = 173561 }, + { url = "https://files.pythonhosted.org/packages/cf/54/68285d208918b6d83e32d872d8dcbf8d479ed2c74b863b836e48a2702a3f/hiredis-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001", size = 165914 }, + { url = "https://files.pythonhosted.org/packages/56/4f/5f36865f9f032caf00d603ff9cbde21506d2b1e0e0ce0b5d2ce2851411c9/hiredis-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232", size = 163968 }, + { url = "https://files.pythonhosted.org/packages/d3/ee/c38693bd1dbce34806ecc3536dc425e87e420030de7018194865511860c2/hiredis-3.0.0-cp312-cp312-win32.whl", hash = "sha256:120f2dda469b28d12ccff7c2230225162e174657b49cf4cd119db525414ae281", size = 20189 }, + { url = "https://files.pythonhosted.org/packages/4e/67/f50b45071bb8652fa9a28a84ee470a02042fb7a096a16f3c08842f2a5c2b/hiredis-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:e584fe5f4e6681d8762982be055f1534e0170f6308a7a90f58d737bab12ff6a8", size = 21971 }, + { url = "https://files.pythonhosted.org/packages/6c/26/fee1a29d7d0cbb76e27ac0914bb17565b1d7cfa24d58922010a667190afc/hiredis-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50da7a9edf371441dfcc56288d790985ee9840d982750580710a9789b8f4a290", size = 39805 }, + { url = "https://files.pythonhosted.org/packages/c7/da/4e9fadc0615958b58e6632d6e85375062f80b60b268b21fa3f449aeee02e/hiredis-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b285ef6bf1581310b0d5e8f6ce64f790a1c40e89c660e1320b35f7515433672", size = 36883 }, + { url = "https://files.pythonhosted.org/packages/cf/d5/cc88b23e466ee070e0109a3e7d7e7835608ad90f80d8415bf7c8c726e71d/hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dcfa684966f25b335072115de2f920228a3c2caf79d4bfa2b30f6e4f674a948", size = 47867 }, + { url = "https://files.pythonhosted.org/packages/09/5b/848006ee860cf543a8b964c17ef04a61ea16967c9b5f173557286ae1afd2/hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a41be8af1fd78ca97bc948d789a09b730d1e7587d07ca53af05758f31f4b985d", size = 48254 }, + { url = "https://files.pythonhosted.org/packages/91/41/ef57d7f6f324ea5052d707a510093ec61fde8c5f271029116490790168cf/hiredis-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:038756db735e417ab36ee6fd7725ce412385ed2bd0767e8179a4755ea11b804f", size = 55556 }, + { url = "https://files.pythonhosted.org/packages/81/52/150658b3006241f2de243e2ccb7f94cfeb74a855435e872dbde7d87f6842/hiredis-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fcecbd39bd42cef905c0b51c9689c39d0cc8b88b1671e7f40d4fb213423aef3a", size = 21938 }, +] + +[[package]] +name = "hpack" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/fda93fb4d957db19b0f6b370e79d586b3e8528b20252c729c476a2c02954/hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095", size = 49117 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/34/e8b383f35b77c402d28563d2b8f83159319b509bc5f760b15d60b0abf165/hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", size = 32611 }, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "h11", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/b0/5e8b8674f8d203335a62fdfcfa0d11ebe09e23613c3391033cbba35f7926/httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", size = 83234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/d4/e5d7e4f2174f8a4d63c8897d79eb8fe2503f7ecc03282fee1fa2719c2704/httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5", size = 77926 }, +] + +[[package]] +name = "httplib2" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/ad/2371116b22d616c194aa25ec410c9c6c37f23599dcd590502b74db197584/httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81", size = 351116 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/6c/d2fbdaaa5959339d53ba38e94c123e4e84b8fbc4b84beb0e70d7c1608486/httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", size = 96854 }, +] + +[[package]] +name = "httptools" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/d77686502fced061b3ead1c35a2d70f6b281b5f723c4eff7a2277c04e4a2/httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a", size = 191228 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/6a/80bce0216b63babf51cdc34814c3f0f10489e13ab89fb6bc91202736a8a2/httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f", size = 149778 }, + { url = "https://files.pythonhosted.org/packages/bd/7d/4cd75356dfe0ed0b40ca6873646bf9ff7b5138236c72338dc569dc57d509/httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563", size = 77604 }, + { url = "https://files.pythonhosted.org/packages/4e/74/6348ce41fb5c1484f35184c172efb8854a288e6090bb54e2210598268369/httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58", size = 346717 }, + { url = "https://files.pythonhosted.org/packages/65/e7/dd5ba95c84047118a363f0755ad78e639e0529be92424bb020496578aa3b/httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185", size = 341442 }, + { url = "https://files.pythonhosted.org/packages/d8/97/b37d596bc32be291477a8912bf9d1508d7e8553aa11a30cd871fd89cbae4/httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142", size = 354531 }, + { url = "https://files.pythonhosted.org/packages/99/c9/53ed7176583ec4b4364d941a08624288f2ae55b4ff58b392cdb68db1e1ed/httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658", size = 347754 }, + { url = "https://files.pythonhosted.org/packages/1e/fc/8a26c2adcd3f141e4729897633f03832b71ebea6f4c31cce67a92ded1961/httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b", size = 58165 }, + { url = "https://files.pythonhosted.org/packages/f5/d1/53283b96ed823d5e4d89ee9aa0f29df5a1bdf67f148e061549a595d534e4/httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1", size = 145855 }, + { url = "https://files.pythonhosted.org/packages/80/dd/cebc9d4b1d4b70e9f3d40d1db0829a28d57ca139d0b04197713816a11996/httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0", size = 75604 }, + { url = "https://files.pythonhosted.org/packages/76/7a/45c5a9a2e9d21f7381866eb7b6ead5a84d8fe7e54e35208eeb18320a29b4/httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc", size = 324784 }, + { url = "https://files.pythonhosted.org/packages/59/23/047a89e66045232fb82c50ae57699e40f70e073ae5ccd53f54e532fbd2a2/httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2", size = 318547 }, + { url = "https://files.pythonhosted.org/packages/82/f5/50708abc7965d7d93c0ee14a148ccc6d078a508f47fe9357c79d5360f252/httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837", size = 330211 }, + { url = "https://files.pythonhosted.org/packages/e3/1e/9823ca7aab323c0e0e9dd82ce835a6e93b69f69aedffbc94d31e327f4283/httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d", size = 322174 }, + { url = "https://files.pythonhosted.org/packages/14/e4/20d28dfe7f5b5603b6b04c33bb88662ad749de51f0c539a561f235f42666/httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3", size = 55434 }, + { url = "https://files.pythonhosted.org/packages/60/13/b62e086b650752adf9094b7e62dab97f4cb7701005664544494b7956a51e/httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0", size = 146354 }, + { url = "https://files.pythonhosted.org/packages/f8/5d/9ad32b79b6c24524087e78aa3f0a2dfcf58c11c90e090e4593b35def8a86/httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2", size = 75785 }, + { url = "https://files.pythonhosted.org/packages/d0/a4/b503851c40f20bcbd453db24ed35d961f62abdae0dccc8f672cd5d350d87/httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90", size = 345396 }, + { url = "https://files.pythonhosted.org/packages/a2/9a/aa406864f3108e06f7320425a528ff8267124dead1fd72a3e9da2067f893/httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503", size = 344741 }, + { url = "https://files.pythonhosted.org/packages/cf/3a/3fd8dfb987c4247651baf2ac6f28e8e9f889d484ca1a41a9ad0f04dfe300/httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84", size = 345096 }, + { url = "https://files.pythonhosted.org/packages/80/01/379f6466d8e2edb861c1f44ccac255ed1f8a0d4c5c666a1ceb34caad7555/httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb", size = 343535 }, + { url = "https://files.pythonhosted.org/packages/d3/97/60860e9ee87a7d4712b98f7e1411730520053b9d69e9e42b0b9751809c17/httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949", size = 55660 }, +] + +[[package]] +name = "httpx" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httpcore", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "sniffio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/3da5bdf4408b8b2800061c339f240c1802f2e82d55e50bd39c5a881f47f0/httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5", size = 126413 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/7b/ddacf6dcebb42466abd03f368782142baa82e08fc0c1f8eaa05b4bae87d5/httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5", size = 75590 }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.24.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "fsspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/24/b98fce967b7d63700e5805b915012ba25bb538a81fcf11e97f3cc3f4f012/huggingface_hub-0.24.6.tar.gz", hash = "sha256:cc2579e761d070713eaa9c323e3debe39d5b464ae3a7261c39a9195b27bb8000", size = 349200 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/8f/d6718641c14d98a5848c6a24d2376028d292074ffade0702940a4b1dde76/huggingface_hub-0.24.6-py3-none-any.whl", hash = "sha256:a990f3232aa985fe749bc9474060cbad75e8b2f115f6665a9fda5b9c97818970", size = 417509 }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 }, +] + +[[package]] +name = "hyperframe" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/2a/4747bff0a17f7281abe73e955d60d80aae537a5d203f417fa1c2e7578ebb/hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914", size = 25008 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/de/85a784bcc4a3779d1753a7ec2dee5de90e18c7bcf402e71b51fcf150b129/hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15", size = 12389 }, +] + +[[package]] +name = "identify" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/32/f4/8e8f7db397a7ce20fbdeac5f25adaf567fc362472432938d25556008e03a/identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf", size = 99116 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/6c/a4f39abe7f19600b74528d0c717b52fff0b300bb0161081510d39c53cb00/identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0", size = 98962 }, +] + +[[package]] +name = "idna" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/ed/f86a79a07470cb07819390452f178b3bef1d375f2ec021ecfc709fc7cf07/idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", size = 189575 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0", size = 66836 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/ff/bd28f70283b9cca0cbf0c2a6082acbecd822d1962ae7b2a904861b9965f8/importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812", size = 52667 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/ef/38766b2edb096260d9b1b6ad35adaa0bce3b0567abb452b21eb074af88c4/importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f", size = 24769 }, +] + +[[package]] +name = "importlib-resources" +version = "6.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b3/0412c28d21e31447e97728efcf8913afe1936692917629e6bdb847563484/importlib_resources-6.4.3.tar.gz", hash = "sha256:4a202b9b9d38563b46da59221d77bb73862ab5d79d461307bcb826d725448b98", size = 42026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8b/e848c888201b211159cfceaac65cc3bc1e32ed9ab6ca30366c43e5f1969b/importlib_resources-6.4.3-py3-none-any.whl", hash = "sha256:2d6dfe3b9e055f72495c2085890837fc8c758984e209115c8792bddcb762cd93", size = 35265 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "ipykernel" +version = "6.29.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "(platform_system == 'Darwin' and sys_platform == 'darwin') or (platform_system == 'Darwin' and sys_platform == 'linux') or (platform_system == 'Darwin' and sys_platform == 'win32')" }, + { name = "comm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "debugpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "ipython", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jupyter-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jupyter-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "matplotlib-inline", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nest-asyncio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyzmq", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tornado", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173 }, +] + +[[package]] +name = "ipython" +version = "8.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, + { name = "jedi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "matplotlib-inline", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pexpect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "prompt-toolkit", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "stack-data", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/f4/dc45805e5c3e327a626139c023b296bafa4537e602a61055d377704ca54c/ipython-8.26.0.tar.gz", hash = "sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c", size = 5493422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/48/4d2818054671bb272d1b12ca65748a4145dc602a463683b5c21b260becee/ipython-8.26.0-py3-none-any.whl", hash = "sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff", size = 817939 }, +] + +[[package]] +name = "isodate" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/7a/c0a56c7d56c7fa723988f122fa1f1ccf8c5c4ccc48efad0d214b49e5b1af/isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9", size = 28443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/85/7882d311924cbcfc70b1890780763e36ff0b140c7e51c110fc59a532f087/isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96", size = 41722 }, +] + +[[package]] +name = "jedi" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/99/99b493cec4bf43176b678de30f81ed003fd6a647a301b9c927280c600f0a/jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", size = 1227821 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/9f/bc63f0f0737ad7a60800bfd472a4836661adae21f9c2535f3957b1e54ceb/jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0", size = 1569361 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "jiter" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/1a/aa64be757afc614484b370a4d9fc1747dc9237b37ce464f7f9d9ca2a3d38/jiter-0.5.0.tar.gz", hash = "sha256:1d916ba875bcab5c5f7d927df998c4cb694d27dceddf3392e58beaf10563368a", size = 158300 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/09/f659fc67d6aaa82c56432c4a7cc8365fff763acbf1c8f24121076617f207/jiter-0.5.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b599f4e89b3def9a94091e6ee52e1d7ad7bc33e238ebb9c4c63f211d74822c3f", size = 284126 }, + { url = "https://files.pythonhosted.org/packages/07/2d/5bdaddfefc44f91af0f3340e75ef327950d790c9f86490757ac8b395c074/jiter-0.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a063f71c4b06225543dddadbe09d203dc0c95ba352d8b85f1221173480a71d5", size = 299265 }, + { url = "https://files.pythonhosted.org/packages/74/bd/964485231deaec8caa6599f3f27c8787a54e9f9373ae80dcfbda2ad79c02/jiter-0.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc0d5b8b3dd12e91dd184b87273f864b363dfabc90ef29a1092d269f18c7e28", size = 332178 }, + { url = "https://files.pythonhosted.org/packages/cf/4f/6353179174db10254549bbf2eb2c7ea102e59e0460ee374adb12071c274d/jiter-0.5.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c22541f0b672f4d741382a97c65609332a783501551445ab2df137ada01e019e", size = 342533 }, + { url = "https://files.pythonhosted.org/packages/76/6f/21576071b8b056ef743129b9dacf9da65e328b58766f3d1ea265e966f000/jiter-0.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63314832e302cc10d8dfbda0333a384bf4bcfce80d65fe99b0f3c0da8945a91a", size = 363469 }, + { url = "https://files.pythonhosted.org/packages/73/a1/9ef99a279c72a031dbe8a4085db41e3521ae01ab0058651d6ccc809a5e93/jiter-0.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a25fbd8a5a58061e433d6fae6d5298777c0814a8bcefa1e5ecfff20c594bd749", size = 379078 }, + { url = "https://files.pythonhosted.org/packages/41/6a/c038077509d67fe876c724bfe9ad15334593851a7def0d84518172bdd44a/jiter-0.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:503b2c27d87dfff5ab717a8200fbbcf4714516c9d85558048b1fc14d2de7d8dc", size = 318943 }, + { url = "https://files.pythonhosted.org/packages/67/0d/d82673814eb38c208b7881581df596e680f8c2c003e2b80c25ca58975ee4/jiter-0.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d1f3d27cce923713933a844872d213d244e09b53ec99b7a7fdf73d543529d6d", size = 357394 }, + { url = "https://files.pythonhosted.org/packages/56/9e/cbd8f6612346c38cc42e41e35cda19ce78f5b12e4106d1186e8e95ee839b/jiter-0.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c95980207b3998f2c3b3098f357994d3fd7661121f30669ca7cb945f09510a87", size = 511080 }, + { url = "https://files.pythonhosted.org/packages/ff/33/135c0c33565b6d5c3010d047710837427dd24c9adbc9ca090f3f92df446e/jiter-0.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:afa66939d834b0ce063f57d9895e8036ffc41c4bd90e4a99631e5f261d9b518e", size = 492827 }, + { url = "https://files.pythonhosted.org/packages/68/c1/491a8ef682508edbaf2a32e41c1b1e34064078b369b0c2d141170999d1c9/jiter-0.5.0-cp310-none-win32.whl", hash = "sha256:f16ca8f10e62f25fd81d5310e852df6649af17824146ca74647a018424ddeccf", size = 195081 }, + { url = "https://files.pythonhosted.org/packages/31/20/8cda4faa9571affea6130b150289522a22329778bdfa45a7aab4e7edff95/jiter-0.5.0-cp310-none-win_amd64.whl", hash = "sha256:b2950e4798e82dd9176935ef6a55cf6a448b5c71515a556da3f6b811a7844f1e", size = 190977 }, + { url = "https://files.pythonhosted.org/packages/94/5f/3ac960ed598726aae46edea916e6df4df7ff6fe084bc60774b95cf3154e6/jiter-0.5.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4c8e1ed0ef31ad29cae5ea16b9e41529eb50a7fba70600008e9f8de6376d553", size = 284131 }, + { url = "https://files.pythonhosted.org/packages/03/eb/2308fa5f5c14c97c4c7720fef9465f1fa0771826cddb4eec9866bdd88846/jiter-0.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6f16e21276074a12d8421692515b3fd6d2ea9c94fd0734c39a12960a20e85f3", size = 299310 }, + { url = "https://files.pythonhosted.org/packages/3c/f6/dba34ca10b44715fa5302b8e8d2113f72eb00a9297ddf3fa0ae4fd22d1d1/jiter-0.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5280e68e7740c8c128d3ae5ab63335ce6d1fb6603d3b809637b11713487af9e6", size = 332282 }, + { url = "https://files.pythonhosted.org/packages/69/f7/64e0a7439790ec47f7681adb3871c9d9c45fff771102490bbee5e92c00b7/jiter-0.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:583c57fc30cc1fec360e66323aadd7fc3edeec01289bfafc35d3b9dcb29495e4", size = 342370 }, + { url = "https://files.pythonhosted.org/packages/55/31/1efbfff2ae8e4d919144c53db19b828049ad0622a670be3bbea94a86282c/jiter-0.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26351cc14507bdf466b5f99aba3df3143a59da75799bf64a53a3ad3155ecded9", size = 363591 }, + { url = "https://files.pythonhosted.org/packages/30/c3/7ab2ca2276426a7398c6dfb651e38dbc81954c79a3bfbc36c514d8599499/jiter-0.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829df14d656b3fb87e50ae8b48253a8851c707da9f30d45aacab2aa2ba2d614", size = 378551 }, + { url = "https://files.pythonhosted.org/packages/47/e7/5d88031cd743c62199b125181a591b1671df3ff2f6e102df85c58d8f7d31/jiter-0.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42a4bdcf7307b86cb863b2fb9bb55029b422d8f86276a50487982d99eed7c6e", size = 319152 }, + { url = "https://files.pythonhosted.org/packages/4c/2d/09ea58e1adca9f0359f3d41ef44a1a18e59518d7c43a21f4ece9e72e28c0/jiter-0.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04d461ad0aebf696f8da13c99bc1b3e06f66ecf6cfd56254cc402f6385231c06", size = 357377 }, + { url = "https://files.pythonhosted.org/packages/7d/2f/83ff1058cb56fc3ff73e0d3c6440703ddc9cdb7f759b00cfbde8228fc435/jiter-0.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6375923c5f19888c9226582a124b77b622f8fd0018b843c45eeb19d9701c403", size = 511091 }, + { url = "https://files.pythonhosted.org/packages/ae/c9/4f85f97c9894382ab457382337aea0012711baaa17f2ed55c0ff25f3668a/jiter-0.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cec323a853c24fd0472517113768c92ae0be8f8c384ef4441d3632da8baa646", size = 492948 }, + { url = "https://files.pythonhosted.org/packages/4d/f2/2e987e0eb465e064c5f52c2f29c8d955452e3b316746e326269263bfb1b7/jiter-0.5.0-cp311-none-win32.whl", hash = "sha256:aa1db0967130b5cab63dfe4d6ff547c88b2a394c3410db64744d491df7f069bb", size = 195183 }, + { url = "https://files.pythonhosted.org/packages/ab/59/05d1c3203c349b37c4dd28b02b9b4e5915a7bcbd9319173b4548a67d2e93/jiter-0.5.0-cp311-none-win_amd64.whl", hash = "sha256:aa9d2b85b2ed7dc7697597dcfaac66e63c1b3028652f751c81c65a9f220899ae", size = 191032 }, + { url = "https://files.pythonhosted.org/packages/aa/bd/c3950e2c478161e131bed8cb67c36aed418190e2a961a1c981e69954e54b/jiter-0.5.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9f664e7351604f91dcdd557603c57fc0d551bc65cc0a732fdacbf73ad335049a", size = 283511 }, + { url = "https://files.pythonhosted.org/packages/80/1c/8ce58d8c37a589eeaaa5d07d131fd31043886f5e77ab50c00a66d869a361/jiter-0.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:044f2f1148b5248ad2c8c3afb43430dccf676c5a5834d2f5089a4e6c5bbd64df", size = 296974 }, + { url = "https://files.pythonhosted.org/packages/4d/b8/6faeff9eed8952bed93a77ea1cffae7b946795b88eafd1a60e87a67b09e0/jiter-0.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:702e3520384c88b6e270c55c772d4bd6d7b150608dcc94dea87ceba1b6391248", size = 331897 }, + { url = "https://files.pythonhosted.org/packages/4f/54/1d9a2209b46d39ce6f0cef3ad87c462f9c50312ab84585e6bd5541292b35/jiter-0.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:528d742dcde73fad9d63e8242c036ab4a84389a56e04efd854062b660f559544", size = 342962 }, + { url = "https://files.pythonhosted.org/packages/2a/de/90360be7fc54b2b4c2dfe79eb4ed1f659fce9c96682e6a0be4bbe71371f7/jiter-0.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf80e5fe6ab582c82f0c3331df27a7e1565e2dcf06265afd5173d809cdbf9ba", size = 363844 }, + { url = "https://files.pythonhosted.org/packages/ba/ad/ef32b173191b7a53ea8a6757b80723cba321f8469834825e8c71c96bde17/jiter-0.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44dfc9ddfb9b51a5626568ef4e55ada462b7328996294fe4d36de02fce42721f", size = 378709 }, + { url = "https://files.pythonhosted.org/packages/07/de/353ce53743c0defbbbd652e89c106a97dbbac4eb42c95920b74b5056b93a/jiter-0.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c451f7922992751a936b96c5f5b9bb9312243d9b754c34b33d0cb72c84669f4e", size = 319038 }, + { url = "https://files.pythonhosted.org/packages/3f/92/42d47310bf9530b9dece9e2d7c6d51cf419af5586ededaf5e66622d160e2/jiter-0.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:308fce789a2f093dca1ff91ac391f11a9f99c35369117ad5a5c6c4903e1b3e3a", size = 357763 }, + { url = "https://files.pythonhosted.org/packages/bd/8c/2bb76a9a84474d48fdd133d3445db8a4413da4e87c23879d917e000a9d87/jiter-0.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7f5ad4a7c6b0d90776fdefa294f662e8a86871e601309643de30bf94bb93a64e", size = 511031 }, + { url = "https://files.pythonhosted.org/packages/33/4f/9f23d79c0795e0a8e56e7988e8785c2dcda27e0ed37977256d50c77c6a19/jiter-0.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ea189db75f8eca08807d02ae27929e890c7d47599ce3d0a6a5d41f2419ecf338", size = 493042 }, + { url = "https://files.pythonhosted.org/packages/df/67/8a4f975aa834b8aecdb6b131422390173928fd47f42f269dcc32034ab432/jiter-0.5.0-cp312-none-win32.whl", hash = "sha256:e3bbe3910c724b877846186c25fe3c802e105a2c1fc2b57d6688b9f8772026e4", size = 195405 }, + { url = "https://files.pythonhosted.org/packages/15/81/296b1e25c43db67848728cdab34ac3eb5c5cbb4955ceb3f51ae60d4a5e3d/jiter-0.5.0-cp312-none-win_amd64.whl", hash = "sha256:a586832f70c3f1481732919215f36d41c59ca080fa27a65cf23d9490e75b2ef5", size = 189720 }, +] + +[[package]] +name = "joblib" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 }, +] + +[[package]] +name = "jsonschema" +version = "4.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jsonschema-specifications", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "referencing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "rpds-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, +] + +[[package]] +name = "jsonschema-path" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "referencing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/39/3a58b63a997b0cf824536d6f84fff82645a1ca8de222ee63586adab44dfa/jsonschema_path-0.3.3.tar.gz", hash = "sha256:f02e5481a4288ec062f8e68c808569e427d905bedfecb7f2e4c69ef77957c382", size = 11589 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/b0/69237e85976916b2e37586b7ddc48b9547fc38b440e25103d084b2b02ab3/jsonschema_path-0.3.3-py3-none-any.whl", hash = "sha256:203aff257f8038cd3c67be614fe6b2001043408cb1b4e36576bc4921e09d83c4", size = 14817 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b9/cc0cc592e7c195fb8a650c1d5990b10175cf13b4c97465c72ec841de9e4b/jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", size = 13983 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/07/44bd408781594c4d0a027666ef27fab1e441b109dc3b76b4f836f8fd04fe/jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c", size = 18482 }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyzmq", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tornado", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/61/3cd51dea7878691919adc34ff6ad180f13bfe25fb8c7662a9ee6dc64e643/jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df", size = 341102 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/d3/c4bb02580bc0db807edb9a29b2d0c56031be1ef0d804336deb2699a470f6/jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f", size = 105901 }, +] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/11/b56381fa6c3f4cc5d2cf54a7dbf98ad9aa0b339ef7a601d6053538b079a7/jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9", size = 87629 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", size = 28965 }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884 }, +] + +[[package]] +name = "kubernetes" +version = "30.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-auth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "oauthlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests-oauthlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "websocket-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/3c/9f29f6cab7f35df8e54f019e5719465fa97b877be2454e99f989270b4f34/kubernetes-30.1.0.tar.gz", hash = "sha256:41e4c77af9f28e7a6c314e3bd06a8c6229ddd787cad684e0ab9f69b498e98ebc", size = 887810 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/2027ddede72d33be2effc087580aeba07e733a7360780ae87226f1f91bd8/kubernetes-30.1.0-py2.py3-none-any.whl", hash = "sha256:e212e8b7579031dd2e512168b617373bc1e03888d41ac4e04039240a292d478d", size = 1706042 }, +] + +[[package]] +name = "lazy-object-proxy" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/f0/f02e2d150d581a294efded4020094a371bbab42423fe78625ac18854d89b/lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69", size = 43271 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/42/a96d9d153f6ea38b925494cb9b42cf4a9f98fd30cad3124fc22e9d04ec34/lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977", size = 27432 }, + { url = "https://files.pythonhosted.org/packages/4a/0d/b325461e43dde8d7644e9b9e9dd57f2a4af472b588c51ccbc92778e60ea4/lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3", size = 69133 }, + { url = "https://files.pythonhosted.org/packages/8b/fc/83711d743fb5aaca5747bbf225fe3b5cbe085c7f6c115856b5cce80f3224/lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05", size = 68272 }, + { url = "https://files.pythonhosted.org/packages/8d/b5/ea47215abd4da45791664d7bbfe2976ca0de2c37af38b5e9e6cf89e0e65e/lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895", size = 70891 }, + { url = "https://files.pythonhosted.org/packages/8b/9b/908e12e5fa265ea1579261ff80f7b2136fd2ba254bc7f4f7e3dba83fd0f2/lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83", size = 70451 }, + { url = "https://files.pythonhosted.org/packages/16/ab/d9a47f2e70767af5ee311d71109be6ef2991c66c77bfa18e66707edd9f8c/lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9", size = 25778 }, + { url = "https://files.pythonhosted.org/packages/74/d6/0104e4154d2c30227eb54491dda8a4132be046b4cb37fb4ce915a5abc0d5/lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4", size = 27551 }, + { url = "https://files.pythonhosted.org/packages/ff/e1/99a7ec68b892c9b8c6212617f54e7e9b0304d47edad8c0ff043ae3aeb1a9/lazy_object_proxy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fec03caabbc6b59ea4a638bee5fce7117be8e99a4103d9d5ad77f15d6f81020c", size = 27434 }, + { url = "https://files.pythonhosted.org/packages/1a/76/6a41de4b44d1dcfe4c720d4606de0d7b69b6b450f0bdce16f2e1fb8abc89/lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c83f957782cbbe8136bee26416686a6ae998c7b6191711a04da776dc9e47d4", size = 70687 }, + { url = "https://files.pythonhosted.org/packages/1e/5d/eaa12126e8989c9bdd21d864cbba2b258cb9ee2f574ada1462a0004cfad8/lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009e6bb1f1935a62889ddc8541514b6a9e1fcf302667dcb049a0be5c8f613e56", size = 69757 }, + { url = "https://files.pythonhosted.org/packages/53/a9/6f22cfe9572929656988b72c0de266c5d10755369b575322725f67364c4e/lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75fc59fc450050b1b3c203c35020bc41bd2695ed692a392924c6ce180c6f1dc9", size = 73709 }, + { url = "https://files.pythonhosted.org/packages/bd/e6/b10fd94710a99a6309f3ad61a4eb480944bbb17fcb41bd2d852fdbee57ee/lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:782e2c9b2aab1708ffb07d4bf377d12901d7a1d99e5e410d648d892f8967ab1f", size = 73191 }, + { url = "https://files.pythonhosted.org/packages/c9/78/a9b9d314da02fe66b632f2354e20e40fc3508befb450b5a17987a222b383/lazy_object_proxy-1.10.0-cp311-cp311-win32.whl", hash = "sha256:edb45bb8278574710e68a6b021599a10ce730d156e5b254941754a9cc0b17d03", size = 25773 }, + { url = "https://files.pythonhosted.org/packages/94/e6/e2d3b0c9efe61f72dc327ce2355941f540e0b0d1f2b3490cbab6bab7d3ea/lazy_object_proxy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:e271058822765ad5e3bca7f05f2ace0de58a3f4e62045a8c90a0dfd2f8ad8cc6", size = 27550 }, + { url = "https://files.pythonhosted.org/packages/d0/5d/768a7f2ccebb29604def61842fd54f6f5f75c79e366ee8748dda84de0b13/lazy_object_proxy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e98c8af98d5707dcdecc9ab0863c0ea6e88545d42ca7c3feffb6b4d1e370c7ba", size = 27560 }, + { url = "https://files.pythonhosted.org/packages/b3/ce/f369815549dbfa4bebed541fa4e1561d69e4f268a1f6f77da886df182dab/lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:952c81d415b9b80ea261d2372d2a4a2332a3890c2b83e0535f263ddfe43f0d43", size = 72403 }, + { url = "https://files.pythonhosted.org/packages/44/46/3771e0a4315044aa7b67da892b2fb1f59dfcf0eaff2c8967b2a0a85d5896/lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b39d3a151309efc8cc48675918891b865bdf742a8616a337cb0090791a0de9", size = 72401 }, + { url = "https://files.pythonhosted.org/packages/81/39/84ce4740718e1c700bd04d3457ac92b2e9ce76529911583e7a2bf4d96eb2/lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e221060b701e2aa2ea991542900dd13907a5c90fa80e199dbf5a03359019e7a3", size = 75375 }, + { url = "https://files.pythonhosted.org/packages/86/3b/d6b65da2b864822324745c0a73fe7fd86c67ccea54173682c3081d7adea8/lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92f09ff65ecff3108e56526f9e2481b8116c0b9e1425325e13245abfd79bdb1b", size = 75466 }, + { url = "https://files.pythonhosted.org/packages/f5/33/467a093bf004a70022cb410c590d937134bba2faa17bf9dc42a48f49af35/lazy_object_proxy-1.10.0-cp312-cp312-win32.whl", hash = "sha256:3ad54b9ddbe20ae9f7c1b29e52f123120772b06dbb18ec6be9101369d63a4074", size = 25914 }, + { url = "https://files.pythonhosted.org/packages/77/ce/7956dc5ac2f8b62291b798c8363c81810e22a9effe469629d297d087e350/lazy_object_proxy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:127a789c75151db6af398b8972178afe6bda7d6f68730c057fbbc2e96b08d282", size = 27525 }, + { url = "https://files.pythonhosted.org/packages/31/8b/94dc8d58704ab87b39faed6f2fc0090b9d90e2e2aa2bbec35c79f3d2a054/lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d", size = 16405 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206 }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620 }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818 }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493 }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630 }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745 }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021 }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659 }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213 }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, +] + +[[package]] +name = "marshmallow" +version = "3.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/40/faa10dc4500bca85f41ca9d8cefab282dd23d0fcc7a9b5fab40691e72e76/marshmallow-3.22.0.tar.gz", hash = "sha256:4972f529104a220bb8637d595aa4c9762afbe7f7a77d82dc58c1615d70c5823e", size = 176836 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/78/c1de55eb3311f2c200a8b91724414b8d6f5ae78891c15d9d936ea43c3dba/marshmallow-3.22.0-py3-none-any.whl", hash = "sha256:71a2dce49ef901c3f97ed296ae5051135fd3febd2bf43afe0ae9a82143a494d9", size = 49334 }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "milvus" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/b7/c82bef4474045a82d204eaf48f100e28b281920377c399abcf327b9ba6ac/milvus-2.3.5-py3-none-macosx_12_0_arm64.whl", hash = "sha256:328d2ba24fb04a595f47ab226abf5565691bfe242beb88e61b31326d0416bf1a", size = 37754340 }, + { url = "https://files.pythonhosted.org/packages/fa/a2/67dccec2690afac9c738c70bd2f4b5b58c9845bc1b2b0764a7f8470de602/milvus-2.3.5-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:e35a8d6397da1f0f685d0f55afad8654296ff3b3aea296439e53ce9980d1ad22", size = 41879314 }, + { url = "https://files.pythonhosted.org/packages/bd/ed/e216ec677abac11b49bbcc35c3eadf48e6db832e8e4f368f8eed34f23cec/milvus-2.3.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:69515a0630ce29fd10e101fa442afea8ca1387b93a456cd9bd41fdf3deb93d04", size = 57692521 }, +] + +[[package]] +name = "minio" +version = "7.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pycryptodome", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/be/6ddefcacca569bc1199cf8796fef891e67596ae30d865ea27e86b247ca4f/minio-7.2.8.tar.gz", hash = "sha256:f8af2dafc22ebe1aef3ac181b8e217037011c430aa6da276ed627e55aaf7c815", size = 135078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/89/f4d5cfb0a5494e7dae1c11d6d1ab82811d93f6af8ca54e1393c046ff0e75/minio-7.2.8-py3-none-any.whl", hash = "sha256:aa3b485788b63b12406a5798465d12a57e4be2ac2a58a8380959b6b748e64ddd", size = 93488 }, +] + +[[package]] +name = "mistralai" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "orjson", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/20/4204f461588310b3a7ffbbbb7fa573493dc1c8185d376ee72516c04575bf/mistralai-0.4.2.tar.gz", hash = "sha256:5eb656710517168ae053f9847b0bb7f617eda07f1f93f946ad6c91a4d407fd93", size = 14234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/fe/79dad76b8d94b62d9e2aab8446183190e1dc384c617d06c3c93307850e11/mistralai-0.4.2-py3-none-any.whl", hash = "sha256:63c98eea139585f0a3b2c4c6c09c453738bac3958055e6f2362d3866e96b0168", size = 20334 }, +] + +[[package]] +name = "mistune" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c8/f0173fe3bf85fd891aee2e7bcd8207dfe26c2c683d727c5a6cc3aec7b628/mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8", size = 90840 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/74/c95adcdf032956d9ef6c89a9b8a5152bf73915f8c633f3e3d88d06bd699c/mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205", size = 47958 }, +] + +[[package]] +name = "mmh3" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/96/aa247e82878b123468f0079ce2ac77e948315bab91ce45d2934a62e0af95/mmh3-4.1.0.tar.gz", hash = "sha256:a1cf25348b9acd229dda464a094d6170f47d2850a1fcb762a3b6172d2ce6ca4a", size = 26357 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/5a/8609dc74421858f7e94a89dc69221ab9b2c14d0d63a139b46ec190eedc44/mmh3-4.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:be5ac76a8b0cd8095784e51e4c1c9c318c19edcd1709a06eb14979c8d850c31a", size = 39433 }, + { url = "https://files.pythonhosted.org/packages/93/6c/e7a0f07c7082c76964b1ff46aa852f36e2ec6a9c3530dec0afa0b3162fc2/mmh3-4.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98a49121afdfab67cd80e912b36404139d7deceb6773a83620137aaa0da5714c", size = 29280 }, + { url = "https://files.pythonhosted.org/packages/76/84/60ca728ec7d7e1779a98000d64941c6221786124b4f07bf105a627055890/mmh3-4.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5259ac0535874366e7d1a5423ef746e0d36a9e3c14509ce6511614bdc5a7ef5b", size = 30130 }, + { url = "https://files.pythonhosted.org/packages/2a/22/f2ec190b491f712d9ef5ea6252204b6f05255ac9af54a7b505adc3128aed/mmh3-4.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5950827ca0453a2be357696da509ab39646044e3fa15cad364eb65d78797437", size = 68837 }, + { url = "https://files.pythonhosted.org/packages/ae/b9/c1e8065671e1d2f4e280c9c57389e74964f4a5792cac26717ad592002c7d/mmh3-4.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dd0f652ae99585b9dd26de458e5f08571522f0402155809fd1dc8852a613a39", size = 72275 }, + { url = "https://files.pythonhosted.org/packages/6b/18/92bbdb102ab2b4e80084e927187d871758280eb067c649693e42bfc6d0d1/mmh3-4.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99d25548070942fab1e4a6f04d1626d67e66d0b81ed6571ecfca511f3edf07e6", size = 70919 }, + { url = "https://files.pythonhosted.org/packages/e2/cd/391ce1d1bb559871a5d3a6bbb30b82bf51d3e3b42c4e8589cccb201953da/mmh3-4.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53db8d9bad3cb66c8f35cbc894f336273f63489ce4ac416634932e3cbe79eb5b", size = 65885 }, + { url = "https://files.pythonhosted.org/packages/03/87/4b01a43336bd506478850d1bc3d180648b2d26b4acf1fc4bf1df72bf562f/mmh3-4.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75da0f615eb55295a437264cc0b736753f830b09d102aa4c2a7d719bc445ec05", size = 67610 }, + { url = "https://files.pythonhosted.org/packages/e8/12/b464149a1b7181c7ce431ebf3d24fa994863f2f1abc75b78d202dde966e0/mmh3-4.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b926b07fd678ea84b3a2afc1fa22ce50aeb627839c44382f3d0291e945621e1a", size = 74888 }, + { url = "https://files.pythonhosted.org/packages/fc/3e/f4eb45a23fc17b970394c1fe74eba157514577ae2d63757684241651d754/mmh3-4.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c5b053334f9b0af8559d6da9dc72cef0a65b325ebb3e630c680012323c950bb6", size = 72969 }, + { url = "https://files.pythonhosted.org/packages/c0/3b/83934fd9494371357da0ca026d55ad427c199d611b97b6ffeecacfd8e720/mmh3-4.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bf33dc43cd6de2cb86e0aa73a1cc6530f557854bbbe5d59f41ef6de2e353d7b", size = 80338 }, + { url = "https://files.pythonhosted.org/packages/b6/c4/5bcd709ea7269173d7e925402f05e05cf12194ef53cc9912a5ad166f8ded/mmh3-4.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fa7eacd2b830727ba3dd65a365bed8a5c992ecd0c8348cf39a05cc77d22f4970", size = 76580 }, + { url = "https://files.pythonhosted.org/packages/da/6a/4c0680d64475e551d7f4cc78bf0fd247c711ed2717f6bb311934993d1e69/mmh3-4.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:42dfd6742b9e3eec599f85270617debfa0bbb913c545bb980c8a4fa7b2d047da", size = 75325 }, + { url = "https://files.pythonhosted.org/packages/70/bc/e2ed99e580b3dd121f6462147bd5f521c57b3c81c692aa2d416b0678c89f/mmh3-4.1.0-cp310-cp310-win32.whl", hash = "sha256:2974ad343f0d39dcc88e93ee6afa96cedc35a9883bc067febd7ff736e207fa47", size = 31235 }, + { url = "https://files.pythonhosted.org/packages/73/2b/3aec865da7feb52830782d9fb7c54115cc18815680c244301adf9080622f/mmh3-4.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:74699a8984ded645c1a24d6078351a056f5a5f1fe5838870412a68ac5e28d865", size = 31271 }, + { url = "https://files.pythonhosted.org/packages/17/2a/925439189ccf562bdcb839aed6263d718359f0c376d673beb3b83d3864ac/mmh3-4.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f0dc874cedc23d46fc488a987faa6ad08ffa79e44fb08e3cd4d4cf2877c00a00", size = 30147 }, + { url = "https://files.pythonhosted.org/packages/2e/d6/86beea107e7e9700df9522466346c23a2f54faa81337c86fd17002aa95a6/mmh3-4.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3280a463855b0eae64b681cd5b9ddd9464b73f81151e87bb7c91a811d25619e6", size = 39427 }, + { url = "https://files.pythonhosted.org/packages/1c/08/65fa5489044e2afc304e8540c6c607d5d7b136ddc5cd8315c13de0adc34c/mmh3-4.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:97ac57c6c3301769e757d444fa7c973ceb002cb66534b39cbab5e38de61cd896", size = 29281 }, + { url = "https://files.pythonhosted.org/packages/b3/aa/98511d3ea3f6ba958136d913be3be3c1009be935a20ecc7b2763f0a605b6/mmh3-4.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b6502cdb4dbd880244818ab363c8770a48cdccecf6d729ade0241b736b5ec0", size = 30130 }, + { url = "https://files.pythonhosted.org/packages/3c/b7/1a93f81643435b0e57f1046c4ffe46f0214693eaede0d9b0a1a236776e70/mmh3-4.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52ba2da04671a9621580ddabf72f06f0e72c1c9c3b7b608849b58b11080d8f14", size = 69072 }, + { url = "https://files.pythonhosted.org/packages/45/9e/2ff70246aefd9cf146bc6a420c28ed475a0d1a325f31ee203be02f9215d4/mmh3-4.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a5fef4c4ecc782e6e43fbeab09cff1bac82c998a1773d3a5ee6a3605cde343e", size = 72470 }, + { url = "https://files.pythonhosted.org/packages/dc/cb/57bc1fdbdbe6837aebfca982494e23e2498ee2a89585c9054713b22e4167/mmh3-4.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5135358a7e00991f73b88cdc8eda5203bf9de22120d10a834c5761dbeb07dd13", size = 71251 }, + { url = "https://files.pythonhosted.org/packages/4d/c2/46d7d2721b69fbdfd30231309e6395f62ff6744e5c00dd8113b9faa06fba/mmh3-4.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cff9ae76a54f7c6fe0167c9c4028c12c1f6de52d68a31d11b6790bb2ae685560", size = 66035 }, + { url = "https://files.pythonhosted.org/packages/6f/a4/7ba4bcc838818bcf018e26d118d5ddb605c23c4fad040dc4d811f1cfcb04/mmh3-4.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f02576a4d106d7830ca90278868bf0983554dd69183b7bbe09f2fcd51cf54f", size = 67844 }, + { url = "https://files.pythonhosted.org/packages/71/ed/8e80d1038e7bb15eaf739711d1fc36f2341acb6b1b95fa77003f2799c91e/mmh3-4.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:073d57425a23721730d3ff5485e2da489dd3c90b04e86243dd7211f889898106", size = 76724 }, + { url = "https://files.pythonhosted.org/packages/1c/22/a6a70ca81f0ce8fe2f3a68d89c1184c2d2d0fbe0ee305da50e972c5ff9fa/mmh3-4.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:71e32ddec7f573a1a0feb8d2cf2af474c50ec21e7a8263026e8d3b4b629805db", size = 75004 }, + { url = "https://files.pythonhosted.org/packages/73/20/abe50b605760f1f5b6e0b436c650649e69ca478d0f41b154f300367c09e4/mmh3-4.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7cbb20b29d57e76a58b40fd8b13a9130db495a12d678d651b459bf61c0714cea", size = 82230 }, + { url = "https://files.pythonhosted.org/packages/45/80/a1fc99d3ee50b573df0bfbb1ad518463af78d2ebca44bfca3b3f9473d651/mmh3-4.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:a42ad267e131d7847076bb7e31050f6c4378cd38e8f1bf7a0edd32f30224d5c9", size = 78679 }, + { url = "https://files.pythonhosted.org/packages/9e/51/6c9ee2ddf3b386f45ff83b6926a5e826635757d91dab04cbf16eee05f9a7/mmh3-4.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4a013979fc9390abadc445ea2527426a0e7a4495c19b74589204f9b71bcaafeb", size = 77382 }, + { url = "https://files.pythonhosted.org/packages/ee/fa/4b377f244c27fac5f0343cc4dc0d2eb0a08049afc8d5322d07be7461a768/mmh3-4.1.0-cp311-cp311-win32.whl", hash = "sha256:1d3b1cdad7c71b7b88966301789a478af142bddcb3a2bee563f7a7d40519a00f", size = 31232 }, + { url = "https://files.pythonhosted.org/packages/d1/b0/500ef56c29b276d796bfdb47c16d34fa18a68945e4d730a6fa7d483583ed/mmh3-4.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0dc6dc32eb03727467da8e17deffe004fbb65e8b5ee2b502d36250d7a3f4e2ec", size = 31276 }, + { url = "https://files.pythonhosted.org/packages/cc/84/94795e6e710c3861f8f355a12be9c9f4b8433a538c983e75bd4c00496a8a/mmh3-4.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9ae3a5c1b32dda121c7dc26f9597ef7b01b4c56a98319a7fe86c35b8bc459ae6", size = 30142 }, + { url = "https://files.pythonhosted.org/packages/18/45/b4d41e86b00eed8c500adbe0007129861710e181c7f49c507ef6beae9496/mmh3-4.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0033d60c7939168ef65ddc396611077a7268bde024f2c23bdc283a19123f9e9c", size = 39495 }, + { url = "https://files.pythonhosted.org/packages/a6/d4/f041b8704cb8d1aad3717105daa582e29818b78a540622dfed84cd00d88f/mmh3-4.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d6af3e2287644b2b08b5924ed3a88c97b87b44ad08e79ca9f93d3470a54a41c5", size = 29334 }, + { url = "https://files.pythonhosted.org/packages/cb/bb/8f75378e1a83b323f9ed06248333c383e7dac614c2f95e1419965cb91693/mmh3-4.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d82eb4defa245e02bb0b0dc4f1e7ee284f8d212633389c91f7fba99ba993f0a2", size = 30144 }, + { url = "https://files.pythonhosted.org/packages/3e/50/5e36c1945bd83e780a37361fc1999fc4c5a59ecc10a373557fdf0e58eb1f/mmh3-4.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba245e94b8d54765e14c2d7b6214e832557e7856d5183bc522e17884cab2f45d", size = 69094 }, + { url = "https://files.pythonhosted.org/packages/70/c7/6ae37e7519a938226469476b84bcea2650e2a2cc7a848e6a206ea98ecee3/mmh3-4.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb04e2feeabaad6231e89cd43b3d01a4403579aa792c9ab6fdeef45cc58d4ec0", size = 72611 }, + { url = "https://files.pythonhosted.org/packages/5e/47/6613f69f57f1e5045e66b22fae9c2fb39ef754c455805d3917f6073e316e/mmh3-4.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e3b1a27def545ce11e36158ba5d5390cdbc300cfe456a942cc89d649cf7e3b2", size = 71462 }, + { url = "https://files.pythonhosted.org/packages/e0/0a/e423db18ce7b479c4b96381a112b443f0985c611de420f95c58a9f934080/mmh3-4.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce0ab79ff736d7044e5e9b3bfe73958a55f79a4ae672e6213e92492ad5e734d5", size = 66165 }, + { url = "https://files.pythonhosted.org/packages/4c/7b/bfeb68bee5bddc8baf7ef630b93edc0a533202d84eb076dbb6c77e7e5fd5/mmh3-4.1.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b02268be6e0a8eeb8a924d7db85f28e47344f35c438c1e149878bb1c47b1cd3", size = 68088 }, + { url = "https://files.pythonhosted.org/packages/d4/a6/b82e30143997c05776887f5177f724e3b714aa7e7346fbe2ec70f52abcd0/mmh3-4.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:deb887f5fcdaf57cf646b1e062d56b06ef2f23421c80885fce18b37143cba828", size = 76241 }, + { url = "https://files.pythonhosted.org/packages/6c/60/a3d5872cf7610fcb13e36c472476020c5cf217b23c092bad452eb7784407/mmh3-4.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99dd564e9e2b512eb117bd0cbf0f79a50c45d961c2a02402787d581cec5448d5", size = 74538 }, + { url = "https://files.pythonhosted.org/packages/f6/d5/742173a94c78f4edab71c04097f6f9150c47f8fd034d592f5f34a9444719/mmh3-4.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:08373082dfaa38fe97aa78753d1efd21a1969e51079056ff552e687764eafdfe", size = 81793 }, + { url = "https://files.pythonhosted.org/packages/d0/7a/a1db0efe7c67b761d83be3d50e35ef26628ef56b3b8bc776d07412ee8b16/mmh3-4.1.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:54b9c6a2ea571b714e4fe28d3e4e2db37abfd03c787a58074ea21ee9a8fd1740", size = 78217 }, + { url = "https://files.pythonhosted.org/packages/b3/78/1ff8da7c859cd09704e2f500588d171eda9688fcf6f29e028ef261262a16/mmh3-4.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a7b1edf24c69e3513f879722b97ca85e52f9032f24a52284746877f6a7304086", size = 77052 }, + { url = "https://files.pythonhosted.org/packages/ed/c7/cf16ace81fc9fbe54a75c914306252af26c6ea485366bb3b579bf6e3dbb8/mmh3-4.1.0-cp312-cp312-win32.whl", hash = "sha256:411da64b951f635e1e2284b71d81a5a83580cea24994b328f8910d40bed67276", size = 31277 }, + { url = "https://files.pythonhosted.org/packages/d2/0b/b3b1637dca9414451edf287fd91e667e7231d5ffd7498137fe011951fc0a/mmh3-4.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bebc3ecb6ba18292e3d40c8712482b4477abd6981c2ebf0e60869bd90f8ac3a9", size = 31318 }, + { url = "https://files.pythonhosted.org/packages/dd/6c/c0f06040c58112ccbd0df989055ede98f7c1a1f392dc6a3fc63ec6c124ec/mmh3-4.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:168473dd608ade6a8d2ba069600b35199a9af837d96177d3088ca91f2b3798e3", size = 30147 }, +] + +[[package]] +name = "monotonic" +version = "1.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/ca/8e91948b782ddfbd194f323e7e7d9ba12e5877addf04fb2bf8fca38e86ac/monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7", size = 7615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/67/7e8406a29b6c45be7af7740456f7f37025f0506ae2e05fb9009a53946860/monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c", size = 8154 }, +] + +[[package]] +name = "more-itertools" +version = "10.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/0d/ad6a82320cb8eba710fd0dceb0f678d5a1b58d67d03ae5be14874baa39e0/more-itertools-10.4.0.tar.gz", hash = "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923", size = 120755 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/0b/6a51175e1395774449fca317fb8861379b7a2d59be411b8cce3d19d6ce78/more_itertools-10.4.0-py3-none-any.whl", hash = "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27", size = 60935 }, +] + +[[package]] +name = "motor" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymongo", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/e3/f5244c84d7bdc149d99f9baa4313f197f7d14cfa1bfe1a6ac181e10cb3e2/motor-3.3.2.tar.gz", hash = "sha256:d2fc38de15f1c8058f389c1a44a4d4105c0405c48c061cd492a654496f7bc26a", size = 272583 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/9a/1a43a329dffbd1a631c52e64c1e9c036621afdfd7f42096ae4bf2de4132b/motor-3.3.2-py3-none-any.whl", hash = "sha256:6fe7e6f0c4f430b9e030b9d22549b732f7c2226af3ab71ecc309e4a1b7d19953", size = 70598 }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, +] + +[[package]] +name = "msal" +version = "1.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyjwt", extra = ["crypto"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/ce/45b9af8f43fbbf34d15162e1e39ce34b675c234c56638277cc05562b6dbf/msal-1.30.0.tar.gz", hash = "sha256:b4bf00850092e465157d814efa24a18f788284c9a479491024d62903085ea2fb", size = 142510 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/82/8f19334da43b7ef72d995587991a446f140346d76edb96a2c1a2689588e9/msal-1.30.0-py3-none-any.whl", hash = "sha256:423872177410cb61683566dc3932db7a76f661a5d2f6f52f02a047f101e1c1de", size = 111760 }, +] + +[[package]] +name = "msal-extensions" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msal", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "portalocker", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/38/ad49272d0a5af95f7a0cb64a79bbd75c9c187f3b789385a143d8d537a5eb/msal_extensions-1.2.0.tar.gz", hash = "sha256:6f41b320bfd2933d631a215c91ca0dd3e67d84bd1a2f50ce917d5874ec646bef", size = 22391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/69/314d887a01599669fb330da14e5c6ff5f138609e322812a942a74ef9b765/msal_extensions-1.2.0-py3-none-any.whl", hash = "sha256:cf5ba83a2113fa6dc011a254a72f1c223c88d7dfad74cc30617c4679a417704d", size = 19254 }, +] + +[[package]] +name = "multidict" +version = "6.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/79/722ca999a3a09a63b35aac12ec27dfa8e5bb3a38b0f857f7a1a209a88836/multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da", size = 59867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/36/48097b96135017ed1b806c5ea27b6cdc2ed3a6861c5372b793563206c586/multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9", size = 50955 }, + { url = "https://files.pythonhosted.org/packages/d9/48/037440edb5d4a1c65e002925b2f24071d6c27754e6f4734f63037e3169d6/multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604", size = 30361 }, + { url = "https://files.pythonhosted.org/packages/a4/eb/d8e7693c9064554a1585698d1902839440c6c695b0f53c9a8be5d9d4a3b8/multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600", size = 30508 }, + { url = "https://files.pythonhosted.org/packages/f3/7d/fe7648d4b2f200f8854066ce6e56bf51889abfaf859814c62160dd0e32a9/multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c", size = 126318 }, + { url = "https://files.pythonhosted.org/packages/8d/ea/0230b6faa9a5bc10650fd50afcc4a86e6c37af2fe05bc679b74d79253732/multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5", size = 133998 }, + { url = "https://files.pythonhosted.org/packages/36/6d/d2f982fb485175727a193b4900b5f929d461e7aa87d6fb5a91a377fcc9c0/multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f", size = 129150 }, + { url = "https://files.pythonhosted.org/packages/33/62/2c9085e571318d51212a6914566fe41dd0e33d7f268f7e2f23dcd3f06c56/multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae", size = 124266 }, + { url = "https://files.pythonhosted.org/packages/ce/e2/88cdfeaf03eab3498f688a19b62ca704d371cd904cb74b682541ca7b20a7/multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182", size = 116637 }, + { url = "https://files.pythonhosted.org/packages/12/4d/99dfc36872dcc53956879f5da80a6505bbd29214cce90ce792a86e15fddf/multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf", size = 155908 }, + { url = "https://files.pythonhosted.org/packages/c2/5c/1e76b2c742cb9e0248d1e8c4ed420817879230c833fa27d890b5fd22290b/multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442", size = 147111 }, + { url = "https://files.pythonhosted.org/packages/bc/84/9579004267e1cc5968ef2ef8718dab9d8950d99354d85b739dd67b09c273/multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a", size = 160502 }, + { url = "https://files.pythonhosted.org/packages/11/b7/bef33e84e3722bc42531af020d7ae8c31235ce8846bacaa852b6484cf868/multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef", size = 156587 }, + { url = "https://files.pythonhosted.org/packages/26/ce/f745a2d6104e56f7fa0d7d0756bb9ed27b771dd7b8d9d7348cd7f0f7b9de/multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc", size = 151948 }, + { url = "https://files.pythonhosted.org/packages/f1/50/714da64281d2b2b3b4068e84f115e1ef3bd3ed3715b39503ff3c59e8d30d/multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319", size = 25734 }, + { url = "https://files.pythonhosted.org/packages/ef/3d/ba0dc18e96c5d83731c54129819d5892389e180f54ebb045c6124b2e8b87/multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8", size = 28182 }, + { url = "https://files.pythonhosted.org/packages/5f/da/b10ea65b850b54f44a6479177c6987f456bc2d38f8dc73009b78afcf0ede/multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba", size = 50815 }, + { url = "https://files.pythonhosted.org/packages/21/db/3403263f158b0bc7b0d4653766d71cb39498973f2042eead27b2e9758782/multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e", size = 30269 }, + { url = "https://files.pythonhosted.org/packages/02/c1/b15ecceb6ffa5081ed2ed450aea58d65b0e0358001f2b426705f9f41f4c2/multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd", size = 30500 }, + { url = "https://files.pythonhosted.org/packages/3f/e1/7fdd0f39565df3af87d6c2903fb66a7d529fbd0a8a066045d7a5b6ad1145/multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3", size = 130751 }, + { url = "https://files.pythonhosted.org/packages/76/bc/9f593f9e38c6c09bbf0344b56ad67dd53c69167937c2edadee9719a5e17d/multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf", size = 138185 }, + { url = "https://files.pythonhosted.org/packages/28/32/d7799a208701d537b92705f46c777ded812a6dc139c18d8ed599908f6b1c/multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29", size = 133585 }, + { url = "https://files.pythonhosted.org/packages/52/ec/be54a3ad110f386d5bd7a9a42a4ff36b3cd723ebe597f41073a73ffa16b8/multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed", size = 128684 }, + { url = "https://files.pythonhosted.org/packages/36/e1/a680eabeb71e25d4733276d917658dfa1cd3a99b1223625dbc247d266c98/multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733", size = 120994 }, + { url = "https://files.pythonhosted.org/packages/ef/08/08f4f44a8a43ea4cee13aa9cdbbf4a639af8db49310a0637ca389c4cf817/multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f", size = 159689 }, + { url = "https://files.pythonhosted.org/packages/aa/a9/46cdb4cb40bbd4b732169413f56b04a6553460b22bd914f9729c9ba63761/multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4", size = 150611 }, + { url = "https://files.pythonhosted.org/packages/e9/32/35668bb3e6ab2f12f4e4f7f4000f72f714882a94f904d4c3633fbd036753/multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1", size = 164444 }, + { url = "https://files.pythonhosted.org/packages/fa/10/f1388a91552af732d8ec48dab928abc209e732767e9e8f92d24c3544353c/multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc", size = 160158 }, + { url = "https://files.pythonhosted.org/packages/14/c3/f602601f1819983e018156e728e57b3f19726cb424b543667faab82f6939/multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e", size = 156072 }, + { url = "https://files.pythonhosted.org/packages/82/a6/0290af8487326108c0d03d14f8a0b8b1001d71e4494df5f96ab0c88c0b88/multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c", size = 25731 }, + { url = "https://files.pythonhosted.org/packages/88/aa/ea217cb18325aa05cb3e3111c19715f1e97c50a4a900cbc20e54648de5f5/multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea", size = 28176 }, + { url = "https://files.pythonhosted.org/packages/90/9c/7fda9c0defa09538c97b1f195394be82a1f53238536f70b32eb5399dfd4e/multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e", size = 49575 }, + { url = "https://files.pythonhosted.org/packages/be/21/d6ca80dd1b9b2c5605ff7475699a8ff5dc6ea958cd71fb2ff234afc13d79/multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b", size = 29638 }, + { url = "https://files.pythonhosted.org/packages/9c/18/9565f32c19d186168731e859692dfbc0e98f66a1dcf9e14d69c02a78b75a/multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5", size = 29874 }, + { url = "https://files.pythonhosted.org/packages/4e/4e/3815190e73e6ef101b5681c174c541bf972a1b064e926e56eea78d06e858/multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450", size = 129914 }, + { url = "https://files.pythonhosted.org/packages/0c/08/bb47f886457e2259aefc10044e45c8a1b62f0c27228557e17775869d0341/multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496", size = 134589 }, + { url = "https://files.pythonhosted.org/packages/d5/2f/952f79b5f0795cf4e34852fc5cf4dfda6166f63c06c798361215b69c131d/multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a", size = 133259 }, + { url = "https://files.pythonhosted.org/packages/24/1f/af976383b0b772dd351210af5b60ff9927e3abb2f4a103e93da19a957da0/multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226", size = 130779 }, + { url = "https://files.pythonhosted.org/packages/fc/b1/b0a7744be00b0f5045c7ed4e4a6b8ee6bde4672b2c620474712299df5979/multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271", size = 120125 }, + { url = "https://files.pythonhosted.org/packages/d0/bf/2a1d667acf11231cdf0b97a6cd9f30e7a5cf847037b5cf6da44884284bd0/multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb", size = 167095 }, + { url = "https://files.pythonhosted.org/packages/5e/e8/ad6ee74b1a2050d3bc78f566dabcc14c8bf89cbe87eecec866c011479815/multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef", size = 155823 }, + { url = "https://files.pythonhosted.org/packages/45/7c/06926bb91752c52abca3edbfefac1ea90d9d1bc00c84d0658c137589b920/multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24", size = 170233 }, + { url = "https://files.pythonhosted.org/packages/3c/29/3dd36cf6b9c5abba8b97bba84eb499a168ba59c3faec8829327b3887d123/multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6", size = 169035 }, + { url = "https://files.pythonhosted.org/packages/60/47/9a0f43470c70bbf6e148311f78ef5a3d4996b0226b6d295bdd50fdcfe387/multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda", size = 166229 }, + { url = "https://files.pythonhosted.org/packages/1d/23/c1b7ae7a0b8a3e08225284ef3ecbcf014b292a3ee821bc4ed2185fd4ce7d/multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5", size = 25840 }, + { url = "https://files.pythonhosted.org/packages/4a/68/66fceb758ad7a88993940dbdf3ac59911ba9dc46d7798bf6c8652f89f853/multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556", size = 27905 }, + { url = "https://files.pythonhosted.org/packages/fa/a2/17e1e23c6be0a916219c5292f509360c345b5fa6beeb50d743203c27532c/multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7", size = 9729 }, +] + +[[package]] +name = "mypy" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/9c/a4b3bda53823439cf395db8ecdda6229a83f9bf201714a68a15190bb2919/mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08", size = 3078369 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/ba/858cc9631c24a349c1c63814edc16448da7d6b8716b2c83a10aa20f5ee89/mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c", size = 10937885 }, + { url = "https://files.pythonhosted.org/packages/2d/88/2ae81f7489da8313d0f2043dd657ba847650b00a0fb8e07f40e716ed8c58/mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411", size = 10111978 }, + { url = "https://files.pythonhosted.org/packages/df/4b/d211d6036366f9ea5ee9fb949e80d133b4b8496cdde78c7119f518c49734/mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03", size = 12498441 }, + { url = "https://files.pythonhosted.org/packages/94/d2/973278d03ad11e006d71d4c858bfe45cf571ae061f3997911925c70a59f0/mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4", size = 13020595 }, + { url = "https://files.pythonhosted.org/packages/0b/c2/7f4285eda528883c5c34cb4b8d88080792967f7f7f24256ad8090d303702/mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58", size = 9568307 }, + { url = "https://files.pythonhosted.org/packages/0b/b1/62d8ce619493a5364dda4f410912aa12c27126926e8fb8393edca0664640/mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5", size = 10858723 }, + { url = "https://files.pythonhosted.org/packages/fe/aa/2ad15a318bc6a17b7f23e1641a624603949904f6131e09681f40340fb875/mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca", size = 10038078 }, + { url = "https://files.pythonhosted.org/packages/4d/7f/77feb389d91603f55b3c4e3e16ccf8752bce007ed73ca921e42c9a5dff12/mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de", size = 12420213 }, + { url = "https://files.pythonhosted.org/packages/bc/5b/907b4681f68e7ee2e2e88eed65c514cf6406b8f2f83b243ea79bd4eddb97/mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809", size = 12898278 }, + { url = "https://files.pythonhosted.org/packages/5b/b3/2a83be637825d7432b8e6a51e45d02de4f463b6c7ec7164a45009a7cf477/mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72", size = 9564438 }, + { url = "https://files.pythonhosted.org/packages/3a/34/69638cee2e87303f19a0c35e80d42757e14d9aba328f272fdcdc0bf3c9b8/mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8", size = 10995789 }, + { url = "https://files.pythonhosted.org/packages/c4/3c/3e0611348fc53a4a7c80485959478b4f6eae706baf3b7c03cafa22639216/mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a", size = 10002696 }, + { url = "https://files.pythonhosted.org/packages/1c/21/a6b46c91b4c9d1918ee59c305f46850cde7cbea748635a352e7c3c8ed204/mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417", size = 12505772 }, + { url = "https://files.pythonhosted.org/packages/c4/55/07904d4c8f408e70308015edcbff067eaa77514475938a9dd81b063de2a8/mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e", size = 12954190 }, + { url = "https://files.pythonhosted.org/packages/1e/b7/3a50f318979c8c541428c2f1ee973cda813bcc89614de982dafdd0df2b3e/mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525", size = 9663138 }, + { url = "https://files.pythonhosted.org/packages/f8/d4/4960d0df55f30a7625d9c3c9414dfd42f779caabae137ef73ffaed0c97b9/mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54", size = 2619257 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nbclient" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jupyter-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nbformat", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/d2/39bc36604f24bccd44d374ac34769bc58c53a1da5acd1e83f0165aa4940e/nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09", size = 62246 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/e8/00517a23d3eeaed0513e718fbc94aab26eaa1758f5690fc8578839791c79/nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f", size = 25318 }, +] + +[[package]] +name = "nbconvert" +version = "7.16.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "defusedxml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jupyter-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jupyterlab-pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "markupsafe", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "mistune", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nbclient", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nbformat", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pandocfilters", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tinycss2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/e8/ba521a033b21132008e520c28ceb818f9f092da5f0261e94e509401b29f9/nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4", size = 854422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/bb/bb5b6a515d1584aa2fd89965b11db6632e4bdc69495a52374bcc36e56cfa/nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3", size = 257388 }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jsonschema", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jupyter-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454 }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, +] + +[[package]] +name = "networkx" +version = "3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/e6/b164f94c869d6b2c605b5128b7b0cfe912795a87fc90e78533920001f3ec/networkx-3.3.tar.gz", hash = "sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9", size = 2126579 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/e9/5f72929373e1a0e8d142a130f3f97e6ff920070f87f91c4e13e40e0fba5a/networkx-3.3-py3-none-any.whl", hash = "sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2", size = 1702396 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "numpy" +version = "1.25.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/41/8f53eff8e969dd8576ddfb45e7ed315407d27c7518ae49418be8ed532b07/numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760", size = 10805282 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/50/8aedb5ff1460e7c8527af15c6326115009e7c270ec705487155b779ebabb/numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3", size = 20814934 }, + { url = "https://files.pythonhosted.org/packages/c3/ea/1d95b399078ecaa7b5d791e1fdbb3aee272077d9fd5fb499593c87dec5ea/numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f", size = 13994425 }, + { url = "https://files.pythonhosted.org/packages/b1/39/3f88e2bfac1fb510c112dc0c78a1e7cad8f3a2d75e714d1484a044c56682/numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187", size = 14167163 }, + { url = "https://files.pythonhosted.org/packages/71/3c/3b1981c6a1986adc9ee7db760c0c34ea5b14ac3da9ecfcf1ea2a4ec6c398/numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357", size = 18219190 }, + { url = "https://files.pythonhosted.org/packages/73/6f/2a0d0ad31a588d303178d494787f921c246c6234eccced236866bc1beaa5/numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9", size = 18068385 }, + { url = "https://files.pythonhosted.org/packages/63/bd/a1c256cdea5d99e2f7e1acc44fc287455420caeb2e97d43ff0dda908fae8/numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044", size = 12661360 }, + { url = "https://files.pythonhosted.org/packages/b7/db/4d37359e2c9cf8bf071c08b8a6f7374648a5ab2e76e2e22e3b808f81d507/numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545", size = 15554633 }, + { url = "https://files.pythonhosted.org/packages/c9/57/3cb8131a0e6d559501e088d3e685f4122e9ff9104c4b63e4dfd3a577b491/numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418", size = 20801693 }, + { url = "https://files.pythonhosted.org/packages/86/a1/b8ef999c32f26a97b5f714887e21f96c12ae99a38583a0a96e65283ac0a1/numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f", size = 14004130 }, + { url = "https://files.pythonhosted.org/packages/50/67/3e966d99a07d60a21a21d7ec016e9e4c2642a86fea251ec68677daf71d4d/numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2", size = 14158219 }, + { url = "https://files.pythonhosted.org/packages/32/6a/65dbc57a89078af9ff8bfcd4c0761a50172d90192eaeb1b6f56e5fbf1c3d/numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf", size = 18209344 }, + { url = "https://files.pythonhosted.org/packages/cd/fe/e900cb2ebafae04b7570081cefc65b6fdd9e202b9b353572506cea5cafdf/numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364", size = 18072378 }, + { url = "https://files.pythonhosted.org/packages/5c/e4/990c6cb09f2cd1a3f53bcc4e489dad903faa01b058b625d84bb62d2e9391/numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d", size = 12654351 }, + { url = "https://files.pythonhosted.org/packages/72/b2/02770e60c4e2f7e158d923ab0dea4e9f146a2dbf267fec6d8dc61d475689/numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4", size = 15546748 }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'linux'", + "python_full_version >= '3.13' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468 }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411 }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016 }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889 }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746 }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620 }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659 }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905 }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554 }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127 }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994 }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005 }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297 }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567 }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812 }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913 }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901 }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868 }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109 }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613 }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172 }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643 }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803 }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754 }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.1.3.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/6d/121efd7382d5b0284239f4ab1fc1590d86d34ed4a4a2fdb13b30ca8e5740/nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728", size = 410594774 }, + { url = "https://files.pythonhosted.org/packages/c5/ef/32a375b74bea706c93deea5613552f7c9104f961b21df423f5887eca713b/nvidia_cublas_cu12-12.1.3.1-py3-none-win_amd64.whl", hash = "sha256:2b964d60e8cf11b5e1073d179d85fa340c120e99b3067558f3cf98dd69d02906", size = 439918445 }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/00/6b218edd739ecfc60524e585ba8e6b00554dd908de2c9c66c1af3e44e18d/nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e", size = 14109015 }, + { url = "https://files.pythonhosted.org/packages/d0/56/0021e32ea2848c24242f6b56790bd0ccc8bf99f973ca790569c6ca028107/nvidia_cuda_cupti_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:bea8236d13a0ac7190bd2919c3e8e6ce1e402104276e6f9694479e48bb0eb2a4", size = 10154340 }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/9f/c64c03f49d6fbc56196664d05dba14e3a561038a81a638eeb47f4d4cfd48/nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2", size = 23671734 }, + { url = "https://files.pythonhosted.org/packages/ad/1d/f76987c4f454eb86e0b9a0e4f57c3bf1ac1d13ad13cd1a4da4eb0e0c0ce9/nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:0a98a522d9ff138b96c010a65e145dc1b4850e9ecb75a0172371793752fd46ed", size = 19331863 }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d5/c68b1d2cdfcc59e72e8a5949a37ddb22ae6cade80cd4a57a84d4c8b55472/nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40", size = 823596 }, + { url = "https://files.pythonhosted.org/packages/9f/e2/7a2b4b5064af56ea8ea2d8b2776c0f2960d95c88716138806121ae52a9c9/nvidia_cuda_runtime_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:dfb46ef84d73fababab44cf03e3b83f80700d27ca300e537f85f636fac474344", size = 821226 }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "8.9.2.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/74/a2e2be7fb83aaedec84f391f082cf765dfb635e7caa9b49065f73e4835d8/nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9", size = 731725872 }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.0.2.54" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/94/eb540db023ce1d162e7bea9f8f5aa781d57c65aed513c33ee9a5123ead4d/nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56", size = 121635161 }, + { url = "https://files.pythonhosted.org/packages/f7/57/7927a3aa0e19927dfed30256d1c854caf991655d847a4e7c01fe87e3d4ac/nvidia_cufft_cu12-11.0.2.54-py3-none-win_amd64.whl", hash = "sha256:d9ac353f78ff89951da4af698f80870b1534ed69993f10a4cf1d96f21357e253", size = 121344196 }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.2.106" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/31/4890b1c9abc496303412947fc7dcea3d14861720642b49e8ceed89636705/nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0", size = 56467784 }, + { url = "https://files.pythonhosted.org/packages/5c/97/4c9c7c79efcdf5b70374241d48cf03b94ef6707fd18ea0c0f53684931d0b/nvidia_curand_cu12-10.3.2.106-py3-none-win_amd64.whl", hash = "sha256:75b6b0c574c0037839121317e17fd01f8a69fd2ef8e25853d826fec30bdba74a", size = 55995813 }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.4.5.107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928 }, + { url = "https://files.pythonhosted.org/packages/b8/80/8fca0bf819122a631c3976b6fc517c1b10741b643b94046bd8dd451522c5/nvidia_cusolver_cu12-11.4.5.107-py3-none-win_amd64.whl", hash = "sha256:74e0c3a24c78612192a74fcd90dd117f1cf21dea4822e66d89e8ea80e3cd2da5", size = 121643081 }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.1.0.106" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278 }, + { url = "https://files.pythonhosted.org/packages/0f/95/48fdbba24c93614d1ecd35bc6bdc6087bd17cbacc3abc4b05a9c2a1ca232/nvidia_cusparse_cu12-12.1.0.106-py3-none-win_amd64.whl", hash = "sha256:b798237e81b9719373e8fae8d4f091b70a0cf09d9d85c95a557e11df2d8e9a5a", size = 195414588 }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.19.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/00/d0d4e48aef772ad5aebcf70b73028f88db6e5640b36c38e90445b7a57c45/nvidia_nccl_cu12-2.19.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:a9734707a2c96443331c1e48c717024aa6678a0e2a4cb66b2c364d18cee6b48d", size = 165987969 }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.6.20" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/b3/e456a1b2d499bb84bdc6670bfbcf41ff3bac58bd2fae6880d62834641558/nvidia_nvjitlink_cu12-12.6.20-py3-none-manylinux2014_aarch64.whl", hash = "sha256:84fb38465a5bc7c70cbc320cfd0963eb302ee25a5e939e9f512bbba55b6072fb", size = 19252608 }, + { url = "https://files.pythonhosted.org/packages/59/65/7ff0569494fbaea45ad2814972cc88da843d53cc96eb8554fcd0908941d9/nvidia_nvjitlink_cu12-12.6.20-py3-none-manylinux2014_x86_64.whl", hash = "sha256:562ab97ea2c23164823b2a89cb328d01d45cb99634b8c65fe7cd60d14562bd79", size = 19724950 }, + { url = "https://files.pythonhosted.org/packages/cb/ef/8f96c82e1cfcf6d5b770f7b043c3cc24841fc247b37629a7cc643dbf72a1/nvidia_nvjitlink_cu12-12.6.20-py3-none-win_amd64.whl", hash = "sha256:ed3c43a17f37b0c922a919203d2d36cbef24d41cc3e6b625182f8b58203644f6", size = 162012830 }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/d3/8057f0587683ed2fcd4dbfbdfdfa807b9160b809976099d36b8f60d08f03/nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5", size = 99138 }, + { url = "https://files.pythonhosted.org/packages/b8/d7/bd7cb2d95ac6ac6e8d05bfa96cdce69619f1ef2808e072919044c2d47a8c/nvidia_nvtx_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:65f4d98982b31b60026e0e6de73fbdfc09d08a96f4656dd3665ca616a11e1e82", size = 66307 }, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, +] + +[[package]] +name = "ollama" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/4e/fc7ad9232c251b4885b1bf2e0f9ce35882e0f167a6ce7d3d15473dc07e7d/ollama-0.3.1.tar.gz", hash = "sha256:032572fb494a4fba200c65013fe937a65382c846b5f358d9e8918ecbc9ac44b5", size = 10033 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/25/c3442864bd77621809a208a483b0857f8d6444b7a67906b58b9dcddd1574/ollama-0.3.1-py3-none-any.whl", hash = "sha256:db50034c73d6350349bdfba19c3f0d54a3cea73eb97b35f9d7419b2fc7206454", size = 10028 }, +] + +[[package]] +name = "onnxruntime" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "flatbuffers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "sympy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/5d/7022b1506c68f1a29118130c19c320cd75129a6cae1445c3fe0093dd992c/onnxruntime-1.19.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6ce22a98dfec7b646ae305f52d0ce14a189a758b02ea501860ca719f4b0ae04b", size = 16775785 }, + { url = "https://files.pythonhosted.org/packages/64/98/8789df3b25caf732cf215a22ac80f2e45801394e8f5403c45eb24939fb21/onnxruntime-1.19.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19019c72873f26927aa322c54cf2bf7312b23451b27451f39b88f57016c94f8b", size = 11498421 }, + { url = "https://files.pythonhosted.org/packages/47/ff/8e3831e9a780be2235f6505e8cd9fb6acd7ba48d16dab7061281fc3b49e9/onnxruntime-1.19.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8eaa16df99171dc636e30108d15597aed8c4c2dd9dbfdd07cc464d57d73fb275", size = 13169357 }, + { url = "https://files.pythonhosted.org/packages/99/29/38324e534756280c68250ac178264fa33bc600523c236108c5bd0149a3ee/onnxruntime-1.19.0-cp310-cp310-win32.whl", hash = "sha256:0eb0f8dbe596fd0f4737fe511fdbb17603853a7d204c5b2ca38d3c7808fc556b", size = 9589434 }, + { url = "https://files.pythonhosted.org/packages/9d/e7/9eed7292c62c96f1acf201c3b039d9d867b54671cf2894d011619f58b0b5/onnxruntime-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:616092d54ba8023b7bc0a5f6d900a07a37cc1cfcc631873c15f8c1d6e9e184d4", size = 11083142 }, + { url = "https://files.pythonhosted.org/packages/80/16/fc200316725d04731d8ffc5d2105887a1e400d760b0c7fd464744335cd29/onnxruntime-1.19.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a2b53b3c287cd933e5eb597273926e899082d8c84ab96e1b34035764a1627e17", size = 16778356 }, + { url = "https://files.pythonhosted.org/packages/cc/3c/ff2ecf2a842822bc5e9758747bdfd4163c53af470421f07afd6cba1ced7d/onnxruntime-1.19.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e94984663963e74fbb468bde9ec6f19dcf890b594b35e249c4dc8789d08993c5", size = 11492628 }, + { url = "https://files.pythonhosted.org/packages/fa/ca/769da06e76b14a315a1effa5b01d906963379495cd82c00b5023be4c3e61/onnxruntime-1.19.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f379d1f050cfb55ce015d53727b78ee362febc065c38eed81512b22b757da73", size = 13172071 }, + { url = "https://files.pythonhosted.org/packages/75/7c/5a7e3fd98f9af3c43d6073c38afff8c18d201a72d1eba77c93dd230b8501/onnxruntime-1.19.0-cp311-cp311-win32.whl", hash = "sha256:4ccb48faea02503275ae7e79e351434fc43c294c4cb5c4d8bcb7479061396614", size = 9589924 }, + { url = "https://files.pythonhosted.org/packages/78/86/fd21288f9e4096d9c27bd0f221cb61719baa97d5e187549a9f0e84e386ae/onnxruntime-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:9cdc8d311289a84e77722de68bd22b8adfb94eea26f4be6f9e017350faac8b18", size = 11083172 }, + { url = "https://files.pythonhosted.org/packages/d1/3c/7cd126254658f0371fadf8651957387d7f743b1b85545e3b783a7f717215/onnxruntime-1.19.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1b59eaec1be9a8613c5fdeaafe67f73a062edce3ac03bbbdc9e2d98b58a30617", size = 16789643 }, + { url = "https://files.pythonhosted.org/packages/bf/6e/aae5420a45cbbcacef4c65f70067c11bed7cbb8fda12e0728f37d29746e5/onnxruntime-1.19.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be4144d014a4b25184e63ce7a463a2e7796e2f3df931fccc6a6aefa6f1365dc5", size = 11483896 }, + { url = "https://files.pythonhosted.org/packages/e6/0f/ad2ec6d490d9cb4ea82dd46382396827cb8ca9a469a56368fc7ef2fb52a4/onnxruntime-1.19.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10d7e7d4ca7021ce7f29a66dbc6071addf2de5839135339bd855c6d9c2bba371", size = 13177713 }, + { url = "https://files.pythonhosted.org/packages/de/4e/059cae46e48d183ac9b1d0be7ece1c5878711f4a31a206a9dcb34a89e3f5/onnxruntime-1.19.0-cp312-cp312-win32.whl", hash = "sha256:87f2c58b577a1fb31dc5d92b647ecc588fd5f1ea0c3ad4526f5f80a113357c8d", size = 9591661 }, + { url = "https://files.pythonhosted.org/packages/a0/ed/7ac157855cd2135ba894836ce4d027830b78d71832c9e658046e5b1b3d23/onnxruntime-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a1f50d49676d7b69566536ff039d9e4e95fc482a55673719f46528218ecbb94", size = 11084335 }, +] + +[[package]] +name = "openai" +version = "1.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "distro", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jiter", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "sniffio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/1f/310b0b5efb6178ad9f9ca4a80b2ead3cb7cbc16a1b843941bcf1c52dd884/openai-1.42.0.tar.gz", hash = "sha256:c9d31853b4e0bc2dc8bd08003b462a006035655a701471695d0bfdc08529cde3", size = 290549 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/9e/d77569d06e365f093977d94f305a395b7ac5ccd746016a2e8dd34c4e20c1/openai-1.42.0-py3-none-any.whl", hash = "sha256:dc91e0307033a4f94931e5d03cc3b29b9717014ad5e73f9f2051b6cb5eda4d80", size = 362858 }, +] + +[[package]] +name = "openapi-core" +version = "0.19.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jsonschema", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jsonschema-path", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "more-itertools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "openapi-schema-validator", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "openapi-spec-validator", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "parse", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "werkzeug", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/34/26eef886b9a9470952ab248b961fea29e23c9fd5e5083371c1f7f0aa4443/openapi_core-0.19.3.tar.gz", hash = "sha256:5db6479ecccf76c52422961dc42b411b7625a802087d847251fdd66f0392b095", size = 109026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/08/7ed984041e003113c648583c6f3ee5a88510f8d69901d64aa08acec5cc67/openapi_core-0.19.3-py3-none-any.whl", hash = "sha256:88c8be49b083a39923ada4c1269919ba119ab617c951f901757a054a483988b0", size = 103690 }, +] + +[[package]] +name = "openapi-schema-validator" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jsonschema-specifications", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "rfc3339-validator", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b2/7d5bdf2b26b6a95ebf4fbec294acaf4306c713f3a47c2453962511110248/openapi_schema_validator-0.6.2.tar.gz", hash = "sha256:11a95c9c9017912964e3e5f2545a5b11c3814880681fcacfb73b1759bb4f2804", size = 11860 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/dc/9aefae8891454130968ff079ece851d1ae9ccf6fb7965761f47c50c04853/openapi_schema_validator-0.6.2-py3-none-any.whl", hash = "sha256:c4887c1347c669eb7cded9090f4438b710845cd0f90d1fb9e1b3303fb37339f8", size = 8750 }, +] + +[[package]] +name = "openapi-spec-validator" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jsonschema-path", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "lazy-object-proxy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "openapi-schema-validator", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/fe/21954ff978239dc29ebb313f5c87eeb4ec929b694b9667323086730998e2/openapi_spec_validator-0.7.1.tar.gz", hash = "sha256:8577b85a8268685da6f8aa30990b83b7960d4d1117e901d451b5d572605e5ec7", size = 37985 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/4d/e744fff95aaf3aeafc968d5ba7297c8cda0d1ecb8e3acd21b25adae4d835/openapi_spec_validator-0.7.1-py3-none-any.whl", hash = "sha256:3c81825043f24ccbcd2f4b149b11e8231abce5ba84f37065e14ec947d8f4e959", size = 38998 }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "importlib-metadata", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/d4/e9a0ddef6eed086c96e8265d864a46da099611b7be153b0cfb63fd47e1b4/opentelemetry_api-1.26.0.tar.gz", hash = "sha256:2bd639e4bed5b18486fef0b5a520aaffde5a18fc225e808a1ac4df363f43a1ce", size = 60904 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/a7/6322d1d7a1fb926e8b99208c27730f21217da2f1e0e11dab48a78a0427a4/opentelemetry_api-1.26.0-py3-none-any.whl", hash = "sha256:7d7ea33adf2ceda2dd680b18b1677e4152000b37ca76e679da71ff103b943064", size = 61533 }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/cd/ed9eaa1d80facb6609d02af6c393b02ce3797a15742361be4859db6fdc17/opentelemetry_exporter_otlp_proto_common-1.26.0.tar.gz", hash = "sha256:bdbe50e2e22a1c71acaa0c8ba6efaadd58882e5a5978737a44a4c4b10d304c92", size = 17815 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/2f/0f7e0a73fd901c9abc6ea680d7f19a803dac830c450f21e1123d3a3ec488/opentelemetry_exporter_otlp_proto_common-1.26.0-py3-none-any.whl", hash = "sha256:ee4d8f8891a1b9c372abf8d109409e5b81947cf66423fd998e56880057afbc71", size = 17837 }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "googleapis-common-protos", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-exporter-otlp-proto-common", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-proto", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/23/cac89aca97ecb8f7498a875dc2ac89224b4f3345bcb8ffff643b59886196/opentelemetry_exporter_otlp_proto_grpc-1.26.0.tar.gz", hash = "sha256:a65b67a9a6b06ba1ec406114568e21afe88c1cdb29c464f2507d529eb906d8ae", size = 25239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/0c/e4473692fec8076008c7926dfcef7223fc6d2785f04ad9d8402347a4eba9/opentelemetry_exporter_otlp_proto_grpc-1.26.0-py3-none-any.whl", hash = "sha256:e2be5eff72ebcb010675b818e8d7c2e7d61ec451755b8de67a140bc49b9b0280", size = 18228 }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.47b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/9d/de2726729dbe5d210683245315ed5a20bf90465d1cc5e7f9cb0bee6673a6/opentelemetry_instrumentation-0.47b0.tar.gz", hash = "sha256:96f9885e450c35e3f16a4f33145f2ebf620aea910c9fd74a392bbc0f807a350f", size = 24516 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/6a/be31a84ddd13e9018fcca6885e4710f227eb0fd06eda1896da67287faa2e/opentelemetry_instrumentation-0.47b0-py3-none-any.whl", hash = "sha256:88974ee52b1db08fc298334b51c19d47e53099c33740e48c4f084bd1afd052d5", size = 29218 }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.47b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-util-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/a5/895c3810f27cdd3bdb02320df3489d2d33f158970d8447755deb7fc3fef7/opentelemetry_instrumentation_asgi-0.47b0.tar.gz", hash = "sha256:e78b7822c1bca0511e5e9610ec484b8994a81670375e570c76f06f69af7c506a", size = 23398 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/d9/c74cb6d69589cc97d856cb3f427dfcef37ec16f9564586290c9c075d9020/opentelemetry_instrumentation_asgi-0.47b0-py3-none-any.whl", hash = "sha256:b798dc4957b3edc9dfecb47a4c05809036a4b762234c5071212fda39ead80ade", size = 15946 }, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.47b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-instrumentation-asgi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-util-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/8f/c68dbef4be5db9330b0e9f492277b0dcdc8870d86de0c749b537406c590a/opentelemetry_instrumentation_fastapi-0.47b0.tar.gz", hash = "sha256:0c7c10b5d971e99a420678ffd16c5b1ea4f0db3b31b62faf305fbb03b4ebee36", size = 17332 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/29/a97842d6dfa679bf0f3624ce1ea3458eb185befd536cafe580daa9ab68ae/opentelemetry_instrumentation_fastapi-0.47b0-py3-none-any.whl", hash = "sha256:5ac28dd401160b02e4f544a85a9e4f61a8cbe5b077ea0379d411615376a2bd21", size = 11715 }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/06/9505ef04e527fa711ebffb47f3f56cac6015405953ff688fc349d170fb9c/opentelemetry_proto-1.26.0.tar.gz", hash = "sha256:c5c18796c0cab3751fc3b98dee53855835e90c0422924b484432ac852d93dc1e", size = 34749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/f4/66a3892eea913cded9bac0fdd3fb1a412fa2da8eb50014ec87a52648444a/opentelemetry_proto-1.26.0-py3-none-any.whl", hash = "sha256:6c4d7b4d4d9c88543bcf8c28ae3f8f0448a753dc291c18c5390444c90b76a725", size = 52466 }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/85/8ca0d5ebfe708287b091dffcd15553b74bbfe4532f8dd42662b78b2e0cab/opentelemetry_sdk-1.26.0.tar.gz", hash = "sha256:c90d2868f8805619535c05562d699e2f4fb1f00dbd55a86dcefca4da6fa02f85", size = 143139 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/f1/a9b550d0f9c049653dd2eab45cecf8fe4baa9795ed143d87834056ffabaf/opentelemetry_sdk-1.26.0-py3-none-any.whl", hash = "sha256:feb5056a84a88670c041ea0ded9921fca559efec03905dddeb3885525e0af897", size = 109475 }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.47b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/85/edef14d10ad00ddd9fffb20e4d3d938f4c5c1247e11a175066fe2b4a72f8/opentelemetry_semantic_conventions-0.47b0.tar.gz", hash = "sha256:a8d57999bbe3495ffd4d510de26a97dadc1dace53e0275001b2c1b2f67992a7e", size = 83994 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c2/ca5cef8e4cd8eec5a95deed95ec3f6005e499fd9d17ca08731ced03a6921/opentelemetry_semantic_conventions-0.47b0-py3-none-any.whl", hash = "sha256:4ff9d595b85a59c1c1413f02bba320ce7ea6bf9e2ead2b0913c4395c7bbc1063", size = 138027 }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.47b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/b5/fb15aafe7391b6a36f5cd9bcb9f6c3efaeb87a0626e4d2dfef12f66ebf3e/opentelemetry_util_http-0.47b0.tar.gz", hash = "sha256:352a07664c18eef827eb8ddcbd64c64a7284a39dd1655e2f16f577eb046ccb32", size = 7863 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/7e/98749e14a4e3f4db8bc016e6b42aba40e4d934baeb8767b8658a99d0dfac/opentelemetry_util_http-0.47b0-py3-none-any.whl", hash = "sha256:3d3215e09c4a723b12da6d0233a31395aeb2bb33a64d7b15a1500690ba250f19", size = 6946 }, +] + +[[package]] +name = "orjson" +version = "3.10.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/03/821c8197d0515e46ea19439f5c5d5fd9a9889f76800613cfac947b5d7845/orjson-3.10.7.tar.gz", hash = "sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3", size = 5056450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/12/60931cf808b9334f26210ab496442f4a7a3d66e29d1cf12e0a01857e756f/orjson-3.10.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12", size = 251312 }, + { url = "https://files.pythonhosted.org/packages/fe/0e/efbd0a2d25f8e82b230eb20b6b8424be6dd95b6811b669be9af16234b6db/orjson-3.10.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac", size = 148124 }, + { url = "https://files.pythonhosted.org/packages/dd/47/1ddff6e23fe5f4aeaaed996a3cde422b3eaac4558c03751723e106184c68/orjson-3.10.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7", size = 147277 }, + { url = "https://files.pythonhosted.org/packages/04/da/d03d72b54bdd60d05de372114abfbd9f05050946895140c6ff5f27ab8f49/orjson-3.10.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c", size = 152955 }, + { url = "https://files.pythonhosted.org/packages/7f/7e/ef8522dbba112af6cc52227dcc746dd3447c7d53ea8cea35740239b547ee/orjson-3.10.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9", size = 163955 }, + { url = "https://files.pythonhosted.org/packages/b6/bc/fbd345d771a73cacc5b0e774d034cd081590b336754c511f4ead9fdc4cf1/orjson-3.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91", size = 141896 }, + { url = "https://files.pythonhosted.org/packages/82/0a/1f09c12d15b1e83156b7f3f621561d38650fe5b8f39f38f04a64de1a87fc/orjson-3.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250", size = 170166 }, + { url = "https://files.pythonhosted.org/packages/a6/d8/eee30caba21a8d6a9df06d2519bb0ecd0adbcd57f2e79d360de5570031cf/orjson-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84", size = 167804 }, + { url = "https://files.pythonhosted.org/packages/44/fe/d1d89d3f15e343511417195f6ccd2bdeb7ebc5a48a882a79ab3bbcdf5fc7/orjson-3.10.7-cp310-none-win32.whl", hash = "sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175", size = 143010 }, + { url = "https://files.pythonhosted.org/packages/88/8c/0e7b8d5a523927774758ac4ce2de4d8ca5dda569955ba3aeb5e208344eda/orjson-3.10.7-cp310-none-win_amd64.whl", hash = "sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c", size = 137306 }, + { url = "https://files.pythonhosted.org/packages/89/c9/dd286c97c2f478d43839bd859ca4d9820e2177d4e07a64c516dc3e018062/orjson-3.10.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2", size = 251312 }, + { url = "https://files.pythonhosted.org/packages/b9/72/d90bd11e83a0e9623b3803b079478a93de8ec4316c98fa66110d594de5fa/orjson-3.10.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09", size = 148125 }, + { url = "https://files.pythonhosted.org/packages/9d/b6/ed61e87f327a4cbb2075ed0716e32ba68cb029aa654a68c3eb27803050d8/orjson-3.10.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0", size = 147278 }, + { url = "https://files.pythonhosted.org/packages/66/9f/e6a11b5d1ad11e9dc869d938707ef93ff5ed20b53d6cda8b5e2ac532a9d2/orjson-3.10.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a", size = 152954 }, + { url = "https://files.pythonhosted.org/packages/92/ee/702d5e8ccd42dc2b9d1043f22daa1ba75165616aa021dc19fb0c5a726ce8/orjson-3.10.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e", size = 163953 }, + { url = "https://files.pythonhosted.org/packages/d3/cb/55205f3f1ee6ba80c0a9a18ca07423003ca8de99192b18be30f1f31b4cdd/orjson-3.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6", size = 141895 }, + { url = "https://files.pythonhosted.org/packages/bb/ab/1185e472f15c00d37d09c395e478803ed0eae7a3a3d055a5f3885e1ea136/orjson-3.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6", size = 170169 }, + { url = "https://files.pythonhosted.org/packages/53/b9/10abe9089bdb08cd4218cc45eb7abfd787c82cf301cecbfe7f141542d7f4/orjson-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0", size = 167808 }, + { url = "https://files.pythonhosted.org/packages/8a/ad/26b40ccef119dcb0f4a39745ffd7d2d319152c1a52859b1ebbd114eca19c/orjson-3.10.7-cp311-none-win32.whl", hash = "sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f", size = 143010 }, + { url = "https://files.pythonhosted.org/packages/e7/63/5f4101e4895b78ada568f4cf8f870dd594139ca2e75e654e373da78b03b0/orjson-3.10.7-cp311-none-win_amd64.whl", hash = "sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5", size = 137307 }, + { url = "https://files.pythonhosted.org/packages/14/7c/b4ecc2069210489696a36e42862ccccef7e49e1454a3422030ef52881b01/orjson-3.10.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f", size = 251409 }, + { url = "https://files.pythonhosted.org/packages/60/84/e495edb919ef0c98d054a9b6d05f2700fdeba3886edd58f1c4dfb25d514a/orjson-3.10.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3", size = 147913 }, + { url = "https://files.pythonhosted.org/packages/c5/27/e40bc7d79c4afb7e9264f22320c285d06d2c9574c9c682ba0f1be3012833/orjson-3.10.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93", size = 147390 }, + { url = "https://files.pythonhosted.org/packages/30/be/fd646fb1a461de4958a6eacf4ecf064b8d5479c023e0e71cc89b28fa91ac/orjson-3.10.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313", size = 152973 }, + { url = "https://files.pythonhosted.org/packages/b1/00/414f8d4bc5ec3447e27b5c26b4e996e4ef08594d599e79b3648f64da060c/orjson-3.10.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864", size = 164039 }, + { url = "https://files.pythonhosted.org/packages/a0/6b/34e6904ac99df811a06e42d8461d47b6e0c9b86e2fe7ee84934df6e35f0d/orjson-3.10.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09", size = 142035 }, + { url = "https://files.pythonhosted.org/packages/17/7e/254189d9b6df89660f65aec878d5eeaa5b1ae371bd2c458f85940445d36f/orjson-3.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5", size = 169941 }, + { url = "https://files.pythonhosted.org/packages/02/1a/d11805670c29d3a1b29fc4bd048dc90b094784779690592efe8c9f71249a/orjson-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b", size = 167994 }, + { url = "https://files.pythonhosted.org/packages/20/5f/03d89b007f9d6733dc11bc35d64812101c85d6c4e9c53af9fa7e7689cb11/orjson-3.10.7-cp312-none-win32.whl", hash = "sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb", size = 143130 }, + { url = "https://files.pythonhosted.org/packages/c6/9d/9b9fb6c60b8a0e04031ba85414915e19ecea484ebb625402d968ea45b8d5/orjson-3.10.7-cp312-none-win_amd64.whl", hash = "sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1", size = 137326 }, + { url = "https://files.pythonhosted.org/packages/15/05/121af8a87513c56745d01ad7cf215c30d08356da9ad882ebe2ba890824cd/orjson-3.10.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149", size = 251331 }, + { url = "https://files.pythonhosted.org/packages/73/7f/8d6ccd64a6f8bdbfe6c9be7c58aeb8094aa52a01fbbb2cda42ff7e312bd7/orjson-3.10.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe", size = 142012 }, + { url = "https://files.pythonhosted.org/packages/04/65/f2a03fd1d4f0308f01d372e004c049f7eb9bc5676763a15f20f383fa9c01/orjson-3.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c", size = 169920 }, + { url = "https://files.pythonhosted.org/packages/e2/1c/3ef8d83d7c6a619ad3d69a4d5318591b4ce5862e6eda7c26bbe8208652ca/orjson-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad", size = 167916 }, + { url = "https://files.pythonhosted.org/packages/f2/0d/820a640e5a7dfbe525e789c70871ebb82aff73b0c7bf80082653f86b9431/orjson-3.10.7-cp313-none-win32.whl", hash = "sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2", size = 143089 }, + { url = "https://files.pythonhosted.org/packages/1a/72/a424db9116c7cad2950a8f9e4aeb655a7b57de988eb015acd0fcd1b4609b/orjson-3.10.7-cp313-none-win_amd64.whl", hash = "sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024", size = 137081 }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "pandas" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, + { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytz", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tzdata", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/d9/ecf715f34c73ccb1d8ceb82fc01cd1028a65a5f6dbc57bfa6ea155119058/pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54", size = 4398391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/2d/39600d073ea70b9cafdc51fab91d69c72b49dd92810f24cb5ac6631f387f/pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce", size = 12551798 }, + { url = "https://files.pythonhosted.org/packages/fd/4b/0cd38e68ab690b9df8ef90cba625bf3f93b82d1c719703b8e1b333b2c72d/pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238", size = 11287392 }, + { url = "https://files.pythonhosted.org/packages/01/c6/d3d2612aea9b9f28e79a30b864835dad8f542dcf474eee09afeee5d15d75/pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08", size = 15634823 }, + { url = "https://files.pythonhosted.org/packages/89/1b/12521efcbc6058e2673583bb096c2b5046a9df39bd73eca392c1efed24e5/pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0", size = 13032214 }, + { url = "https://files.pythonhosted.org/packages/e4/d7/303dba73f1c3a9ef067d23e5afbb6175aa25e8121be79be354dcc740921a/pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51", size = 16278302 }, + { url = "https://files.pythonhosted.org/packages/ba/df/8ff7c5ed1cc4da8c6ab674dc8e4860a4310c3880df1283e01bac27a4333d/pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99", size = 13892866 }, + { url = "https://files.pythonhosted.org/packages/69/a6/81d5dc9a612cf0c1810c2ebc4f2afddb900382276522b18d128213faeae3/pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772", size = 11621592 }, + { url = "https://files.pythonhosted.org/packages/1b/70/61704497903d43043e288017cb2b82155c0d41e15f5c17807920877b45c2/pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288", size = 12574808 }, + { url = "https://files.pythonhosted.org/packages/16/c6/75231fd47afd6b3f89011e7077f1a3958441264aca7ae9ff596e3276a5d0/pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151", size = 11304876 }, + { url = "https://files.pythonhosted.org/packages/97/2d/7b54f80b93379ff94afb3bd9b0cd1d17b48183a0d6f98045bc01ce1e06a7/pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b", size = 15602548 }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4d82be566f069d7a9a702dcdf6f9106df0e0b042e738043c0cc7ddd7e3f6/pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee", size = 13031332 }, + { url = "https://files.pythonhosted.org/packages/92/a2/b79c48f530673567805e607712b29814b47dcaf0d167e87145eb4b0118c6/pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db", size = 16286054 }, + { url = "https://files.pythonhosted.org/packages/40/c7/47e94907f1d8fdb4868d61bd6c93d57b3784a964d52691b77ebfdb062842/pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1", size = 13879507 }, + { url = "https://files.pythonhosted.org/packages/ab/63/966db1321a0ad55df1d1fe51505d2cdae191b84c907974873817b0a6e849/pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24", size = 11634249 }, + { url = "https://files.pythonhosted.org/packages/dd/49/de869130028fb8d90e25da3b7d8fb13e40f5afa4c4af1781583eb1ff3839/pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef", size = 12500886 }, + { url = "https://files.pythonhosted.org/packages/db/7c/9a60add21b96140e22465d9adf09832feade45235cd22f4cb1668a25e443/pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce", size = 11340320 }, + { url = "https://files.pythonhosted.org/packages/b0/85/f95b5f322e1ae13b7ed7e97bd999160fa003424711ab4dc8344b8772c270/pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad", size = 15204346 }, + { url = "https://files.pythonhosted.org/packages/40/10/79e52ef01dfeb1c1ca47a109a01a248754ebe990e159a844ece12914de83/pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad", size = 12733396 }, + { url = "https://files.pythonhosted.org/packages/35/9d/208febf8c4eb5c1d9ea3314d52d8bd415fd0ef0dd66bb24cc5bdbc8fa71a/pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76", size = 15858913 }, + { url = "https://files.pythonhosted.org/packages/99/d1/2d9bd05def7a9e08a92ec929b5a4c8d5556ec76fae22b0fa486cbf33ea63/pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32", size = 13417786 }, + { url = "https://files.pythonhosted.org/packages/22/a5/a0b255295406ed54269814bc93723cfd1a0da63fb9aaf99e1364f07923e5/pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23", size = 11498828 }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663 }, +] + +[[package]] +name = "parse" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126 }, +] + +[[package]] +name = "parso" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, +] + +[[package]] +name = "pathable" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/ed/e0e29300253b61dea3b7ec3a31f5d061d577c2a6fd1e35c5cfd0e6f2cd6d/pathable-0.4.3.tar.gz", hash = "sha256:5c869d315be50776cc8a993f3af43e0c60dc01506b399643f919034ebf4cdcab", size = 8679 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/0a/acfb251ba01009d3053f04f4661e96abf9d485266b04a0a4deebc702d9cb/pathable-0.4.3-py3-none-any.whl", hash = "sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14", size = 9587 }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, +] + +[[package]] +name = "pillow" +version = "10.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271 }, + { url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658 }, + { url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075 }, + { url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808 }, + { url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290 }, + { url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163 }, + { url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100 }, + { url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880 }, + { url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218 }, + { url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487 }, + { url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219 }, + { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265 }, + { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655 }, + { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304 }, + { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804 }, + { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126 }, + { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541 }, + { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616 }, + { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802 }, + { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213 }, + { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498 }, + { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219 }, + { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350 }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980 }, + { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799 }, + { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973 }, + { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054 }, + { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484 }, + { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375 }, + { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773 }, + { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690 }, + { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951 }, + { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427 }, + { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685 }, + { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883 }, + { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837 }, + { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562 }, + { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761 }, + { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767 }, + { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989 }, + { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255 }, + { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603 }, + { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972 }, + { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375 }, + { url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889 }, + { url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160 }, + { url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020 }, + { url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539 }, + { url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125 }, + { url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373 }, + { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661 }, +] + +[[package]] +name = "pinecone-client" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pinecone-plugin-inference", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pinecone-plugin-interface", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "urllib3", marker = "(python_full_version < '4.0' and sys_platform == 'darwin') or (python_full_version < '4.0' and sys_platform == 'linux') or (python_full_version < '4.0' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/739fe0a4a173658d541206ec7fdb0cc4c9ddc364de216af668b988bf0868/pinecone_client-5.0.1.tar.gz", hash = "sha256:11c33ff5d1c38a6ce69e69fe532c0f22f312fb28d761bb30b3767816d3181d64", size = 122207 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/d0/c64336b8f76e63296d04b885c545c0872ff070e6b2bc725dd0ff3ae681dc/pinecone_client-5.0.1-py3-none-any.whl", hash = "sha256:c8f7835e1045ba84e295f217a8e85573ffb80b41501bbc1af6d92c9631c567a7", size = 244818 }, +] + +[[package]] +name = "pinecone-plugin-inference" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pinecone-plugin-interface", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/40/7b2a88e68ede294dc293c6196a71f3d6583d403320a2008153b095cd3e39/pinecone_plugin_inference-1.0.3.tar.gz", hash = "sha256:c6519ba730123713a181c010f0db9d6449d11de451b8e79bec4efd662b096f41", size = 54372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/b7/0d57cad06545ac8fbb7a362dddaff01b0ecfe6e47c135345e94b3d8ab2ca/pinecone_plugin_inference-1.0.3-py3-none-any.whl", hash = "sha256:bbdfe5dba99a87374d9e3315b62b8e1bbca52d5fe069a64cd6b212efbc8b9afd", size = 117566 }, +] + +[[package]] +name = "pinecone-plugin-interface" +version = "0.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/fb/e8a4063264953ead9e2b24d9b390152c60f042c951c47f4592e9996e57ff/pinecone_plugin_interface-0.0.7.tar.gz", hash = "sha256:b8e6675e41847333aa13923cc44daa3f85676d7157324682dc1640588a982846", size = 3370 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/1d/a21fdfcd6d022cb64cef5c2a29ee6691c6c103c4566b41646b080b7536a5/pinecone_plugin_interface-0.0.7-py3-none-any.whl", hash = "sha256:875857ad9c9fc8bbc074dbe780d187a2afd21f5bfe0f3b08601924a61ef1bba8", size = 6249 }, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/52/0763d1d976d5c262df53ddda8d8d4719eedf9594d046f117c25a27261a19/platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3", size = 20916 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/13/2aa1f0e1364feb2c9ef45302f387ac0bd81484e9c9a4c5688a322fbdfd08/platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", size = 18146 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "portalocker" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "(platform_system == 'Windows' and sys_platform == 'darwin') or (platform_system == 'Windows' and sys_platform == 'linux') or (platform_system == 'Windows' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/d3/c6c64067759e87af98cc668c1cc75171347d0f1577fab7ca3749134e3cd4/portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f", size = 40891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/fb/a70a4214956182e0d7a9099ab17d50bfcba1056188e9b14f35b9e2b62a0d/portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf", size = 18423 }, +] + +[[package]] +name = "posthog" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "monotonic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/c8/8a7308d5355fedfc400098a75fd191cf615b55aa22ef2a937995326e6f5e/posthog-3.5.0.tar.gz", hash = "sha256:8f7e3b2c6e8714d0c0c542a2109b83a7549f63b7113a133ab2763a89245ef2ef", size = 38142 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/5f/24cb22118db0e11703b6b80ef9f982eadde21eb585c3a769719e48dce893/posthog-3.5.0-py2.py3-none-any.whl", hash = "sha256:3c672be7ba6f95d555ea207d4486c171d06657eb34b3ce25eb043bfe7b6b5b76", size = 41300 }, +] + +[[package]] +name = "prance" +version = "23.6.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chardet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "ruamel-yaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/f0/bcb5ffc8b7ab8e3d02dbef3bd945cf8fd6e12c146774f900659406b9fce1/prance-23.6.21.0.tar.gz", hash = "sha256:d8c15f8ac34019751cc4945f866d8d964d7888016d10de3592e339567177cabe", size = 2798776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/db/4fb4901ee61274d0ab97746461fc5f2637e5d73aa73f34ee28e941a699a1/prance-23.6.21.0-py3-none-any.whl", hash = "sha256:6a4276fa07ed9f22feda4331097d7503c4adc3097e46ffae97425f2c1026bd9f", size = 36279 }, +] + +[[package]] +name = "pre-commit" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "identify", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nodeenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "virtualenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/10/97ee2fa54dff1e9da9badbc5e35d0bbaef0776271ea5907eccf64140f72f/pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", size = 177815 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/92/caae8c86e94681b42c246f0bca35c059a2f0529e5b92619f6aba4cf7e7b6/pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f", size = 204643 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.47" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/6d/0279b119dafc74c1220420028d490c4399b790fc1256998666e3a341879f/prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360", size = 425859 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/23/22750c4b768f09386d1c3cc4337953e8936f48a888fa6dddfb669b2c9088/prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10", size = 386411 }, +] + +[[package]] +name = "proto-plus" +version = "1.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/fc/e9a65cd52c1330d8d23af6013651a0bc50b6d76bcbdf91fae7cd19c68f29/proto-plus-1.24.0.tar.gz", hash = "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445", size = 55942 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/6f/db31f0711c0402aa477257205ce7d29e86a75cb52cd19f7afb585f75cda0/proto_plus-1.24.0-py3-none-any.whl", hash = "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12", size = 50080 }, +] + +[[package]] +name = "protobuf" +version = "4.25.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ab/cb61a4b87b2e7e6c312dce33602bd5884797fd054e0e53205f1c27cf0f66/protobuf-4.25.4.tar.gz", hash = "sha256:0dc4a62cc4052a036ee2204d26fe4d835c62827c855c8a03f29fe6da146b380d", size = 380283 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/43/27b48d9040763b78177d3083e16c70dba6e3c3ee2af64b659f6332c2b06e/protobuf-4.25.4-cp310-abi3-win32.whl", hash = "sha256:db9fd45183e1a67722cafa5c1da3e85c6492a5383f127c86c4c4aa4845867dc4", size = 392409 }, + { url = "https://files.pythonhosted.org/packages/0c/d4/589d673ada9c4c62d5f155218d7ff7ac796efb9c6af95b0bd29d438ae16e/protobuf-4.25.4-cp310-abi3-win_amd64.whl", hash = "sha256:ba3d8504116a921af46499471c63a85260c1a5fc23333154a427a310e015d26d", size = 413398 }, + { url = "https://files.pythonhosted.org/packages/34/ca/bf85ffe3dd16f1f2aaa6c006da8118800209af3da160ae4d4f47500eabd9/protobuf-4.25.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:eecd41bfc0e4b1bd3fa7909ed93dd14dd5567b98c941d6c1ad08fdcab3d6884b", size = 394160 }, + { url = "https://files.pythonhosted.org/packages/68/1d/e8961af9a8e534d66672318d6b70ea8e3391a6b13e16a29b039e4a99c214/protobuf-4.25.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:4c8a70fdcb995dcf6c8966cfa3a29101916f7225e9afe3ced4395359955d3835", size = 293700 }, + { url = "https://files.pythonhosted.org/packages/ca/6c/cc7ab2fb3a4a7f07f211d8a7bbb76bba633eb09b148296dbd4281e217f95/protobuf-4.25.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:3319e073562e2515c6ddc643eb92ce20809f5d8f10fead3332f71c63be6a7040", size = 294612 }, + { url = "https://files.pythonhosted.org/packages/b5/95/0ba7f66934a0a798006f06fc3d74816da2b7a2bcfd9b98c53d26f684c89e/protobuf-4.25.4-py3-none-any.whl", hash = "sha256:bfbebc1c8e4793cfd58589acfb8a1026be0003e852b9da7db5a4285bde996978", size = 156464 }, +] + +[[package]] +name = "psutil" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/8c6872f7372eb6a6b2e4708b88419fb46b857f7a2e1892966b851cc79fc9/psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2", size = 508067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/66/78c9c3020f573c58101dc43a44f6855d01bbbd747e24da2f0c4491200ea3/psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35", size = 249766 }, + { url = "https://files.pythonhosted.org/packages/e1/3f/2403aa9558bea4d3854b0e5e567bc3dd8e9fbc1fc4453c0aa9aafeb75467/psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1", size = 253024 }, + { url = "https://files.pythonhosted.org/packages/0b/37/f8da2fbd29690b3557cca414c1949f92162981920699cd62095a984983bf/psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0", size = 250961 }, + { url = "https://files.pythonhosted.org/packages/35/56/72f86175e81c656a01c4401cd3b1c923f891b31fbcebe98985894176d7c9/psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0", size = 287478 }, + { url = "https://files.pythonhosted.org/packages/19/74/f59e7e0d392bc1070e9a70e2f9190d652487ac115bb16e2eff6b22ad1d24/psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd", size = 290455 }, + { url = "https://files.pythonhosted.org/packages/cd/5f/60038e277ff0a9cc8f0c9ea3d0c5eb6ee1d2470ea3f9389d776432888e47/psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132", size = 292046 }, + { url = "https://files.pythonhosted.org/packages/8b/20/2ff69ad9c35c3df1858ac4e094f20bd2374d33c8643cf41da8fd7cdcb78b/psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d", size = 253560 }, + { url = "https://files.pythonhosted.org/packages/73/44/561092313ae925f3acfaace6f9ddc4f6a9c748704317bad9c8c8f8a36a79/psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3", size = 257399 }, + { url = "https://files.pythonhosted.org/packages/7c/06/63872a64c312a24fb9b4af123ee7007a306617da63ff13bcc1432386ead7/psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0", size = 251988 }, +] + +[[package]] +name = "psycopg" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/8e/f176997fd790d3dce9fa0ca695391beaeee39af7ecd6d426c4c063cf6744/psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7", size = 155313 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/0f755db36f47f96464463385552f8f132a981731356837c9a30a11ab2d35/psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175", size = 197743 }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "(implementation_name != 'pypy' and sys_platform == 'darwin') or (implementation_name != 'pypy' and sys_platform == 'linux') or (implementation_name != 'pypy' and sys_platform == 'win32')" }, +] +pool = [ + { name = "psycopg-pool", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/43/a57494f47d29bd371ab38b745b2e93b9d486067391631c50beda889e7706/psycopg_binary-3.2.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:cad2de17804c4cfee8640ae2b279d616bb9e4734ac3c17c13db5e40982bd710d", size = 3379984 }, + { url = "https://files.pythonhosted.org/packages/bb/60/3b23bab21de16d08a15612ebf8727604b13f0d7457c66b5cf3fed05420eb/psycopg_binary-3.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:592b27d6c46a40f9eeaaeea7c1fef6f3c60b02c634365eb649b2d880669f149f", size = 3501361 }, + { url = "https://files.pythonhosted.org/packages/40/88/f8055b32f72bed87a7989254975aa9d5a692356df6ba0971f10c53b73420/psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a997efbaadb5e1a294fb5760e2f5643d7b8e4e3fe6cb6f09e6d605fd28e0291", size = 4467387 }, + { url = "https://files.pythonhosted.org/packages/aa/6b/b3fd5c22212172cb480775e0c19bb13926581536f131d9fd44def27385cf/psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1d2b6438fb83376f43ebb798bf0ad5e57bc56c03c9c29c85bc15405c8c0ac5a", size = 4269322 }, + { url = "https://files.pythonhosted.org/packages/d5/ba/1682c91820235c6aa953772e28aa133488e827cbd17f35bb3e9140c922d4/psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f087bd84bdcac78bf9f024ebdbfacd07fc0a23ec8191448a50679e2ac4a19e", size = 4513654 }, + { url = "https://files.pythonhosted.org/packages/64/dd/bc81a1e5da6827efbf80c6881e36feb425dcd43efd91182ae50c07d19b1c/psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:415c3b72ea32119163255c6504085f374e47ae7345f14bc3f0ef1f6e0976a879", size = 4213509 }, + { url = "https://files.pythonhosted.org/packages/75/13/edc342fbb4347affbc4df85300c69e56c5f56648d0ed63ae954448915c83/psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f092114f10f81fb6bae544a0ec027eb720e2d9c74a4fcdaa9dd3899873136935", size = 3135832 }, + { url = "https://files.pythonhosted.org/packages/f5/b6/2079baff967b5f42f5f3d5476cfd70d85f0931e382b9e107e2653850fe0a/psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06a7aae34edfe179ddc04da005e083ff6c6b0020000399a2cbf0a7121a8a22ea", size = 3113278 }, + { url = "https://files.pythonhosted.org/packages/58/a0/c1c31306361142197a736eda60bc7ff4d735e481e8bb63b55d95cc982e3f/psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b018631e5c80ce9bc210b71ea885932f9cca6db131e4df505653d7e3873a938", size = 3222019 }, + { url = "https://files.pythonhosted.org/packages/c4/39/7af53a485b916232d7423dc58e610a79961af6f4e2c3827ec111cdb3684e/psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8a509aeaac364fa965454e80cd110fe6d48ba2c80f56c9b8563423f0b5c3cfd", size = 3253667 }, + { url = "https://files.pythonhosted.org/packages/31/65/28feb23d1ab2d9d1215899faeedd2504fca37e0dbae546e9e7e62fee05f6/psycopg_binary-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:413977d18412ff83486eeb5875eb00b185a9391c57febac45b8993bf9c0ff489", size = 2922341 }, + { url = "https://files.pythonhosted.org/packages/43/68/f49dd22dc9f9869597d90fff73dcc8c9754304cdfeefa5f463abb4a1fcce/psycopg_binary-3.2.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:62b1b7b07e00ee490afb39c0a47d8282a9c2822c7cfed9553a04b0058adf7e7f", size = 3388952 }, + { url = "https://files.pythonhosted.org/packages/ef/3c/90210e090be228e9876bc210576cfd75e240505f16c92fa8b11839acbf35/psycopg_binary-3.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f8afb07114ea9b924a4a0305ceb15354ccf0ef3c0e14d54b8dbeb03e50182dd7", size = 3506474 }, + { url = "https://files.pythonhosted.org/packages/9d/2a/d45ff1f4b8d5b334695f3f5a68c722dbf483b65348f2e2639cf2f45c7b73/psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40bb515d042f6a345714ec0403df68ccf13f73b05e567837d80c886c7c9d3805", size = 4464849 }, + { url = "https://files.pythonhosted.org/packages/8c/ce/60562887f1363747ce2e074841548f96b433dd50e78d822c88e7ad6ec817/psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6418712ba63cebb0c88c050b3997185b0ef54173b36568522d5634ac06153040", size = 4263085 }, + { url = "https://files.pythonhosted.org/packages/2e/4f/af3cb85b967d2616c9c4e2bea9e865c8d0c38fc83ce5db1ef050ceba2bea/psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:101472468d59c74bb8565fab603e032803fd533d16be4b2d13da1bab8deb32a3", size = 4514411 }, + { url = "https://files.pythonhosted.org/packages/1d/00/685055d15f70e57d24cffe59021d53d428cdd7126b87442b5b07c9ffd222/psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa3931f308ab4a479d0ee22dc04bea867a6365cac0172e5ddcba359da043854b", size = 4207636 }, + { url = "https://files.pythonhosted.org/packages/72/9f/d6f6c8f60c4ebcc270efda17ab22110b24934f610dc7d5d3e2dc1e9eecbc/psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc314a47d44fe1a8069b075a64abffad347a3a1d8652fed1bab5d3baea37acb2", size = 3132484 }, + { url = "https://files.pythonhosted.org/packages/8e/e8/742cca374ab3725606f79a9b3b2429bba73917e1d14d52ba39d83dec0a3c/psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cc304a46be1e291031148d9d95c12451ffe783ff0cc72f18e2cc7ec43cdb8c68", size = 3111128 }, + { url = "https://files.pythonhosted.org/packages/61/a9/046536ef56a785e12c72c2a2507058473889bd7d625fbce142f1a1662bc2/psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f9e13600647087df5928875559f0eb8f496f53e6278b7da9511b4b3d0aff960", size = 3213088 }, + { url = "https://files.pythonhosted.org/packages/2d/40/a988739a5d8e72c553a44abba71217c601400e5164a874916e2aa4285139/psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b140182830c76c74d17eba27df3755a46442ce8d4fb299e7f1cf2f74a87c877b", size = 3252404 }, + { url = "https://files.pythonhosted.org/packages/c7/16/bfefaa5417e05f77c12f1cd099da7a00666fb2c8aef5996014f255a29857/psycopg_binary-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:3c838806eeb99af39f934b7999e35f947a8e577997cc892c12b5053a97a9057f", size = 2925802 }, + { url = "https://files.pythonhosted.org/packages/50/5d/51d39aafab4384a744d5e927b7867f3dadd8537249e8173e34aaf894db94/psycopg_binary-3.2.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7066d3dca196ed0dc6172f9777b2d62e4f138705886be656cccff2d555234d60", size = 3359766 }, + { url = "https://files.pythonhosted.org/packages/e4/7b/75be686af04e2019b53a9ff22de3aa750db7d34f532e4b949ed15a78b627/psycopg_binary-3.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:28ada5f610468c57d8a4a055a8ea915d0085a43d794266c4f3b9d02f4288f4db", size = 3503325 }, + { url = "https://files.pythonhosted.org/packages/3f/9a/28da916a65fb40fb3e1a97e1ae0a26860d8c1265c6e9766bd6c47abc437b/psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e8213bf50af073b1aa8dc3cff123bfeedac86332a16c1b7274910bc88a847c7", size = 4443593 }, + { url = "https://files.pythonhosted.org/packages/b0/9a/3dc1237a2ef3344b347af79e1aad2a60277cfafa2846f54cb13e1cd8c528/psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74d623261655a169bc84a9669890975c229f2fa6e19a7f2d10a77675dcf1a707", size = 4247005 }, + { url = "https://files.pythonhosted.org/packages/d0/a9/06491cb0338b6f0868d349d2a526586dc165e508b64daa2ff45f9db7ba4b/psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42781ba94e8842ee98bca5a7d0c44cc9d067500fedca2d6a90fa3609b6d16b42", size = 4484179 }, + { url = "https://files.pythonhosted.org/packages/4b/5f/b1116467dd18b4efc1aa7f03c96da751724a43c6a630979c61f60a9fbe5f/psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e6669091d09f8ba36e10ce678a6d9916e110446236a9b92346464a3565635e", size = 4186490 }, + { url = "https://files.pythonhosted.org/packages/a4/87/6092d1701d36c5aeb74c35cb54266fd44ee0f7711cafa4c0bffd873bdb61/psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b09e8a576a2ac69d695032ee76f31e03b30781828b5dd6d18c6a009e5a3d1c35", size = 3109385 }, + { url = "https://files.pythonhosted.org/packages/62/61/4ad7e29d09202478b6f568fff19efa978a4f2c25cb5efcd73544a4ee8be7/psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8f28ff0cb9f1defdc4a6f8c958bf6787274247e7dfeca811f6e2f56602695fb1", size = 3094397 }, + { url = "https://files.pythonhosted.org/packages/b7/dd/0ae42c64bf524d1fcf9bf861ab09d331e693ae00e527ba08131b2d3729a3/psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c84fcac8a3a3479ac14673095cc4e1fdba2935499f72c436785ac679bec0d1a", size = 3184097 }, + { url = "https://files.pythonhosted.org/packages/dd/f0/09329ebb0cd03e2ee5786fc9914ac904f4965b78627f15826f8258fde734/psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:950fd666ec9e9fe6a8eeb2b5a8f17301790e518953730ad44d715b59ffdbc67f", size = 3228517 }, + { url = "https://files.pythonhosted.org/packages/60/2f/979228189adbeb59afce626f1e7c3bf73cc7ff94217099a2ddfd6fd132ff/psycopg_binary-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:334046a937bb086c36e2c6889fe327f9f29bfc085d678f70fac0b0618949f674", size = 2911959 }, +] + +[[package]] +name = "psycopg-pool" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/57/9353b9ca259eaa3f0da2780eae7136948e70a8423e66b08a1115e7501860/psycopg_pool-3.2.2.tar.gz", hash = "sha256:9e22c370045f6d7f2666a5ad1b0caf345f9f1912195b0b25d0d3bcc4f3a7389c", size = 29665 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/0f/1cbe48737ac568e09fe03fbbcc585cdb535b5efb7709ba9b3f38a7ad7645/psycopg_pool-3.2.2-py3-none-any.whl", hash = "sha256:273081d0fbfaced4f35e69200c89cb8fbddfe277c38cc86c235b90a2ec2c8153", size = 38140 }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, +] + +[[package]] +name = "pyarrow" +version = "17.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/4e/ea6d43f324169f8aec0e57569443a38bab4b398d09769ca64f7b4d467de3/pyarrow-17.0.0.tar.gz", hash = "sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28", size = 1112479 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/5d/78d4b040bc5ff2fc6c3d03e80fca396b742f6c125b8af06bcf7427f931bc/pyarrow-17.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07", size = 28994846 }, + { url = "https://files.pythonhosted.org/packages/3b/73/8ed168db7642e91180330e4ea9f3ff8bab404678f00d32d7df0871a4933b/pyarrow-17.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655", size = 27165908 }, + { url = "https://files.pythonhosted.org/packages/81/36/e78c24be99242063f6d0590ef68c857ea07bdea470242c361e9a15bd57a4/pyarrow-17.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545", size = 39264209 }, + { url = "https://files.pythonhosted.org/packages/18/4c/3db637d7578f683b0a8fb8999b436bdbedd6e3517bd4f90c70853cf3ad20/pyarrow-17.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2", size = 39862883 }, + { url = "https://files.pythonhosted.org/packages/81/3c/0580626896c842614a523e66b351181ed5bb14e5dfc263cd68cea2c46d90/pyarrow-17.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8", size = 38723009 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/c1b47f0ada36d856a352da261a44d7344d8f22e2f7db3945f8c3b81be5dd/pyarrow-17.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047", size = 39855626 }, + { url = "https://files.pythonhosted.org/packages/19/09/b0a02908180a25d57312ab5919069c39fddf30602568980419f4b02393f6/pyarrow-17.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087", size = 25147242 }, + { url = "https://files.pythonhosted.org/packages/f9/46/ce89f87c2936f5bb9d879473b9663ce7a4b1f4359acc2f0eb39865eaa1af/pyarrow-17.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977", size = 29028748 }, + { url = "https://files.pythonhosted.org/packages/8d/8e/ce2e9b2146de422f6638333c01903140e9ada244a2a477918a368306c64c/pyarrow-17.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3", size = 27190965 }, + { url = "https://files.pythonhosted.org/packages/3b/c8/5675719570eb1acd809481c6d64e2136ffb340bc387f4ca62dce79516cea/pyarrow-17.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15", size = 39269081 }, + { url = "https://files.pythonhosted.org/packages/5e/78/3931194f16ab681ebb87ad252e7b8d2c8b23dad49706cadc865dff4a1dd3/pyarrow-17.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597", size = 39864921 }, + { url = "https://files.pythonhosted.org/packages/d8/81/69b6606093363f55a2a574c018901c40952d4e902e670656d18213c71ad7/pyarrow-17.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420", size = 38740798 }, + { url = "https://files.pythonhosted.org/packages/4c/21/9ca93b84b92ef927814cb7ba37f0774a484c849d58f0b692b16af8eebcfb/pyarrow-17.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4", size = 39871877 }, + { url = "https://files.pythonhosted.org/packages/30/d1/63a7c248432c71c7d3ee803e706590a0b81ce1a8d2b2ae49677774b813bb/pyarrow-17.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03", size = 25151089 }, + { url = "https://files.pythonhosted.org/packages/d4/62/ce6ac1275a432b4a27c55fe96c58147f111d8ba1ad800a112d31859fae2f/pyarrow-17.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22", size = 29019418 }, + { url = "https://files.pythonhosted.org/packages/8e/0a/dbd0c134e7a0c30bea439675cc120012337202e5fac7163ba839aa3691d2/pyarrow-17.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053", size = 27152197 }, + { url = "https://files.pythonhosted.org/packages/cb/05/3f4a16498349db79090767620d6dc23c1ec0c658a668d61d76b87706c65d/pyarrow-17.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a", size = 39263026 }, + { url = "https://files.pythonhosted.org/packages/c2/0c/ea2107236740be8fa0e0d4a293a095c9f43546a2465bb7df34eee9126b09/pyarrow-17.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc", size = 39880798 }, + { url = "https://files.pythonhosted.org/packages/f6/b0/b9164a8bc495083c10c281cc65064553ec87b7537d6f742a89d5953a2a3e/pyarrow-17.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a", size = 38715172 }, + { url = "https://files.pythonhosted.org/packages/f1/c4/9625418a1413005e486c006e56675334929fad864347c5ae7c1b2e7fe639/pyarrow-17.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b", size = 39874508 }, + { url = "https://files.pythonhosted.org/packages/ae/49/baafe2a964f663413be3bd1cf5c45ed98c5e42e804e2328e18f4570027c1/pyarrow-17.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7", size = 25099235 }, +] + +[[package]] +name = "pyasn1" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/a3/d2157f333900747f20984553aca98008b6dc843eb62f3a36030140ccec0d/pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", size = 148088 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/7e/5f50d07d5e70a2addbccd90ac2950f81d1edd0783630651d9268d7f1db49/pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473", size = 85313 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/00/e7bd1dec10667e3f2be602686537969a7ac92b0a7c5165be2e5875dc3971/pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", size = 307859 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/68/8906226b15ef38e71dc926c321d2fe99de8048e9098b5dfd38343011c886/pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b", size = 181220 }, +] + +[[package]] +name = "pybars4" +version = "0.9.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymeta3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/52/9aa428633ef5aba4b096b2b2f8d046ece613cecab28b4ceed54126d25ea5/pybars4-0.9.13.tar.gz", hash = "sha256:425817da20d4ad320bc9b8e77a60cab1bb9d3c677df3dce224925c3310fcd635", size = 29907 } + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pycryptodome" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/ed/19223a0a0186b8a91ebbdd2852865839237a21c74f1fbc4b8d5b62965239/pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7", size = 4794232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/96/b0d494defb3346378086848a8ece5ddfd138a66c4a05e038fca873b2518c/pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044", size = 2427142 }, + { url = "https://files.pythonhosted.org/packages/24/80/56a04e2ae622d7f38c1c01aef46a26c6b73a2ad15c9705a8e008b5befb03/pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a", size = 1590045 }, + { url = "https://files.pythonhosted.org/packages/ea/94/82ebfa5c83d980907ceebf79b00909a569d258bdfd9b0264d621fa752cfd/pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2", size = 2061748 }, + { url = "https://files.pythonhosted.org/packages/af/20/5f29ec45462360e7f61e8688af9fe4a0afae057edfabdada662e11bf97e7/pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c", size = 2135687 }, + { url = "https://files.pythonhosted.org/packages/e5/1f/6bc4beb4adc07b847e5d3fddbec4522c2c3aa05df9e61b91dc4eff6a4946/pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25", size = 2164262 }, + { url = "https://files.pythonhosted.org/packages/30/4b/cbc67cda0efd55d7ddcc98374c4b9c853022a595ed1d78dd15c961bc7f6e/pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128", size = 2054347 }, + { url = "https://files.pythonhosted.org/packages/0d/08/01987ab75ca789247a88c8b2f0ce374ef7d319e79589e0842e316a272662/pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c", size = 2192762 }, + { url = "https://files.pythonhosted.org/packages/b5/bf/798630923b67f4201059c2d690105998f20a6a8fb9b5ab68d221985155b3/pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4", size = 2155230 }, + { url = "https://files.pythonhosted.org/packages/39/12/5fe7f5b9212dda9f5a26f842a324d6541fe1ca8059602124ff30db1e874b/pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72", size = 1723464 }, + { url = "https://files.pythonhosted.org/packages/1f/90/d131c0eb643290230dfa4108b7c2d135122d88b714ad241d77beb4782a76/pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9", size = 1759588 }, + { url = "https://files.pythonhosted.org/packages/17/87/c7153fcd400df0f4a67d7d92cdb6b5e43f309c22434374b8a61849dfb280/pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a", size = 1639310 }, + { url = "https://files.pythonhosted.org/packages/68/9a/88d984405b087e8c8dd9a9d4c81a6fa675454e5fcf2ae01d9553b3128637/pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e", size = 1708332 }, + { url = "https://files.pythonhosted.org/packages/c7/10/88fb67d2fa545ce2ac61cfda70947bcbb1769f1956315c4b919d79774897/pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04", size = 1565619 }, + { url = "https://files.pythonhosted.org/packages/a2/40/63dff38fa4f7888f812263494d4a745eeed180ff09dd7b8350a81eb09d21/pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3", size = 1606403 }, + { url = "https://files.pythonhosted.org/packages/8b/61/522235ca81d9dcfcf8b4cbc253b3a8a1f2231603d486369a8a02eb998f31/pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea", size = 1637284 }, + { url = "https://files.pythonhosted.org/packages/e9/a7/5aa0596f7fc710fd55b4e6bbb025fedacfec929465a618f20e61ebf7df76/pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b", size = 1741193 }, +] + +[[package]] +name = "pydantic" +version = "2.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/99/d0a5dca411e0a017762258013ba9905cd6e7baa9a3fd1fe8b6529472902e/pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a", size = 739834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/fa/b7f815b8c9ad021c07f88875b601222ef5e70619391ade4a49234d12d278/pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8", size = 423875 }, +] + +[[package]] +name = "pydantic-core" +version = "2.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/e3/0d5ad91211dba310f7ded335f4dad871172b9cc9ce204f5a56d76ccd6247/pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4", size = 388371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/9d/f30f080f745682e762512f3eef1f6e392c7d74a102e6e96de8a013a5db84/pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3", size = 1837257 }, + { url = "https://files.pythonhosted.org/packages/f2/89/77e7aebdd4a235497ac1e07f0a99e9f40e47f6e0f6783fe30500df08fc42/pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6", size = 1776715 }, + { url = "https://files.pythonhosted.org/packages/18/50/5a4e9120b395108c2a0441a425356c0d26a655d7c617288bec1c28b854ac/pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a", size = 1789023 }, + { url = "https://files.pythonhosted.org/packages/c7/e5/f19e13ba86b968d024b56aa53f40b24828652ac026e5addd0ae49eeada02/pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3", size = 1775598 }, + { url = "https://files.pythonhosted.org/packages/c9/c7/f3c29bed28bd022c783baba5bf9946c4f694cb837a687e62f453c81eb5c6/pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1", size = 1977691 }, + { url = "https://files.pythonhosted.org/packages/41/3e/f62c2a05c554fff34570f6788617e9670c83ed7bc07d62a55cccd1bc0be6/pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953", size = 2693214 }, + { url = "https://files.pythonhosted.org/packages/ae/49/8a6fe79d35e2f3bea566d8ea0e4e6f436d4f749d7838c8e8c4c5148ae706/pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98", size = 2061047 }, + { url = "https://files.pythonhosted.org/packages/51/c6/585355c7c8561e11197dbf6333c57dd32f9f62165d48589b57ced2373d97/pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a", size = 1895106 }, + { url = "https://files.pythonhosted.org/packages/ce/23/829f6b87de0775919e82f8addef8b487ace1c77bb4cb754b217f7b1301b6/pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a", size = 1968506 }, + { url = "https://files.pythonhosted.org/packages/ca/2f/f8ca8f0c40b3ee0a4d8730a51851adb14c5eda986ec09f8d754b2fba784e/pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840", size = 2110217 }, + { url = "https://files.pythonhosted.org/packages/bb/a0/1876656c7b17eb69cc683452cce6bb890dd722222a71b3de57ddb512f561/pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250", size = 1709669 }, + { url = "https://files.pythonhosted.org/packages/be/4a/576524eefa9b301c088c4818dc50ff1c51a88fe29efd87ab75748ae15fd7/pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c", size = 1902386 }, + { url = "https://files.pythonhosted.org/packages/61/db/f6a724db226d990a329910727cfac43539ff6969edc217286dd05cda3ef6/pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312", size = 1834507 }, + { url = "https://files.pythonhosted.org/packages/9b/83/6f2bfe75209d557ae1c3550c1252684fc1827b8b12fbed84c3b4439e135d/pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88", size = 1773527 }, + { url = "https://files.pythonhosted.org/packages/93/ef/513ea76d7ca81f2354bb9c8d7839fc1157673e652613f7e1aff17d8ce05d/pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc", size = 1787879 }, + { url = "https://files.pythonhosted.org/packages/31/0a/ac294caecf235f0cc651de6232f1642bb793af448d1cfc541b0dc1fd72b8/pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43", size = 1774694 }, + { url = "https://files.pythonhosted.org/packages/46/a4/08f12b5512f095963550a7cb49ae010e3f8f3f22b45e508c2cb4d7744fce/pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6", size = 1976369 }, + { url = "https://files.pythonhosted.org/packages/15/59/b2495be4410462aedb399071c71884042a2c6443319cbf62d00b4a7ed7a5/pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121", size = 2691250 }, + { url = "https://files.pythonhosted.org/packages/3c/ae/fc99ce1ba791c9e9d1dee04ce80eef1dae5b25b27e3fc8e19f4e3f1348bf/pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1", size = 2061462 }, + { url = "https://files.pythonhosted.org/packages/44/bb/eb07cbe47cfd638603ce3cb8c220f1a054b821e666509e535f27ba07ca5f/pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b", size = 1893923 }, + { url = "https://files.pythonhosted.org/packages/ce/ef/5a52400553b8faa0e7f11fd7a2ba11e8d2feb50b540f9e7973c49b97eac0/pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27", size = 1966779 }, + { url = "https://files.pythonhosted.org/packages/4c/5b/fb37fe341344d9651f5c5f579639cd97d50a457dc53901aa8f7e9f28beb9/pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b", size = 2109044 }, + { url = "https://files.pythonhosted.org/packages/70/1a/6f7278802dbc66716661618807ab0dfa4fc32b09d1235923bbbe8b3a5757/pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a", size = 1708265 }, + { url = "https://files.pythonhosted.org/packages/35/7f/58758c42c61b0bdd585158586fecea295523d49933cb33664ea888162daf/pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2", size = 1901750 }, + { url = "https://files.pythonhosted.org/packages/6f/47/ef0d60ae23c41aced42921728650460dc831a0adf604bfa66b76028cb4d0/pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231", size = 1839225 }, + { url = "https://files.pythonhosted.org/packages/6a/23/430f2878c9cd977a61bb39f71751d9310ec55cee36b3d5bf1752c6341fd0/pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9", size = 1768604 }, + { url = "https://files.pythonhosted.org/packages/9e/2b/ec4e7225dee79e0dc80ccc3c35ab33cc2c4bbb8a1a7ecf060e5e453651ec/pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f", size = 1789767 }, + { url = "https://files.pythonhosted.org/packages/64/b0/38b24a1fa6d2f96af3148362e10737ec073768cd44d3ec21dca3be40a519/pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52", size = 1772061 }, + { url = "https://files.pythonhosted.org/packages/5e/da/bb73274c42cb60decfa61e9eb0c9029da78b3b9af0a9de0309dbc8ff87b6/pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237", size = 1974573 }, + { url = "https://files.pythonhosted.org/packages/c8/65/41693110fb3552556180460daffdb8bbeefb87fc026fd9aa4b849374015c/pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe", size = 2625596 }, + { url = "https://files.pythonhosted.org/packages/09/b3/a5a54b47cccd1ab661ed5775235c5e06924753c2d4817737c5667bfa19a8/pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e", size = 2099064 }, + { url = "https://files.pythonhosted.org/packages/52/fa/443a7a6ea54beaba45ff3a59f3d3e6e3004b7460bcfb0be77bcf98719d3b/pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24", size = 1900345 }, + { url = "https://files.pythonhosted.org/packages/8e/e6/9aca9ffae60f9cdf0183069de3e271889b628d0fb175913fcb3db5618fb1/pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1", size = 1968252 }, + { url = "https://files.pythonhosted.org/packages/46/5e/6c716810ea20a6419188992973a73c2fb4eb99cd382368d0637ddb6d3c99/pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd", size = 2119191 }, + { url = "https://files.pythonhosted.org/packages/06/fc/6123b00a9240fbb9ae0babad7a005d51103d9a5d39c957a986f5cdd0c271/pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688", size = 1717788 }, + { url = "https://files.pythonhosted.org/packages/d5/36/e61ad5a46607a469e2786f398cd671ebafcd9fb17f09a2359985c7228df5/pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d", size = 1898188 }, + { url = "https://files.pythonhosted.org/packages/49/75/40b0e98b658fdba02a693b3bacb4c875a28bba87796c7b13975976597d8c/pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686", size = 1838688 }, + { url = "https://files.pythonhosted.org/packages/75/02/d8ba2d4a266591a6a623c68b331b96523d4b62ab82a951794e3ed8907390/pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a", size = 1768409 }, + { url = "https://files.pythonhosted.org/packages/91/ae/25ecd9bc4ce4993e99a1a3c9ab111c082630c914260e129572fafed4ecc2/pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b", size = 1789317 }, + { url = "https://files.pythonhosted.org/packages/7a/80/72057580681cdbe55699c367963d9c661b569a1d39338b4f6239faf36cdc/pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19", size = 1771949 }, + { url = "https://files.pythonhosted.org/packages/a2/be/d9bbabc55b05019013180f141fcaf3b14dbe15ca7da550e95b60c321009a/pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac", size = 1974392 }, + { url = "https://files.pythonhosted.org/packages/79/2d/7bcd938c6afb0f40293283f5f09988b61fb0a4f1d180abe7c23a2f665f8e/pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703", size = 2625565 }, + { url = "https://files.pythonhosted.org/packages/ac/88/ca758e979457096008a4b16a064509028e3e092a1e85a5ed6c18ced8da88/pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c", size = 2098784 }, + { url = "https://files.pythonhosted.org/packages/eb/de/2fad6d63c3c42e472e985acb12ec45b7f56e42e6f4cd6dfbc5e87ee8678c/pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83", size = 1900198 }, + { url = "https://files.pythonhosted.org/packages/fe/50/077c7f35b6488dc369a6d22993af3a37901e198630f38ac43391ca730f5b/pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203", size = 1968005 }, + { url = "https://files.pythonhosted.org/packages/5d/1f/f378631574ead46d636b9a04a80ff878b9365d4b361b1905ef1667d4182a/pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0", size = 2118920 }, + { url = "https://files.pythonhosted.org/packages/7a/ea/e4943f17df7a3031d709481fe4363d4624ae875a6409aec34c28c9e6cf59/pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e", size = 1717397 }, + { url = "https://files.pythonhosted.org/packages/13/63/b95781763e8d84207025071c0cec16d921c0163c7a9033ae4b9a0e020dc7/pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20", size = 1898013 }, + { url = "https://files.pythonhosted.org/packages/73/73/0c7265903f66cce39ed7ca939684fba344210cefc91ccc999cfd5b113fd3/pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906", size = 1828190 }, + { url = "https://files.pythonhosted.org/packages/27/55/60b8b0e58b49ee3ed36a18562dd7c6bc06a551c390e387af5872a238f2ec/pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94", size = 1715252 }, + { url = "https://files.pythonhosted.org/packages/28/3d/d66314bad6bb777a36559195a007b31e916bd9e2c198f7bb8f4ccdceb4fa/pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f", size = 1782641 }, + { url = "https://files.pythonhosted.org/packages/9e/f5/f178f4354d0d6c1431a8f9ede71f3c4269ac4dc55d314fdb7555814276dc/pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482", size = 1928788 }, + { url = "https://files.pythonhosted.org/packages/9c/51/1f5e27bb194df79e30b593b608c66e881ed481241e2b9ed5bdf86d165480/pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6", size = 1886116 }, + { url = "https://files.pythonhosted.org/packages/ac/76/450d9258c58dc7c70b9e3aadf6bebe23ddd99e459c365e2adbde80e238da/pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc", size = 1960125 }, + { url = "https://files.pythonhosted.org/packages/dd/9e/0309a7a4bea51771729515e413b3987be0789837de99087f7415e0db1f9b/pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99", size = 2100407 }, + { url = "https://files.pythonhosted.org/packages/af/93/06d44e08277b3b818b75bd5f25e879d7693e4b7dd3505fde89916fcc9ca2/pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6", size = 1914966 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/14/7bfb313ccee79f97dc235721b035174af94ef4472cfe455c259cd2971f2f/pydantic_settings-2.4.0.tar.gz", hash = "sha256:ed81c3a0f46392b4d7c0a565c05884e6e54b3456e6f0fe4d8814981172dc9a88", size = 63033 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/46/7f90f36c1bdcf24962d2b7b0e11aba3bbd65ea7904cb2553072882a4e6b7/pydantic_settings-2.4.0-py3-none-any.whl", hash = "sha256:bb6849dc067f1687574c12a639e231f3a6feeed0a12d710c1382045c5db1c315", size = 23996 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pyjwt" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c", size = 78825 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", size = 22344 }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "pymeta3" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/af/409edba35fc597f1e386e3860303791ab5a28d6cc9a8aecbc567051b19a9/PyMeta3-0.5.1.tar.gz", hash = "sha256:18bda326d9a9bbf587bfc0ee0bc96864964d78b067288bcf55d4d98681d05bcb", size = 29566 } + +[[package]] +name = "pymilvus" +version = "2.3.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-storage-blob", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "environs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "minio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pandas", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyarrow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "ujson", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/54/d01f6609245ea6fbf16f7bb6d7cf28b083342abbf05dad414077279c7004/pymilvus-2.3.8.tar.gz", hash = "sha256:686e30939540114b1b7d42a8b3ab3dfcd0fa323b506e69e624c203c491db2a58", size = 1183645 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/7dd39e2199933a529523ae77788ce3490cb8519243d07f7e700488536558/pymilvus-2.3.8-py3-none-any.whl", hash = "sha256:1301bbb0252a2e7aa970be14b6c0e694242faed0f8e3c7d43ed94f61f313a536", size = 179765 }, +] + +[[package]] +name = "pymongo" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/2c/ad0896cb94668c3cad1eb702ab60ae17036b051f54cfe547f11a0322f1d3/pymongo-4.8.0.tar.gz", hash = "sha256:454f2295875744dc70f1881e4b2eb99cdad008a33574bc8aaf120530f66c0cde", size = 1506091 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/eb/3d1afb6800886174bea7f6d01112fd3e2d29d97aac884dc60524fb0d7f4f/pymongo-4.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2b7bec27e047e84947fbd41c782f07c54c30c76d14f3b8bf0c89f7413fac67a", size = 592364 }, + { url = "https://files.pythonhosted.org/packages/b1/d0/1c6b455817200d4621847db16fc081d8c7b9dc2b372c47874112e2e4500e/pymongo-4.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c68fe128a171493018ca5c8020fc08675be130d012b7ab3efe9e22698c612a1", size = 592510 }, + { url = "https://files.pythonhosted.org/packages/f2/11/17e7585041125c86c55d5a85b4dcf9949e170480502aaa21eced7fc038e5/pymongo-4.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920d4f8f157a71b3cb3f39bc09ce070693d6e9648fb0e30d00e2657d1dca4e49", size = 1160190 }, + { url = "https://files.pythonhosted.org/packages/d9/1b/210ae77937ecccaa72fcd3c8bf4b6a6dfbe12e973c44adab8991852687d7/pymongo-4.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52b4108ac9469febba18cea50db972605cc43978bedaa9fea413378877560ef8", size = 1199235 }, + { url = "https://files.pythonhosted.org/packages/ea/a1/71a2e738379d3c719a92929a63048504270be73e60339d366f0cc2daf037/pymongo-4.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:180d5eb1dc28b62853e2f88017775c4500b07548ed28c0bd9c005c3d7bc52526", size = 1178476 }, + { url = "https://files.pythonhosted.org/packages/62/bd/b5e91ac167b57f3559e405389dad760980cf88b90824d7e9f758eacdd01c/pymongo-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aec2b9088cdbceb87e6ca9c639d0ff9b9d083594dda5ca5d3c4f6774f4c81b33", size = 1158294 }, + { url = "https://files.pythonhosted.org/packages/75/bd/9e67b191656a245612a43fc113dca0b7fbdf4a5da07815e795bcee8f475b/pymongo-4.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0cf61450feadca81deb1a1489cb1a3ae1e4266efd51adafecec0e503a8dcd84", size = 1128379 }, + { url = "https://files.pythonhosted.org/packages/63/e4/57e1e2ea95d4b3e4274f38713d591467267d20b0e08b97259287f2acd517/pymongo-4.8.0-cp310-cp310-win32.whl", hash = "sha256:8b18c8324809539c79bd6544d00e0607e98ff833ca21953df001510ca25915d1", size = 567090 }, + { url = "https://files.pythonhosted.org/packages/08/6c/fe22909894c2ba196661379ac3fc21db697904c1602ee14d5b2a15212e93/pymongo-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e5df28f74002e37bcbdfdc5109799f670e4dfef0fb527c391ff84f078050e7b5", size = 582038 }, + { url = "https://files.pythonhosted.org/packages/0a/3d/bba2845c76dddcd8c34d5014da80346851df048eefa826acb13265affba2/pymongo-4.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b50040d9767197b77ed420ada29b3bf18a638f9552d80f2da817b7c4a4c9c68", size = 645578 }, + { url = "https://files.pythonhosted.org/packages/c2/ca/d177c3ad846bad631b548b27c261821d25a08d608dca134aedb1b00b98fe/pymongo-4.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:417369ce39af2b7c2a9c7152c1ed2393edfd1cbaf2a356ba31eb8bcbd5c98dd7", size = 645731 }, + { url = "https://files.pythonhosted.org/packages/be/1a/3d9b9fb3f9de9da46919fef900fe88090f5865a09ae9e0e19496a603a819/pymongo-4.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf821bd3befb993a6db17229a2c60c1550e957de02a6ff4dd0af9476637b2e4d", size = 1399930 }, + { url = "https://files.pythonhosted.org/packages/57/64/281c9c8efb98ab6c6fcf44bf7cc33e17bcb163cb9c9260c9d78d2318d013/pymongo-4.8.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9365166aa801c63dff1a3cb96e650be270da06e3464ab106727223123405510f", size = 1451584 }, + { url = "https://files.pythonhosted.org/packages/37/ed/5258d22a91ea6e0b9d72e0aa7674f5a9951fea0c036d1063f29bc45a35d2/pymongo-4.8.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc8b8582f4209c2459b04b049ac03c72c618e011d3caa5391ff86d1bda0cc486", size = 1423899 }, + { url = "https://files.pythonhosted.org/packages/f3/7f/6d231046d9caf43395f9406dbef885f122edbee172ec6a3a6ea330e07848/pymongo-4.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e5019f75f6827bb5354b6fef8dfc9d6c7446894a27346e03134d290eb9e758", size = 1397112 }, + { url = "https://files.pythonhosted.org/packages/af/81/4074148396415ac19074a1a144e1cd6b2ff000f5ef253ed24a4e3e9ff340/pymongo-4.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b5802151fc2b51cd45492c80ed22b441d20090fb76d1fd53cd7760b340ff554", size = 1357689 }, + { url = "https://files.pythonhosted.org/packages/bc/26/799fe943573b2d86970698a0667d8d8636790e86242d979f4b3d870d269f/pymongo-4.8.0-cp311-cp311-win32.whl", hash = "sha256:4bf58e6825b93da63e499d1a58de7de563c31e575908d4e24876234ccb910eba", size = 611133 }, + { url = "https://files.pythonhosted.org/packages/51/28/577224211f43e2079126bfec53080efba46e59218f47808098f125139558/pymongo-4.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b747c0e257b9d3e6495a018309b9e0c93b7f0d65271d1d62e572747f4ffafc88", size = 630990 }, + { url = "https://files.pythonhosted.org/packages/9e/8d/b082d026f96215a76553032620549f931679da7f941018e2c358fd549faa/pymongo-4.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e6a720a3d22b54183352dc65f08cd1547204d263e0651b213a0a2e577e838526", size = 699090 }, + { url = "https://files.pythonhosted.org/packages/eb/da/fa51bb7d8d5c8b4672b72c05a9357b5f9300f48128574c746fa4825f607a/pymongo-4.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31e4d21201bdf15064cf47ce7b74722d3e1aea2597c6785882244a3bb58c7eab", size = 698800 }, + { url = "https://files.pythonhosted.org/packages/7b/dc/78f0c931d38bece6ae1dc49035961c82f3eb42952c745391ebdd3a910222/pymongo-4.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b804bb4f2d9dc389cc9e827d579fa327272cdb0629a99bfe5b83cb3e269ebf", size = 1655527 }, + { url = "https://files.pythonhosted.org/packages/74/36/92f0eeeb5111c332072e37efb1d5a668c5e4b75be53cbd06a77f6b4192d2/pymongo-4.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2fbdb87fe5075c8beb17a5c16348a1ea3c8b282a5cb72d173330be2fecf22f5", size = 1718203 }, + { url = "https://files.pythonhosted.org/packages/98/40/757579f837dadaddf167cd36ae85a7ab29c035bc0ae8d90bdc8a5fbdfc33/pymongo-4.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd39455b7ee70aabee46f7399b32ab38b86b236c069ae559e22be6b46b2bbfc4", size = 1685776 }, + { url = "https://files.pythonhosted.org/packages/24/bb/13d23966ad01511610a471eae480bcb6a94b832c40f2bdbc706f7a757b76/pymongo-4.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:940d456774b17814bac5ea7fc28188c7a1338d4a233efbb6ba01de957bded2e8", size = 1650569 }, + { url = "https://files.pythonhosted.org/packages/b5/80/1f405ce80cb6a3867709147e24a2f69e342ff71fb1b9ba663d0237f0c5ed/pymongo-4.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:236bbd7d0aef62e64caf4b24ca200f8c8670d1a6f5ea828c39eccdae423bc2b2", size = 1601592 }, + { url = "https://files.pythonhosted.org/packages/30/19/cd66230b6407c6b8cf45c1ae073659a88af5699c792c46fd4eaf317bd11e/pymongo-4.8.0-cp312-cp312-win32.whl", hash = "sha256:47ec8c3f0a7b2212dbc9be08d3bf17bc89abd211901093e3ef3f2adea7de7a69", size = 656042 }, + { url = "https://files.pythonhosted.org/packages/99/1c/f5108dc39450077556844abfd92b768c57775f85270fc0b1dc834ad18113/pymongo-4.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e84bc7707492f06fbc37a9f215374d2977d21b72e10a67f1b31893ec5a140ad8", size = 680400 }, +] + +[[package]] +name = "pyparsing" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/3a/31fd28064d016a2182584d579e033ec95b809d8e220e74c4af6f0f2e8842/pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", size = 889571 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/ea/6d76df31432a0e6fdf81681a895f009a4bb47b3c39036db3e1b528191d52/pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742", size = 103245 }, +] + +[[package]] +name = "pypika" +version = "0.48.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/2c/94ed7b91db81d61d7096ac8f2d325ec562fc75e35f3baea8749c85b28784/PyPika-0.48.9.tar.gz", hash = "sha256:838836a61747e7c8380cd1b7ff638694b7a7335345d0f559b04b2cd832ad5378", size = 67259 } + +[[package]] +name = "pyproject-hooks" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/07/6f63dda440d4abb191b91dc383b472dae3dd9f37e4c1e4a5c3db150531c6/pyproject_hooks-1.1.0.tar.gz", hash = "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965", size = 7838 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/f3/431b9d5fe7d14af7a32340792ef43b8a714e7726f1d7b69cc4e8e7a3f1d7/pyproject_hooks-1.1.0-py3-none-any.whl", hash = "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2", size = 9184 }, +] + +[[package]] +name = "pyreadline3" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/86/3d61a61f36a0067874a00cb4dceb9028d34b6060e47828f7fc86fb9f7ee9/pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae", size = 86465 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/fc/a3c13ded7b3057680c8ae95a9b6cc83e63657c38e0005c400a5d018a33a7/pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb", size = 95203 }, +] + +[[package]] +name = "pytest" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, + { name = "iniconfig", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pluggy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/8c/9862305bdcd6020bc7b45b1b5e7397a6caf1a33d3025b9a003b39075ffb2/pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce", size = 1439314 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/f9/cf155cf32ca7d6fa3601bc4c5dd19086af4b320b706919d48a4c79081cf9/pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5", size = 341802 }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/b4/0b378b7bf26a8ae161c3890c0b48a91a04106c5713ce81b4b080ea2f4f18/pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3", size = 46920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2", size = 17663 }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, +] + +[[package]] +name = "pytest-xdist" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/c4/3c310a19bc1f1e9ef50075582652673ef2bfc8cd62afef9585683821902f/pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d", size = 84060 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", size = 46108 }, +] + +[package.optional-dependencies] +psutil = [ + { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "pytz" +version = "2024.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/26/9f1f00a5d021fff16dee3de13d43e5e978f3d58928e129c3a62cf7eb9738/pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", size = 316214 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/3d/a121f284241f08268b21359bd425f7d4825cffc5ac5cd0e1b3d82ffd2b10/pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319", size = 505474 }, +] + +[[package]] +name = "pywin32" +version = "306" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/dc/28c668097edfaf4eac4617ef7adf081b9cf50d254672fcf399a70f5efc41/pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d", size = 8506422 }, + { url = "https://files.pythonhosted.org/packages/d3/d6/891894edec688e72c2e308b3243fad98b4066e1839fd2fe78f04129a9d31/pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8", size = 9226392 }, + { url = "https://files.pythonhosted.org/packages/8b/1e/fc18ad83ca553e01b97aa8393ff10e33c1fb57801db05488b83282ee9913/pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", size = 8507689 }, + { url = "https://files.pythonhosted.org/packages/7e/9e/ad6b1ae2a5ad1066dc509350e0fbf74d8d50251a51e420a2a8feaa0cecbd/pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", size = 9227547 }, + { url = "https://files.pythonhosted.org/packages/91/20/f744bff1da8f43388498503634378dbbefbe493e65675f2cc52f7185c2c2/pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", size = 10388324 }, + { url = "https://files.pythonhosted.org/packages/14/91/17e016d5923e178346aabda3dfec6629d1a26efe587d19667542105cf0a6/pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", size = 8507705 }, + { url = "https://files.pythonhosted.org/packages/83/1c/25b79fc3ec99b19b0a0730cc47356f7e2959863bf9f3cd314332bddb4f68/pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", size = 9227429 }, + { url = "https://files.pythonhosted.org/packages/1c/43/e3444dc9a12f8365d9603c2145d16bf0a2f8180f343cf87be47f5579e547/pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", size = 10388145 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "pyzmq" +version = "26.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "(implementation_name == 'pypy' and sys_platform == 'darwin') or (implementation_name == 'pypy' and sys_platform == 'linux') or (implementation_name == 'pypy' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/c7/01a2dd24d3f54012a85af44474cc2eb5bb40c991d5c25e0572e4cb5135a7/pyzmq-26.1.1.tar.gz", hash = "sha256:a7db05d8b7cd1a8c6610e9e9aa55d525baae7a44a43e18bc3260eb3f92de96c6", size = 271185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/01/ea9975053adff30c34d5a42378bee171faa4a4fae0f35d1211e8f9ca6e52/pyzmq-26.1.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:b1bb952d1e407463c9333ea7e0c0600001e54e08ce836d4f0aff1fb3f902cf63", size = 1340141 }, + { url = "https://files.pythonhosted.org/packages/15/76/b29ef0f21b0030b42e34db728df38be7b99165899b1f587ba6fba5c2f749/pyzmq-26.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:65e2a18e845c6ea7ab849c70db932eaeadee5edede9e379eb21c0a44cf523b2e", size = 1008893 }, + { url = "https://files.pythonhosted.org/packages/72/6d/efe916dfe41133ef7bf2edcea4d170b7818324fd106cec0574bd121abb46/pyzmq-26.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:def7ae3006924b8a0c146a89ab4008310913fa903beedb95e25dea749642528e", size = 673252 }, + { url = "https://files.pythonhosted.org/packages/50/a4/96f83a39be4831c30cc8322bca50b9e8d3db7701504f526fd409e271c4f2/pyzmq-26.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8234571df7816f99dde89c3403cb396d70c6554120b795853a8ea56fcc26cd3", size = 911824 }, + { url = "https://files.pythonhosted.org/packages/cf/2e/ba7e04cfdc04e1c0be9d1581dd04cf06f53986b76cfd8ac9572f27c136bc/pyzmq-26.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18da8e84dbc30688fd2baefd41df7190607511f916be34f9a24b0e007551822e", size = 868831 }, + { url = "https://files.pythonhosted.org/packages/75/ab/09001241a7e0e81d315ad3409c48ad9e450c7699e6d3bbe70cda5fa58075/pyzmq-26.1.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c70dab93d98b2bf3f0ac1265edbf6e7f83acbf71dabcc4611889bb0dea45bed7", size = 868895 }, + { url = "https://files.pythonhosted.org/packages/ca/32/a2298fff3d563450fd96175731a45949a111939b35dcd5e963bf70e99de4/pyzmq-26.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fcb90592c5d5c562e1b1a1ceccf6f00036d73c51db0271bf4d352b8d6b31d468", size = 1202921 }, + { url = "https://files.pythonhosted.org/packages/96/fd/25ab3e25171dc338e66334fcc83ecac26bbf935883294a2dc548fd996e0f/pyzmq-26.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cf4be7460a0c1bc71e9b0e64ecdd75a86386ca6afaa36641686f5542d0314e9d", size = 1515366 }, + { url = "https://files.pythonhosted.org/packages/a3/2a/763b45bf6526afc17911b6dd09704034629868394e19b8efaf9014ae51bf/pyzmq-26.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4cbecda4ddbfc1e309c3be04d333f9be3fc6178b8b6592b309676f929767a15", size = 1414773 }, + { url = "https://files.pythonhosted.org/packages/36/9e/592c6f746f256f35a81cc4cfc3ecd83ed7edcc1ac85b5289d24f57c9a996/pyzmq-26.1.1-cp310-cp310-win32.whl", hash = "sha256:583f73b113b8165713b6ce028d221402b1b69483055b5aa3f991937e34dd1ead", size = 586145 }, + { url = "https://files.pythonhosted.org/packages/e7/3b/f6192bbf59a87365038106201202a3afd91012241f71719d41e83bd4a6d5/pyzmq-26.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5e6f39ecb8eb7bfcb976c49262e8cf83ff76e082b77ca23ba90c9b6691a345be", size = 650181 }, + { url = "https://files.pythonhosted.org/packages/c6/b0/77d3eb346510ffea093d9fb9d1137007f1097e39a22b915af9ff0b639557/pyzmq-26.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8d042d6446cab3a1388b38596f5acabb9926b0b95c3894c519356b577a549458", size = 552290 }, + { url = "https://files.pythonhosted.org/packages/05/e4/2226ca5357c404086a332f86f9a80dfdfc911d3aef586484c69fece5db21/pyzmq-26.1.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:362cac2423e36966d336d79d3ec3eafeabc153ee3e7a5cf580d7e74a34b3d912", size = 1340720 }, + { url = "https://files.pythonhosted.org/packages/f3/13/eef5c8f8169e818aef5979bdaee0b304043e98b5212ae42c0a6c77de2564/pyzmq-26.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0841633446cb1539a832a19bb24c03a20c00887d0cedd1d891b495b07e5c5cb5", size = 1008784 }, + { url = "https://files.pythonhosted.org/packages/95/08/710f6ecd9a987993c36d2a6a52526536fd59616577affaa595a4c74a756b/pyzmq-26.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e1fcdc333afbf9918d0a614a6e10858aede7da49a60f6705a77e343fe86a317", size = 673182 }, + { url = "https://files.pythonhosted.org/packages/72/e6/821458f808f009451299f592d29dcb1a98cd0826a55c789503a7cfb399fb/pyzmq-26.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc8d655627d775475eafdcf0e49e74bcc1e5e90afd9ab813b4da98f092ed7b93", size = 910164 }, + { url = "https://files.pythonhosted.org/packages/29/74/a18cf4bed0569f206b461fcf24ca4a106edd6f4736574e27ed14d7cf8dda/pyzmq-26.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32de51744820857a6f7c3077e620ab3f607d0e4388dfead885d5124ab9bcdc5e", size = 868018 }, + { url = "https://files.pythonhosted.org/packages/62/77/a01bfe7e4d49d339cf7fbee5b644c1370a4a2b755dcf643e2d7e7944a50c/pyzmq-26.1.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a880240597010914ffb1d6edd04d3deb7ce6a2abf79a0012751438d13630a671", size = 869265 }, + { url = "https://files.pythonhosted.org/packages/50/2f/e0b315471e0838ef227d9693b81ea7bca471564230aaa2dd73e3ba92f260/pyzmq-26.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:26131b1cec02f941ed2d2b4b8cc051662b1c248b044eff5069df1f500bbced56", size = 1203406 }, + { url = "https://files.pythonhosted.org/packages/55/cd/a9ea641afb68fe32c632b610da830766f65537dae79b4db1ea5abb788ab3/pyzmq-26.1.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ce05841322b58510607f9508a573138d995a46c7928887bc433de9cb760fd2ad", size = 1514267 }, + { url = "https://files.pythonhosted.org/packages/d8/d3/f86bf419202d03df579a67079ff8f9ccb4190ed467ad41f8fd091ac2e613/pyzmq-26.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32123ff0a6db521aadf2b95201e967a4e0d11fb89f73663a99d2f54881c07214", size = 1414397 }, + { url = "https://files.pythonhosted.org/packages/7a/83/061ed3bf2649fccaf6ab2b06dcb46077a09dfbc93f9b32dc675d2fd12d12/pyzmq-26.1.1-cp311-cp311-win32.whl", hash = "sha256:e790602d7ea1d6c7d8713d571226d67de7ffe47b1e22ae2c043ebd537de1bccb", size = 585281 }, + { url = "https://files.pythonhosted.org/packages/33/b2/6c355e8ca7f2ff920a5ba221732722304aaebad919109754753e678404a3/pyzmq-26.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:717960855f2d6fdc2dba9df49dff31c414187bb11c76af36343a57d1f7083d9a", size = 651006 }, + { url = "https://files.pythonhosted.org/packages/8f/7a/0187ae651393fc82fdd841581929b17509252f68b799bb787de4e48e7181/pyzmq-26.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:08956c26dbcd4fd8835cb777a16e21958ed2412317630e19f0018d49dbeeb470", size = 552828 }, + { url = "https://files.pythonhosted.org/packages/9b/b6/210ff26d3dae4ba8d0b9c0dca3299d8d7273b54f5a74a16ecd1f02c4cdd5/pyzmq-26.1.1-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:e80345900ae241c2c51bead7c9fa247bba6d4b2a83423e9791bae8b0a7f12c52", size = 1343185 }, + { url = "https://files.pythonhosted.org/packages/05/23/5c74b72effed61c4087a3b549c22e4023e7ddac239ab50687733ec0ed9a6/pyzmq-26.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ec8fe214fcc45dfb0c32e4a7ad1db20244ba2d2fecbf0cbf9d5242d81ca0a375", size = 1008446 }, + { url = "https://files.pythonhosted.org/packages/c0/be/80ee4eb79b3ba87398cca66c4446d660b5e301a9d938a88d7894181fe98a/pyzmq-26.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf4e283f97688d993cb7a8acbc22889effbbb7cbaa19ee9709751f44be928f5d", size = 665972 }, + { url = "https://files.pythonhosted.org/packages/19/c0/41b74b114d9ae13db6a5f414feaddf1b39b40603bc0db59f6572115cf92c/pyzmq-26.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2508bdc8ab246e5ed7c92023d4352aaad63020ca3b098a4e3f1822db202f703d", size = 903458 }, + { url = "https://files.pythonhosted.org/packages/f1/55/e5ba8f4baa7695c12a0b69baaecc3c3efac17c3a4d268a9b3400bdfa1e25/pyzmq-26.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:741bdb4d96efe8192616abdc3671931d51a8bcd38c71da2d53fb3127149265d1", size = 860090 }, + { url = "https://files.pythonhosted.org/packages/16/5c/e5043f955844c384e7daef810618893b63b57039f3116b71b9ff9f2609db/pyzmq-26.1.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:76154943e4c4054b2591792eb3484ef1dd23d59805759f9cebd2f010aa30ee8c", size = 860638 }, + { url = "https://files.pythonhosted.org/packages/75/9b/3c6e620db4f300057937f26b1b0f1233f4a043393aa1ae1fceefee1ba174/pyzmq-26.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9498ac427d20d0e0ef0e4bbd6200841e91640dfdf619f544ceec7f464cfb6070", size = 1196306 }, + { url = "https://files.pythonhosted.org/packages/95/06/af96f2ebe638872af78e25f13fdfe43df1d6e8dc668f2a978ef4369318c6/pyzmq-26.1.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f34453ef3496ca3462f30435bf85f535f9550392987341f9ccc92c102825a79", size = 1507502 }, + { url = "https://files.pythonhosted.org/packages/9f/f0/91f53f61d0e69b6c551ebe48fccc13a0f04cceaa064e1394b5d58048838b/pyzmq-26.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:50f0669324e27cc2091ef6ab76ca7112f364b6249691790b4cffce31e73fda28", size = 1406558 }, + { url = "https://files.pythonhosted.org/packages/1a/75/995c5ebb4bf06447d477a90ac090e78a39eb482567462bfa88e6351fc4ba/pyzmq-26.1.1-cp312-cp312-win32.whl", hash = "sha256:3ee5cbf2625b94de21c68d0cefd35327c8dfdbd6a98fcc41682b4e8bb00d841f", size = 584275 }, + { url = "https://files.pythonhosted.org/packages/dc/08/3e37b0c3c5e4a554e3aface4d6cf272a1b0156e376c5e667725c767ad4be/pyzmq-26.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:75bd448a28b1001b6928679015bc95dd5f172703ed30135bb9e34fc9cda0a3e7", size = 646907 }, + { url = "https://files.pythonhosted.org/packages/76/54/08e0ab926a2228a3285eec873574ab100c25a86c84844bda933048d97b80/pyzmq-26.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:4350233569b4bbef88595c5e77ee38995a6f1f1790fae148b578941bfffd1c24", size = 548851 }, + { url = "https://files.pythonhosted.org/packages/0e/01/92221845d28c7e0f7432cfaa2babbcf4bda5df1803402e063d17a8fbdc15/pyzmq-26.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8087a3281c20b1d11042d372ed5a47734af05975d78e4d1d6e7bd1018535f3", size = 1006634 }, + { url = "https://files.pythonhosted.org/packages/7b/cc/acce3be8787fb316d52402f58340c2bf288d24b3242dff4c9c4c0c597f99/pyzmq-26.1.1-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:ebef7d3fe11fe4c688f08bc0211a976c3318c097057f258428200737b9fff4da", size = 1340519 }, + { url = "https://files.pythonhosted.org/packages/65/aa/49a4f33dc23982eb3edd197e099f1ca67be251afb0e23388adb0f6253aab/pyzmq-26.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a5342110510045a47de1e87f5f1dcc1d9d90109522316dc9830cfc6157c800f", size = 665538 }, + { url = "https://files.pythonhosted.org/packages/ca/f8/2181c0f52344da3ffcc0a7888c21be77775480ce21a720715284c51f398b/pyzmq-26.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af690ea4be6ca92a67c2b44a779a023bf0838e92d48497a2268175dc4a505691", size = 903542 }, + { url = "https://files.pythonhosted.org/packages/31/ef/7497fbb7738db2dc93d6a04e42ddd240567d5ff7270f52b934b58536805e/pyzmq-26.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc994e220c1403ae087d7f0fa45129d583e46668a019e389060da811a5a9320e", size = 860042 }, + { url = "https://files.pythonhosted.org/packages/ae/b2/ce67ad15dac58d4d2e8747dee6211bf761f620394cd51a59d40fa8ff2727/pyzmq-26.1.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b8e153f5dffb0310af71fc6fc9cd8174f4c8ea312c415adcb815d786fee78179", size = 860391 }, + { url = "https://files.pythonhosted.org/packages/53/49/65a008ba7b9101d163abbcce43e914d620c6d47763d4abeab522fc1bd501/pyzmq-26.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0065026e624052a51033857e5cd45a94b52946b44533f965f0bdf182460e965d", size = 1196235 }, + { url = "https://files.pythonhosted.org/packages/49/ae/43ca5a12eaae55ffe76ef5c0a21bb5ea2e9f29bb2810a2fe747e2d173372/pyzmq-26.1.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:63351392f948b5d50b9f55161994bc4feedbfb3f3cfe393d2f503dea2c3ec445", size = 1507727 }, + { url = "https://files.pythonhosted.org/packages/5e/19/7f1d1c4777742c5abadbccfac64b170b63f003ef391d7f87de7a0ac88cbd/pyzmq-26.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ffecc43b3c18e36b62fcec995761829b6ac325d8dd74a4f2c5c1653afbb4495a", size = 1406599 }, + { url = "https://files.pythonhosted.org/packages/f9/cd/5feb4af7cb3839ba6a62c284398e5777e2fb61e52236d95931093d759a4d/pyzmq-26.1.1-cp313-cp313-win32.whl", hash = "sha256:6ff14c2fae6c0c2c1c02590c5c5d75aa1db35b859971b3ca2fcd28f983d9f2b6", size = 584230 }, + { url = "https://files.pythonhosted.org/packages/ec/bc/d34e344b4e4c2c10f76da0c1a5b1f8bcef48c86e1972bfbe9f7d6ef1eaf5/pyzmq-26.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:85f2d2ee5ea9a8f1de86a300e1062fbab044f45b5ce34d20580c0198a8196db0", size = 646848 }, + { url = "https://files.pythonhosted.org/packages/46/26/9bed841b00d372083730bcb8eeb86f2ee0beff456ff07ff3eb0e92aa087a/pyzmq-26.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:cc09b1de8b985ca5a0ca343dd7fb007267c6b329347a74e200f4654268084239", size = 548564 }, + { url = "https://files.pythonhosted.org/packages/1d/36/08357e1e4df430313292b908fc7338f818ac42d3860b6d38a307fd39a205/pyzmq-26.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:bc904e86de98f8fc5bd41597da5d61232d2d6d60c4397f26efffabb961b2b245", size = 1007452 }, + { url = "https://files.pythonhosted.org/packages/99/f9/69a8d2010fa8dbb719b78f7c1c68d1e8d414c9a9e51a22c872624dda5231/pyzmq-26.1.1-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:00f39c367bbd6aa8e4bc36af6510561944c619b58eb36199fa334b594a18f615", size = 1329613 }, + { url = "https://files.pythonhosted.org/packages/0f/02/a9477dd620115ca3f5f2e90bdd2ab84236808ee510d20136bb8103204193/pyzmq-26.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de6f384864a959866b782e6a3896538d1424d183f2d3c7ef079f71dcecde7284", size = 653296 }, + { url = "https://files.pythonhosted.org/packages/d3/5d/f4e179aba55479648851b133b01e4546d3d06aa9a508f09cc7f3846c70fe/pyzmq-26.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3abb15df0c763339edb27a644c19381b2425ddd1aea3dbd77c1601a3b31867b8", size = 888472 }, + { url = "https://files.pythonhosted.org/packages/2b/ee/616c52d252267cf239e0061e91e67d75732e689ff53a9391637994e96d5b/pyzmq-26.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40908ec2dd3b29bbadc0916a0d3c87f8dbeebbd8fead8e618539f09e0506dec4", size = 845918 }, + { url = "https://files.pythonhosted.org/packages/78/b7/e09f159fe998cc6115fdc91665955e3ce2ac69c40b31aca25bf645b400a6/pyzmq-26.1.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c11a95d3f6fc7e714ccd1066f68f9c1abd764a8b3596158be92f46dd49f41e03", size = 847437 }, + { url = "https://files.pythonhosted.org/packages/c9/13/7b3e09e88e847cc05122284d8d0bb44d3293b54a899b2703b1d65b043695/pyzmq-26.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:4437af9fee7a58302dbd511cc49f0cc2b35c112a33a1111fb123cf0be45205ca", size = 1183545 }, + { url = "https://files.pythonhosted.org/packages/72/ca/89d6b6cc86b77fb8fa0d18662e1da4f8a1dada9304c26547fef1b2860336/pyzmq-26.1.1-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:76390d3d66406cb01b9681c382874400e9dfd77f30ecdea4bd1bf5226dd4aff0", size = 1492993 }, + { url = "https://files.pythonhosted.org/packages/ed/c3/ddc57994e7730a2840941228add6fe6c55d7e3199c9ca8266640cf8d53f8/pyzmq-26.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:4d4c7fe5e50e269f9c63a260638488fec194a73993008618a59b54c47ef6ae72", size = 1392544 }, + { url = "https://files.pythonhosted.org/packages/44/71/c1d407a442179359a7cf437aa4c94b1c0f31233181f05a76370bc4cc7f3c/pyzmq-26.1.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:be3fc2b11c0c384949cf1f01f9a48555039408b0f3e877863b1754225635953e", size = 907001 }, + { url = "https://files.pythonhosted.org/packages/79/fc/f550c6ccbf859e266b85a1a8daf3e93ce3a238e05413300c74610bfe9a78/pyzmq-26.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48dee75c2a9fa4f4a583d4028d564a0453447ee1277a29b07acc3743c092e259", size = 565754 }, + { url = "https://files.pythonhosted.org/packages/90/e5/eee9c82203d398664db7ed357efe89fb3fb7eb02aa383e052b9aa3e1b2da/pyzmq-26.1.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23f2fe4fb567e8098ebaa7204819658195b10ddd86958a97a6058eed2901eed3", size = 794374 }, + { url = "https://files.pythonhosted.org/packages/25/c5/404cbc8949e1f3ce785f23c7624a3502767f45df04a54b406625473fdb22/pyzmq-26.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:472cacd16f627c06d3c8b2d374345ab74446bae913584a6245e2aa935336d929", size = 752835 }, + { url = "https://files.pythonhosted.org/packages/06/59/aaf876e51d6307da4ffc3e870f699d65f4487913c80e926c05f5d8a30311/pyzmq-26.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8285b25aa20fcc46f1ca4afbc39fd3d5f2fe4c4bbf7f2c7f907a214e87a70024", size = 559602 }, +] + +[[package]] +name = "qdrant-client" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "grpcio-tools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httpx", extra = ["http2"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, + { name = "portalocker", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/70/5d033afb5a6f467a7cce4426a30a4113d76f6d6192b6ed0148e1847d6568/qdrant_client-1.11.0.tar.gz", hash = "sha256:7c1d4d7a96cfd1ee0cde2a21c607e9df86bcca795ad8d1fd274d295ab64b8458", size = 228713 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/08/6175fe5191e0c4adee7df1416ae361bc0d02d65ca9c1ce397679afa1484a/qdrant_client-1.11.0-py3-none-any.whl", hash = "sha256:1f574ccebb91c0bc8a620c9a41a5a010084fbc4d8c6f1cd0ab7b2eeb97336fc0", size = 258890 }, +] + +[[package]] +name = "redis" +version = "5.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "(python_full_version < '3.11.3' and sys_platform == 'darwin') or (python_full_version < '3.11.3' and sys_platform == 'linux') or (python_full_version < '3.11.3' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/10/defc227d65ea9c2ff5244645870859865cba34da7373477c8376629746ec/redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870", size = 4595651 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/d1/19a9c76811757684a0f74adc25765c8a901d67f9f6472ac9d57c844a23c8/redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4", size = 255608 }, +] + +[package.optional-dependencies] +hiredis = [ + { name = "hiredis", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "referencing" +version = "0.35.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "rpds-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/73ca1f8e72fff6fa52119dbd185f73a907b1989428917b24cff660129b6d/referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", size = 62991 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/59/2056f61236782a2c86b33906c025d4f4a0b17be0161b63b70fd9e8775d36/referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de", size = 26684 }, +] + +[[package]] +name = "regex" +version = "2024.7.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/51/64256d0dc72816a4fe3779449627c69ec8fee5a5625fd60ba048f53b3478/regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506", size = 393485 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/97/283bd32777e6c30a9bede976cd72ba4b9aa144dc0f0f462bd37fa1a86e01/regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce", size = 470812 }, + { url = "https://files.pythonhosted.org/packages/e4/80/80bc4d7329d04ba519ebcaf26ae21d9e30d33934c458691177c623ceff70/regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024", size = 282129 }, + { url = "https://files.pythonhosted.org/packages/e5/8a/cddcb7942d05ad9a427ad97ab29f1a62c0607ab72bdb2f3a26fc5b07ac0f/regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd", size = 278909 }, + { url = "https://files.pythonhosted.org/packages/a6/d4/93b4011cb83f9a66e0fa398b4d3c6d564d94b686dace676c66502b13dae9/regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53", size = 777687 }, + { url = "https://files.pythonhosted.org/packages/d0/11/d0a12e1cecc1d35bbcbeb99e2ddcb8c1b152b1b58e2ff55f50c3d762b09e/regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca", size = 818982 }, + { url = "https://files.pythonhosted.org/packages/ae/41/01a073765d75427e24710af035d8f0a773b5cedf23f61b63e7ef2ce960d6/regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59", size = 804015 }, + { url = "https://files.pythonhosted.org/packages/3e/66/04b63f31580026c8b819aed7f171149177d10cfab27477ea8800a2268d50/regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41", size = 776517 }, + { url = "https://files.pythonhosted.org/packages/be/49/0c08a7a232e4e26e17afeedf13f331224d9377dde4876ed6e21e4a584a5d/regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5", size = 766860 }, + { url = "https://files.pythonhosted.org/packages/24/44/35769388845cdd7be97e1232a59446b738054b61bc9c92a3b0bacfaf7bb1/regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46", size = 692181 }, + { url = "https://files.pythonhosted.org/packages/50/be/4e09d5bc8de176153f209c95ca4e64b9def1748d693694a95dd4401ee7be/regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f", size = 762956 }, + { url = "https://files.pythonhosted.org/packages/90/63/b37152f25fe348aa31806bafa91df607d096e8f477fed9a5cf3de339dd5f/regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7", size = 771978 }, + { url = "https://files.pythonhosted.org/packages/ab/ac/38186431f7c1874e3f790669be933accf1090ee53aba0ab1a811ef38f07e/regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe", size = 840800 }, + { url = "https://files.pythonhosted.org/packages/e8/23/91b04dbf51a2c0ddf5b1e055e9e05ed091ebcf46f2b0e6e3d2fff121f903/regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce", size = 838991 }, + { url = "https://files.pythonhosted.org/packages/36/fd/822110cc14b99bdd7d8c61487bc774f454120cd3d7492935bf13f3399716/regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa", size = 767539 }, + { url = "https://files.pythonhosted.org/packages/82/54/e24a8adfca74f9a421cd47657c51413919e7755e729608de6f4c5556e002/regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66", size = 257712 }, + { url = "https://files.pythonhosted.org/packages/fb/cc/6485c2fc72d0de9b55392246b80921639f1be62bed1e33e982940306b5ba/regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e", size = 269661 }, + { url = "https://files.pythonhosted.org/packages/cb/ec/261f8434a47685d61e59a4ef3d9ce7902af521219f3ebd2194c7adb171a6/regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281", size = 470810 }, + { url = "https://files.pythonhosted.org/packages/f0/47/f33b1cac88841f95fff862476a9e875d9a10dae6912a675c6f13c128e5d9/regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b", size = 282126 }, + { url = "https://files.pythonhosted.org/packages/fc/1b/256ca4e2d5041c0aa2f1dc222f04412b796346ab9ce2aa5147405a9457b4/regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a", size = 278920 }, + { url = "https://files.pythonhosted.org/packages/91/03/4603ec057c0bafd2f6f50b0bdda4b12a0ff81022decf1de007b485c356a6/regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73", size = 785420 }, + { url = "https://files.pythonhosted.org/packages/75/f8/13b111fab93e6273e26de2926345e5ecf6ddad1e44c4d419d7b0924f9c52/regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2", size = 828164 }, + { url = "https://files.pythonhosted.org/packages/4a/80/bc3b9d31bd47ff578758af929af0ac1d6169b247e26fa6e87764007f3d93/regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e", size = 812621 }, + { url = "https://files.pythonhosted.org/packages/8b/77/92d4a14530900d46dddc57b728eea65d723cc9fcfd07b96c2c141dabba84/regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51", size = 786609 }, + { url = "https://files.pythonhosted.org/packages/35/58/06695fd8afad4c8ed0a53ec5e222156398b9fe5afd58887ab94ea68e4d16/regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364", size = 775290 }, + { url = "https://files.pythonhosted.org/packages/1b/0f/50b97ee1fc6965744b9e943b5c0f3740792ab54792df73d984510964ef29/regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee", size = 772849 }, + { url = "https://files.pythonhosted.org/packages/8f/64/565ff6cf241586ab7ae76bb4138c4d29bc1d1780973b457c2db30b21809a/regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c", size = 778428 }, + { url = "https://files.pythonhosted.org/packages/e5/fe/4ceabf4382e44e1e096ac46fd5e3bca490738b24157116a48270fd542e88/regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce", size = 849436 }, + { url = "https://files.pythonhosted.org/packages/68/23/1868e40d6b594843fd1a3498ffe75d58674edfc90d95e18dd87865b93bf2/regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1", size = 849484 }, + { url = "https://files.pythonhosted.org/packages/f3/52/bff76de2f6e2bc05edce3abeb7e98e6309aa022fc06071100a0216fbeb50/regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e", size = 776712 }, + { url = "https://files.pythonhosted.org/packages/f2/72/70ade7b0b5fe5c6df38fdfa2a5a8273e3ea6a10b772aa671b7e889e78bae/regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c", size = 257716 }, + { url = "https://files.pythonhosted.org/packages/04/4d/80e04f4e27ab0cbc9096e2d10696da6d9c26a39b60db52670fd57614fea5/regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52", size = 269662 }, + { url = "https://files.pythonhosted.org/packages/0f/26/f505782f386ac0399a9237571833f187414882ab6902e2e71a1ecb506835/regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86", size = 471748 }, + { url = "https://files.pythonhosted.org/packages/bb/1d/ea9a21beeb433dbfca31ab82867d69cb67ff8674af9fab6ebd55fa9d3387/regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad", size = 282841 }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c6182095baf0a10169c34e87133a8e73b2e816a80035669b1278e927685e/regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9", size = 279114 }, + { url = "https://files.pythonhosted.org/packages/72/58/b5161bf890b6ca575a25685f19a4a3e3b6f4a072238814f8658123177d84/regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289", size = 789749 }, + { url = "https://files.pythonhosted.org/packages/09/fb/5381b19b62f3a3494266be462f6a015a869cf4bfd8e14d6e7db67e2c8069/regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9", size = 831666 }, + { url = "https://files.pythonhosted.org/packages/3d/6d/2a21c85f970f9be79357d12cf4b97f4fc6bf3bf6b843c39dabbc4e5f1181/regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c", size = 817544 }, + { url = "https://files.pythonhosted.org/packages/f9/ae/5f23e64f6cf170614237c654f3501a912dfb8549143d4b91d1cd13dba319/regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440", size = 790854 }, + { url = "https://files.pythonhosted.org/packages/29/0a/d04baad1bbc49cdfb4aef90c4fc875a60aaf96d35a1616f1dfe8149716bc/regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610", size = 779242 }, + { url = "https://files.pythonhosted.org/packages/3a/27/b242a962f650c3213da4596d70e24c7c1c46e3aa0f79f2a81164291085f8/regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5", size = 776932 }, + { url = "https://files.pythonhosted.org/packages/9c/ae/de659bdfff80ad2c0b577a43dd89dbc43870a4fc4bbf604e452196758e83/regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799", size = 784521 }, + { url = "https://files.pythonhosted.org/packages/d4/ac/eb6a796da0bdefbf09644a7868309423b18d344cf49963a9d36c13502d46/regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05", size = 854548 }, + { url = "https://files.pythonhosted.org/packages/56/77/fde8d825dec69e70256e0925af6c81eea9acf0a634d3d80f619d8dcd6888/regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94", size = 853345 }, + { url = "https://files.pythonhosted.org/packages/ff/04/2b79ad0bb9bc05ab4386caa2c19aa047a66afcbdfc2640618ffc729841e4/regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38", size = 781414 }, + { url = "https://files.pythonhosted.org/packages/bf/71/d0af58199283ada7d25b20e416f5b155f50aad99b0e791c0966ff5a1cd00/regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc", size = 258125 }, + { url = "https://files.pythonhosted.org/packages/95/b3/10e875c45c60b010b66fc109b899c6fc4f05d485fe1d54abff98ce791124/regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908", size = 269162 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "charset-normalizer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490 }, +] + +[[package]] +name = "rich" +version = "13.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", size = 221248 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", size = 240681 }, +] + +[[package]] +name = "rpds-py" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/64/b693f262791b818880d17268f3f8181ef799b0d187f6f731b1772e05a29a/rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121", size = 25814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/2d/a7e60483b72b91909e18f29a5c5ae847bac4e2ae95b77bb77e1f41819a58/rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2", size = 318432 }, + { url = "https://files.pythonhosted.org/packages/b5/b4/f15b0c55a6d880ce74170e7e28c3ed6c5acdbbd118df50b91d1dabf86008/rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f", size = 311333 }, + { url = "https://files.pythonhosted.org/packages/36/10/3f4e490fe6eb069c07c22357d0b4804cd94cb9f8d01345ef9b1d93482b9d/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150", size = 366697 }, + { url = "https://files.pythonhosted.org/packages/f5/c8/cd6ab31b4424c7fab3b17e153b6ea7d1bb0d7cabea5c1ef683cc8adb8bc2/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e", size = 368386 }, + { url = "https://files.pythonhosted.org/packages/60/5e/642a44fda6dda90b5237af7a0ef1d088159c30a504852b94b0396eb62125/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2", size = 395374 }, + { url = "https://files.pythonhosted.org/packages/7c/b5/ff18c093c9e72630f6d6242e5ccb0728ef8265ba0a154b5972f89d23790a/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3", size = 433189 }, + { url = "https://files.pythonhosted.org/packages/4a/6d/1166a157b227f2333f8e8ae320b6b7ea2a6a38fbe7a3563ad76dffc8608d/rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf", size = 354849 }, + { url = "https://files.pythonhosted.org/packages/70/a4/70ea49863ea09ae4c2971f2eef58e80b757e3c0f2f618c5815bb751f7847/rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140", size = 373233 }, + { url = "https://files.pythonhosted.org/packages/3b/d3/822a28152a1e7e2ba0dc5d06cf8736f4cd64b191bb6ec47fb51d1c3c5ccf/rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f", size = 541852 }, + { url = "https://files.pythonhosted.org/packages/c6/a5/6ef91e4425dc8b3445ff77d292fc4c5e37046462434a0423c4e0a596a8bd/rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce", size = 547630 }, + { url = "https://files.pythonhosted.org/packages/72/f8/d5625ee05c4e5c478954a16d9359069c66fe8ac8cd5ddf28f80d3b321837/rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94", size = 525766 }, + { url = "https://files.pythonhosted.org/packages/94/3c/1ff1ed6ae323b3e16fdfcdae0f0a67f373a6c3d991229dc32b499edeffb7/rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee", size = 199174 }, + { url = "https://files.pythonhosted.org/packages/ec/ba/5762c0aee2403dfea14ed74b0f8a2415cfdbb21cf745d600d9a8ac952c5b/rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399", size = 213543 }, + { url = "https://files.pythonhosted.org/packages/ab/2a/191374c52d7be0b056cc2a04d718d2244c152f915d4a8d2db2aacc526189/rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489", size = 318369 }, + { url = "https://files.pythonhosted.org/packages/0e/6a/2c9fdcc6d235ac0d61ec4fd9981184689c3e682abd05e3caa49bccb9c298/rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318", size = 311303 }, + { url = "https://files.pythonhosted.org/packages/d2/b2/725487d29633f64ef8f9cbf4729111a0b61702c8f8e94db1653930f52cce/rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db", size = 366424 }, + { url = "https://files.pythonhosted.org/packages/7a/8c/668195ab9226d01b7cf7cd9e59c1c0be1df05d602df7ec0cf46f857dcf59/rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5", size = 368359 }, + { url = "https://files.pythonhosted.org/packages/52/28/356f6a39c1adeb02cf3e5dd526f5e8e54e17899bef045397abcfbf50dffa/rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5", size = 394886 }, + { url = "https://files.pythonhosted.org/packages/a2/65/640fb1a89080a8fb6f4bebd3dafb65a2edba82e2e44c33e6eb0f3e7956f1/rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6", size = 432416 }, + { url = "https://files.pythonhosted.org/packages/a7/e8/85835077b782555d6b3416874b702ea6ebd7db1f145283c9252968670dd5/rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209", size = 354819 }, + { url = "https://files.pythonhosted.org/packages/4f/87/1ac631e923d65cbf36fbcfc6eaa702a169496de1311e54be142f178e53ee/rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3", size = 373282 }, + { url = "https://files.pythonhosted.org/packages/e4/ce/cb316f7970189e217b998191c7cf0da2ede3d5437932c86a7210dc1e9994/rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272", size = 541540 }, + { url = "https://files.pythonhosted.org/packages/90/d7/4112d7655ec8aff168ecc91d4ceb51c557336edde7e6ccf6463691a2f253/rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad", size = 547640 }, + { url = "https://files.pythonhosted.org/packages/ab/44/4f61d64dfed98cc71623f3a7fcb612df636a208b4b2c6611eaa985e130a9/rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58", size = 525555 }, + { url = "https://files.pythonhosted.org/packages/35/f2/a862d81eacb21f340d584cd1c749c289979f9a60e9229f78bffc0418a199/rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0", size = 199338 }, + { url = "https://files.pythonhosted.org/packages/cc/ec/77d0674f9af4872919f3738018558dd9d37ad3f7ad792d062eadd4af7cba/rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c", size = 213585 }, + { url = "https://files.pythonhosted.org/packages/89/b7/f9682c5cc37fcc035f4a0fc33c1fe92ec9cbfdee0cdfd071cf948f53e0df/rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6", size = 321468 }, + { url = "https://files.pythonhosted.org/packages/b8/ad/fc82be4eaceb8d444cb6fc1956ce972b3a0795104279de05e0e4131d0a47/rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b", size = 313062 }, + { url = "https://files.pythonhosted.org/packages/0e/1c/6039e80b13a08569a304dc13476dc986352dca4598e909384db043b4e2bb/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739", size = 370168 }, + { url = "https://files.pythonhosted.org/packages/dc/c9/5b9aa35acfb58946b4b785bc8e700ac313669e02fb100f3efa6176a83e81/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c", size = 371376 }, + { url = "https://files.pythonhosted.org/packages/7b/dd/0e0dbeb70d8a5357d2814764d467ded98d81d90d3570de4fb05ec7224f6b/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee", size = 397200 }, + { url = "https://files.pythonhosted.org/packages/e4/da/a47d931eb688ccfd77a7389e45935c79c41e8098d984d87335004baccb1d/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96", size = 426824 }, + { url = "https://files.pythonhosted.org/packages/0f/f7/a59a673594e6c2ff2dbc44b00fd4ecdec2fc399bb6a7bd82d612699a0121/rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4", size = 357967 }, + { url = "https://files.pythonhosted.org/packages/5f/61/3ba1905396b2cb7088f9503a460b87da33452da54d478cb9241f6ad16d00/rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef", size = 378905 }, + { url = "https://files.pythonhosted.org/packages/08/31/6d0df9356b4edb0a3a077f1ef714e25ad21f9f5382fc490c2383691885ea/rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821", size = 546348 }, + { url = "https://files.pythonhosted.org/packages/ae/15/d33c021de5cb793101df9961c3c746dfc476953dbbf5db337d8010dffd4e/rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940", size = 553152 }, + { url = "https://files.pythonhosted.org/packages/70/2d/5536d28c507a4679179ab15aa0049440e4d3dd6752050fa0843ed11e9354/rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174", size = 528807 }, + { url = "https://files.pythonhosted.org/packages/e3/62/7ebe6ec0d3dd6130921f8cffb7e34afb7f71b3819aa0446a24c5e81245ec/rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139", size = 200993 }, + { url = "https://files.pythonhosted.org/packages/ec/2f/b938864d66b86a6e4acadefdc56de75ef56f7cafdfd568a6464605457bd5/rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585", size = 214458 }, + { url = "https://files.pythonhosted.org/packages/99/32/43b919a0a423c270a838ac2726b1c7168b946f2563fd99a51aaa9692d00f/rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29", size = 321465 }, + { url = "https://files.pythonhosted.org/packages/58/a9/c4d899cb28e9e47b0ff12462e8f827381f243176036f17bef9c1604667f2/rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91", size = 312900 }, + { url = "https://files.pythonhosted.org/packages/8f/90/9e51670575b5dfaa8c823369ef7d943087bfb73d4f124a99ad6ef19a2b26/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24", size = 370973 }, + { url = "https://files.pythonhosted.org/packages/fc/c1/523f2a03f853fc0d4c1acbef161747e9ab7df0a8abf6236106e333540921/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7", size = 370890 }, + { url = "https://files.pythonhosted.org/packages/51/ca/2458a771f16b0931de4d384decbe43016710bc948036c8f4562d6e063437/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9", size = 397174 }, + { url = "https://files.pythonhosted.org/packages/00/7d/6e06807f6305ea2408b364efb0eef83a6e21b5e7b5267ad6b473b9a7e416/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8", size = 426449 }, + { url = "https://files.pythonhosted.org/packages/8c/d1/6c9e65260a819a1714510a7d69ac1d68aa23ee9ce8a2d9da12187263c8fc/rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879", size = 357698 }, + { url = "https://files.pythonhosted.org/packages/5d/fb/ecea8b5286d2f03eec922be7173a03ed17278944f7c124348f535116db15/rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f", size = 378530 }, + { url = "https://files.pythonhosted.org/packages/e3/e3/ac72f858957f52a109c588589b73bd2fad4a0fc82387fb55fb34aeb0f9cd/rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c", size = 545753 }, + { url = "https://files.pythonhosted.org/packages/b2/a4/a27683b519d5fc98e4390a3b130117d80fd475c67aeda8aac83c0e8e326a/rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2", size = 552443 }, + { url = "https://files.pythonhosted.org/packages/a1/ed/c074d248409b4432b1ccb2056974175fa0af2d1bc1f9c21121f80a358fa3/rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57", size = 528380 }, + { url = "https://files.pythonhosted.org/packages/d5/bd/04caf938895d2d78201e89c0c8a94dfd9990c34a19ff52fb01d0912343e3/rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a", size = 200540 }, + { url = "https://files.pythonhosted.org/packages/95/cc/109eb8b9863680411ae703664abacaa035820c7755acc9686d5dd02cdd2e/rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2", size = 214111 }, + { url = "https://files.pythonhosted.org/packages/06/39/bf1f664c347c946ef56cecaa896e3693d91acc741afa78ebb3fdb7aba08b/rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045", size = 319444 }, + { url = "https://files.pythonhosted.org/packages/c1/71/876135d3cb90d62468540b84e8e83ff4dc92052ab309bfdea7ea0b9221ad/rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc", size = 311699 }, + { url = "https://files.pythonhosted.org/packages/f7/da/8ccaeba6a3dda7467aebaf893de9eafd56275e2c90773c83bf15fb0b8374/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02", size = 367825 }, + { url = "https://files.pythonhosted.org/packages/04/b6/02a54c47c178d180395b3c9a8bfb3b93906e08f9acf7b4a1067d27c3fae0/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92", size = 369046 }, + { url = "https://files.pythonhosted.org/packages/a7/64/df4966743aa4def8727dc13d06527c8b13eb7412c1429def2d4701bee520/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d", size = 395896 }, + { url = "https://files.pythonhosted.org/packages/6f/d9/7ff03ff3642c600f27ff94512bb158a8d815fea5ed4162c75a7e850d6003/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855", size = 432427 }, + { url = "https://files.pythonhosted.org/packages/b8/c6/e1b886f7277b3454e55e85332e165091c19114eecb5377b88d892fd36ccf/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511", size = 355403 }, + { url = "https://files.pythonhosted.org/packages/e2/62/e26bd5b944e547c7bfd0b6ca7e306bfa430f8bd298ab72a1217976a7ca8d/rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51", size = 374491 }, + { url = "https://files.pythonhosted.org/packages/c3/92/93c5a530898d3a5d1ce087455071ba714b77806ed9ffee4070d0c7a53b7e/rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075", size = 543622 }, + { url = "https://files.pythonhosted.org/packages/01/9e/d68fba289625b5d3c9d1925825d7da716fbf812bda2133ac409021d5db13/rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60", size = 548558 }, + { url = "https://files.pythonhosted.org/packages/bf/d6/4b2fad4898154365f0f2bd72ffd190349274a4c1d6a6f94f02a83bb2b8f1/rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344", size = 525753 }, + { url = "https://files.pythonhosted.org/packages/d2/ea/6f121d1802f3adae1981aea4209ea66f9d3c7f2f6d6b85ef4f13a61d17ef/rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989", size = 213529 }, +] + +[[package]] +name = "rsa" +version = "4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.18.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ruamel-yaml-clib", marker = "(python_full_version < '3.13' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (python_full_version < '3.13' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_python_implementation == 'CPython' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/81/4dfc17eb6ebb1aac314a3eb863c1325b907863a1b8b1382cdffcb6ac0ed9/ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b", size = 143362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/67/8ece580cc363331d9a53055130f86b096bf16e38156e33b1d3014fffda6b/ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", size = 117761 }, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/ab/bab9eb1566cd16f060b54055dd39cf6a34bfa0240c53a7218c43e974295b/ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512", size = 213824 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/01/37ac131614f71b98e9b148b2d7790662dcee92217d2fb4bac1aa377def33/ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d", size = 148236 }, + { url = "https://files.pythonhosted.org/packages/61/ee/4874c9fc96010fce85abefdcbe770650c5324288e988d7a48b527a423815/ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462", size = 133996 }, + { url = "https://files.pythonhosted.org/packages/d3/62/c60b034d9a008bbd566eeecf53a5a4c73d191c8de261290db6761802b72d/ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412", size = 526680 }, + { url = "https://files.pythonhosted.org/packages/90/8c/6cdb44f548b29eb6328b9e7e175696336bc856de2ff82e5776f860f03822/ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f", size = 605853 }, + { url = "https://files.pythonhosted.org/packages/88/30/fc45b45d5eaf2ff36cffd215a2f85e9b90ac04e70b97fd4097017abfb567/ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334", size = 655206 }, + { url = "https://files.pythonhosted.org/packages/af/dc/133547f90f744a0c827bac5411d84d4e81da640deb3af1459e38c5f3b6a0/ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d", size = 689649 }, + { url = "https://files.pythonhosted.org/packages/23/1d/589139191b187a3c750ae8d983c42fd799246d5f0dd84451a0575c9bdbe9/ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d", size = 100044 }, + { url = "https://files.pythonhosted.org/packages/4f/5b/744df20285a75ac4c606452ce9a0fcc42087d122f42294518ded1017697c/ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31", size = 117825 }, + { url = "https://files.pythonhosted.org/packages/b1/15/971b385c098e8d0d170893f5ba558452bb7b776a0c90658b8f4dd0e3382b/ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069", size = 148870 }, + { url = "https://files.pythonhosted.org/packages/01/b0/4ddef56e9f703d7909febc3a421d709a3482cda25826816ec595b73e3847/ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248", size = 134475 }, + { url = "https://files.pythonhosted.org/packages/a4/f7/22d6b620ed895a05d40802d8281eff924dc6190f682d933d4efff60db3b5/ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b", size = 544020 }, + { url = "https://files.pythonhosted.org/packages/7c/e4/0d19d65e340f93df1c47f323d95fa4b256bb28320290f5fddef90837853a/ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe", size = 642643 }, + { url = "https://files.pythonhosted.org/packages/c9/ff/f781eb5e2ae011e586d5426e2086a011cf1e0f59704a6cad1387975c5a62/ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899", size = 695832 }, + { url = "https://files.pythonhosted.org/packages/e3/41/f62e67ac651358b8f0d60cfb12ab2daf99b1b69eeaa188d0cec809d943a6/ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9", size = 730923 }, + { url = "https://files.pythonhosted.org/packages/9f/f0/19ab8acbf983cd1b37f47d27ceb8b10a738d60d36316a54bad57e0d73fbb/ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7", size = 99999 }, + { url = "https://files.pythonhosted.org/packages/ec/54/d8a795997921d87224c65d44499ca595a833093fb215b133f920c1062956/ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb", size = 118008 }, + { url = "https://files.pythonhosted.org/packages/7a/a2/eb5e9d088cb9d15c24d956944c09dca0a89108ad6e2e913c099ef36e3f0d/ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1", size = 144636 }, + { url = "https://files.pythonhosted.org/packages/66/98/8de4f22bbfd9135deb3422e96d450c4bc0a57d38c25976119307d2efe0aa/ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2", size = 135684 }, + { url = "https://files.pythonhosted.org/packages/30/d3/5fe978cd01a61c12efd24d65fa68c6f28f28c8073a06cf11db3a854390ca/ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92", size = 734571 }, + { url = "https://files.pythonhosted.org/packages/55/b3/e2531a050758b717c969cbf76c103b75d8a01e11af931b94ba656117fbe9/ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62", size = 643946 }, + { url = "https://files.pythonhosted.org/packages/0d/aa/06db7ca0995b513538402e11280282c615b5ae5f09eb820460d35fb69715/ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9", size = 692169 }, + { url = "https://files.pythonhosted.org/packages/27/38/4cf4d482b84ecdf51efae6635cc5483a83cf5ca9d9c13e205a750e251696/ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d", size = 740325 }, + { url = "https://files.pythonhosted.org/packages/6f/67/c62c6eea53a4feb042727a3d6c18f50dc99683c2b199c06bd2a9e3db8e22/ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa", size = 98639 }, + { url = "https://files.pythonhosted.org/packages/10/d2/52a3d810d0b5b3720725c0504a27b3fced7b6f310fe928f7019d79387bc1/ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b", size = 115305 }, +] + +[[package]] +name = "ruff" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/7e/82271b5ecbb72f24178eac28979380c4ba234f90be5cf92cb513605efb1a/ruff-0.6.1.tar.gz", hash = "sha256:af3ffd8c6563acb8848d33cd19a69b9bfe943667f0419ca083f8ebe4224a3436", size = 2457325 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/d1/ac5091efcc8e2cdc55733ac07f17f961465318a3fa8916e44360e32e6c73/ruff-0.6.1-py3-none-linux_armv6l.whl", hash = "sha256:b4bb7de6a24169dc023f992718a9417380301b0c2da0fe85919f47264fb8add9", size = 9610279 }, + { url = "https://files.pythonhosted.org/packages/2b/ed/c3e1c20e46f5619f133e1ddafbb1a957407ea36d42a477d0d88e9897bed9/ruff-0.6.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45efaae53b360c81043e311cdec8a7696420b3d3e8935202c2846e7a97d4edae", size = 8719541 }, + { url = "https://files.pythonhosted.org/packages/13/49/3ee1c8dca59a8bd87ca833871d86304bce4348b2e019287e45ca0ad5b3dd/ruff-0.6.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bc60c7d71b732c8fa73cf995efc0c836a2fd8b9810e115be8babb24ae87e0850", size = 8320291 }, + { url = "https://files.pythonhosted.org/packages/2a/44/1fec4c3eac790a445f3b9e0759665439c1d88517851f3fca90e32e897d48/ruff-0.6.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c7477c3b9da822e2db0b4e0b59e61b8a23e87886e727b327e7dcaf06213c5cf", size = 10040885 }, + { url = "https://files.pythonhosted.org/packages/86/98/c0b96dda4f751accecd3c0638d8c617a3b3e6de11b4e68aa77cae72912fb/ruff-0.6.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a0af7ab3f86e3dc9f157a928e08e26c4b40707d0612b01cd577cc84b8905cc9", size = 9414183 }, + { url = "https://files.pythonhosted.org/packages/a0/e3/59ac3b2fb4e80f53a96f2c22951589357e22ef3bc2c2b04b2a73772663f8/ruff-0.6.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392688dbb50fecf1bf7126731c90c11a9df1c3a4cdc3f481b53e851da5634fa5", size = 10203467 }, + { url = "https://files.pythonhosted.org/packages/8d/02/3dc1c33877d68341b9764b30e2dcc9209b6adb8a0a41ca04d503dc39006e/ruff-0.6.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5278d3e095ccc8c30430bcc9bc550f778790acc211865520f3041910a28d0024", size = 10962198 }, + { url = "https://files.pythonhosted.org/packages/c5/1f/a36bb06c8b724e3a8ee59124657414182227a353a98408cb5321aa87bd13/ruff-0.6.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe6d5f65d6f276ee7a0fc50a0cecaccb362d30ef98a110f99cac1c7872df2f18", size = 10537682 }, + { url = "https://files.pythonhosted.org/packages/a2/bd/479fbfab1634f2527a3f5ddb44973977f75ffbdf3d9bb16748c558a263ad/ruff-0.6.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e0dd11e2ae553ee5c92a81731d88a9883af8db7408db47fc81887c1f8b672e", size = 11505616 }, + { url = "https://files.pythonhosted.org/packages/e0/94/92bc24e7e58d2f90fa2a370f763d25d9e06ccccfab839b88e389d79fb4e3/ruff-0.6.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d812615525a34ecfc07fd93f906ef5b93656be01dfae9a819e31caa6cfe758a1", size = 10221898 }, + { url = "https://files.pythonhosted.org/packages/f7/47/1aca18f02abd4a3ba739991b719a3aa5d8e39e0bee1a91090c8bfacdcd13/ruff-0.6.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faaa4060f4064c3b7aaaa27328080c932fa142786f8142aff095b42b6a2eb631", size = 10033784 }, + { url = "https://files.pythonhosted.org/packages/e6/48/df16d9b00af42034ee85915914783bc0529a2ff709d6d3ef39c7c15d826d/ruff-0.6.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99d7ae0df47c62729d58765c593ea54c2546d5de213f2af2a19442d50a10cec9", size = 9477381 }, + { url = "https://files.pythonhosted.org/packages/46/d6/d6eadedcc9f9c4927665eee26f4449c15f4c501e7ba9c34c37753748dc11/ruff-0.6.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9eb18dfd7b613eec000e3738b3f0e4398bf0153cb80bfa3e351b3c1c2f6d7b15", size = 9862269 }, + { url = "https://files.pythonhosted.org/packages/4e/30/e2f5b06ac048898a1cac190e1c9c0d88f984596b27f1069341217e42d119/ruff-0.6.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c62bc04c6723a81e25e71715aa59489f15034d69bf641df88cb38bdc32fd1dbb", size = 10287591 }, + { url = "https://files.pythonhosted.org/packages/a0/2c/6a17be1b3c69c03167e5b3d69317ae9b8b2a06091189161751e7a36afef5/ruff-0.6.1-py3-none-win32.whl", hash = "sha256:9fb4c4e8b83f19c9477a8745e56d2eeef07a7ff50b68a6998f7d9e2e3887bdc4", size = 7918031 }, + { url = "https://files.pythonhosted.org/packages/2e/ba/66a6c87f6532e0390ebc67d5ae9bc1064f4e14d1b0e224bdedc999ae2b15/ruff-0.6.1-py3-none-win_amd64.whl", hash = "sha256:c2ebfc8f51ef4aca05dad4552bbcf6fe8d1f75b2f6af546cc47cc1c1ca916b5b", size = 8736178 }, + { url = "https://files.pythonhosted.org/packages/14/da/418c5d40058ad56bd0fa060efa4580ccf446f916167aa6540d31f6844e16/ruff-0.6.1-py3-none-win_arm64.whl", hash = "sha256:3bc81074971b0ffad1bd0c52284b22411f02a11a012082a76ac6da153536e014", size = 8142791 }, +] + +[[package]] +name = "safetensors" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/5b/0e63bf736e171463481c5ea3406650dc25aa044083062d321820e7a1ef9f/safetensors-0.4.4.tar.gz", hash = "sha256:5fe3e9b705250d0172ed4e100a811543108653fb2b66b9e702a088ad03772a07", size = 69522 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/fa/bd12d51c70986156031c25eae2d092ad8ef8b5cadb4e684a78b620b28320/safetensors-0.4.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2adb497ada13097f30e386e88c959c0fda855a5f6f98845710f5bb2c57e14f12", size = 392399 }, + { url = "https://files.pythonhosted.org/packages/b7/1e/f146555161e21918e00726b2bff1e2517faa8b2953e53a5a45c5f5bef64e/safetensors-0.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7db7fdc2d71fd1444d85ca3f3d682ba2df7d61a637dfc6d80793f439eae264ab", size = 381919 }, + { url = "https://files.pythonhosted.org/packages/fb/f7/0c97595790f03ff86505c375cddf3a26b6d645ff2cbc819936287a66a744/safetensors-0.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4f0eed76b430f009fbefca1a0028ddb112891b03cb556d7440d5cd68eb89a9", size = 441235 }, + { url = "https://files.pythonhosted.org/packages/77/8b/0d1e055536f1c0ac137d446806d50d9d952bed85688d733a81913cf09367/safetensors-0.4.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d216fab0b5c432aabf7170883d7c11671622bde8bd1436c46d633163a703f6", size = 440000 }, + { url = "https://files.pythonhosted.org/packages/bd/85/3a73b4ff7a46dd7606f924ededc31468fd385221670d840005b8dbdb7a37/safetensors-0.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d9b76322e49c056bcc819f8bdca37a2daa5a6d42c07f30927b501088db03309", size = 477919 }, + { url = "https://files.pythonhosted.org/packages/dd/41/b832227d04a8b65b32e2be13dbe8212db0135514380148c9b81c1b08c023/safetensors-0.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32f0d1f6243e90ee43bc6ee3e8c30ac5b09ca63f5dd35dbc985a1fc5208c451a", size = 496838 }, + { url = "https://files.pythonhosted.org/packages/18/f3/27bf4d7112b194eea2d8401706953080692d37ace1b74b36fcc7234961cd/safetensors-0.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d464bdc384874601a177375028012a5f177f1505279f9456fea84bbc575c7f", size = 435539 }, + { url = "https://files.pythonhosted.org/packages/b1/98/d75bbdaca03d571e5e5e1ef600f3015cd5f9884126eb53a3377b4111fea1/safetensors-0.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63144e36209ad8e4e65384dbf2d52dd5b1866986079c00a72335402a38aacdc5", size = 457051 }, + { url = "https://files.pythonhosted.org/packages/03/e1/b7849306e47234ef548c2b32e65f2ffee0640bfad8c65e4dd37b6fee981c/safetensors-0.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:051d5ecd490af7245258000304b812825974d5e56f14a3ff7e1b8b2ba6dc2ed4", size = 619613 }, + { url = "https://files.pythonhosted.org/packages/e9/d9/cbf1316161d0a1b4b0aceeb16ddb396f49363133618cc062e4abd66b2ea9/safetensors-0.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51bc8429d9376224cd3cf7e8ce4f208b4c930cd10e515b6ac6a72cbc3370f0d9", size = 605422 }, + { url = "https://files.pythonhosted.org/packages/48/47/16ece1369794b9d3bc057a42fed0601779d21f57d0b0b1b671a78410d74d/safetensors-0.4.4-cp310-none-win32.whl", hash = "sha256:fb7b54830cee8cf9923d969e2df87ce20e625b1af2fd194222ab902d3adcc29c", size = 272398 }, + { url = "https://files.pythonhosted.org/packages/b4/a9/f28d4a8a082ef513755a1a2393a924999892142ed235aed57ab558cd1bc9/safetensors-0.4.4-cp310-none-win_amd64.whl", hash = "sha256:4b3e8aa8226d6560de8c2b9d5ff8555ea482599c670610758afdc97f3e021e9c", size = 285884 }, + { url = "https://files.pythonhosted.org/packages/0f/1b/27cea7a581019d0d674284048ff76e3a6e048bc3ae3c31cb0bfc93641180/safetensors-0.4.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bbaa31f2cb49013818bde319232ccd72da62ee40f7d2aa532083eda5664e85ff", size = 392373 }, + { url = "https://files.pythonhosted.org/packages/36/46/93c39c96188a88ca15d12759bb51f52ce7365f6fd19ef09580bc096e8860/safetensors-0.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9fdcb80f4e9fbb33b58e9bf95e7dbbedff505d1bcd1c05f7c7ce883632710006", size = 381488 }, + { url = "https://files.pythonhosted.org/packages/37/a2/93cab60b8e2c8ea6343a04cdd2c09c860c9640eaaffbf8b771a0e8f98e7d/safetensors-0.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55c14c20be247b8a1aeaf3ab4476265e3ca83096bb8e09bb1a7aa806088def4f", size = 441025 }, + { url = "https://files.pythonhosted.org/packages/19/37/2a5220dce5eff841328bfc3071f4a7063f3eb12341893b2688669fc67115/safetensors-0.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:949aaa1118660f992dbf0968487b3e3cfdad67f948658ab08c6b5762e90cc8b6", size = 439791 }, + { url = "https://files.pythonhosted.org/packages/f8/93/1d894ff44df26baf4c2471a5874388361390d3cb1cc4811cff40fc01373e/safetensors-0.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c11a4ab7debc456326a2bac67f35ee0ac792bcf812c7562a4a28559a5c795e27", size = 477752 }, + { url = "https://files.pythonhosted.org/packages/a5/17/b697f517c7ffb8d62d1ef17c6224c00edbb96b931e565d887476a51ac803/safetensors-0.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0cea44bba5c5601b297bc8307e4075535b95163402e4906b2e9b82788a2a6df", size = 496019 }, + { url = "https://files.pythonhosted.org/packages/af/b9/c33f69f4dad9c65209efb76c2be6968af5219e31ccfd344a0025d972252f/safetensors-0.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9d752c97f6bbe327352f76e5b86442d776abc789249fc5e72eacb49e6916482", size = 435416 }, + { url = "https://files.pythonhosted.org/packages/71/59/f6480a68df2f4fb5aefae45a800d9bc043c0549210075275fef190a896ce/safetensors-0.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03f2bb92e61b055ef6cc22883ad1ae898010a95730fa988c60a23800eb742c2c", size = 456771 }, + { url = "https://files.pythonhosted.org/packages/09/01/2a7507cdf7318fb68596e6537ef81e83cfc171c483b4a786b9c947368e19/safetensors-0.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:87bf3f91a9328a941acc44eceffd4e1f5f89b030985b2966637e582157173b98", size = 619456 }, + { url = "https://files.pythonhosted.org/packages/80/b3/4bb5b1fb025cb8c81fe8a76371334860a9c276fade616f83fd53feef2740/safetensors-0.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:20d218ec2b6899d29d6895419a58b6e44cc5ff8f0cc29fac8d236a8978ab702e", size = 605125 }, + { url = "https://files.pythonhosted.org/packages/09/93/0d6d54b84eff8361dc257fa306ae0ef1899025a2d9657efe8384ac8b7267/safetensors-0.4.4-cp311-none-win32.whl", hash = "sha256:8079486118919f600c603536e2490ca37b3dbd3280e3ad6eaacfe6264605ac8a", size = 272273 }, + { url = "https://files.pythonhosted.org/packages/21/4f/5ee44681c7ea827f9d3c104ca429865b41c05a4163eff7f0599152c2e682/safetensors-0.4.4-cp311-none-win_amd64.whl", hash = "sha256:2f8c2eb0615e2e64ee27d478c7c13f51e5329d7972d9e15528d3e4cfc4a08f0d", size = 285982 }, + { url = "https://files.pythonhosted.org/packages/e2/41/a491dbe3fc1c195ce648939a87d3b4b3800eaade2f05278a6dc02b575c51/safetensors-0.4.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:baec5675944b4a47749c93c01c73d826ef7d42d36ba8d0dba36336fa80c76426", size = 391372 }, + { url = "https://files.pythonhosted.org/packages/3a/a1/d99aa8d10fa8d82276ee2aaa87afd0a6b96e69c128eaa9f93524b52c5276/safetensors-0.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f15117b96866401825f3e94543145028a2947d19974429246ce59403f49e77c6", size = 381800 }, + { url = "https://files.pythonhosted.org/packages/c8/1c/4fa05b79afdd4688a357a42433565b5b09137af6b4f6cd0c9e371466e2f1/safetensors-0.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a13a9caea485df164c51be4eb0c87f97f790b7c3213d635eba2314d959fe929", size = 440817 }, + { url = "https://files.pythonhosted.org/packages/65/c0/152b059debd3cee4f44b7df972e915a38f776379ea99ce4a3cbea3f78dbd/safetensors-0.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b54bc4ca5f9b9bba8cd4fb91c24b2446a86b5ae7f8975cf3b7a277353c3127c", size = 439483 }, + { url = "https://files.pythonhosted.org/packages/9c/93/20c05daeecf6fa93b9403c3660df1d983d7ddd5cdb3e3710ff41b72754dd/safetensors-0.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:08332c22e03b651c8eb7bf5fc2de90044f3672f43403b3d9ac7e7e0f4f76495e", size = 476631 }, + { url = "https://files.pythonhosted.org/packages/84/2f/bfe3e54b7dbcaef3f10b8f3c71146790ab18b0bd79ad9ca2bc2c950b68df/safetensors-0.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb62841e839ee992c37bb75e75891c7f4904e772db3691c59daaca5b4ab960e1", size = 493575 }, + { url = "https://files.pythonhosted.org/packages/1b/0b/2a1b405131f26b95acdb3ed6c8e3a8c84de72d364fd26202d43e68ec4bad/safetensors-0.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5b927acc5f2f59547270b0309a46d983edc44be64e1ca27a7fcb0474d6cd67", size = 434891 }, + { url = "https://files.pythonhosted.org/packages/31/ce/cad390a08128ebcb74be79a1e03c496a4773059b2541c6a97a52fd1705fb/safetensors-0.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a69c71b1ae98a8021a09a0b43363b0143b0ce74e7c0e83cacba691b62655fb8", size = 457631 }, + { url = "https://files.pythonhosted.org/packages/9f/83/d9d6e6a45d624c27155f4336af8e7b2bcde346137f6460dcd5e1bcdc2e3f/safetensors-0.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23654ad162c02a5636f0cd520a0310902c4421aab1d91a0b667722a4937cc445", size = 619367 }, + { url = "https://files.pythonhosted.org/packages/9f/20/b37e1ae87cb83a1c2fe5cf0710bab12d6f186474cbbdda4fda2d7d57d225/safetensors-0.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0677c109d949cf53756859160b955b2e75b0eefe952189c184d7be30ecf7e858", size = 605302 }, + { url = "https://files.pythonhosted.org/packages/99/5a/9237f1d0adba5eec3711d7c1911b3111631a86779d692fe8ad2cd709d6a4/safetensors-0.4.4-cp312-none-win32.whl", hash = "sha256:a51d0ddd4deb8871c6de15a772ef40b3dbd26a3c0451bb9e66bc76fc5a784e5b", size = 273434 }, + { url = "https://files.pythonhosted.org/packages/b9/dd/b11f3a33fe7b6c94fde08b3de094b93d3438d67922ef90bcb5002e306e0b/safetensors-0.4.4-cp312-none-win_amd64.whl", hash = "sha256:2d065059e75a798bc1933c293b68d04d79b586bb7f8c921e0ca1e82759d0dbb1", size = 286347 }, + { url = "https://files.pythonhosted.org/packages/b3/d6/7a4db869a295b57066e1399eb467c38df86439d3766c850ca8eb75b5e3a3/safetensors-0.4.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9d625692578dd40a112df30c02a1adf068027566abd8e6a74893bb13d441c150", size = 391373 }, + { url = "https://files.pythonhosted.org/packages/1e/97/de856ad42ef65822ff982e7af7fc889cd717240672b45c647af7ea05c631/safetensors-0.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7cabcf39c81e5b988d0adefdaea2eb9b4fd9bd62d5ed6559988c62f36bfa9a89", size = 382523 }, + { url = "https://files.pythonhosted.org/packages/07/d2/d9316af4c15b4ca0362cb4498abe47be6e04f7119f3ccf697e38ee04d33b/safetensors-0.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8359bef65f49d51476e9811d59c015f0ddae618ee0e44144f5595278c9f8268c", size = 441039 }, + { url = "https://files.pythonhosted.org/packages/e8/ac/478e910c891feadb693316b31447f14929b7047a612df9b628589b89be3c/safetensors-0.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a32c662e7df9226fd850f054a3ead0e4213a96a70b5ce37b2d26ba27004e013", size = 439516 }, + { url = "https://files.pythonhosted.org/packages/81/43/f9929e854c4fcca98459f03de003d9619dd5f7d10d74e03df7af9907b119/safetensors-0.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c329a4dcc395364a1c0d2d1574d725fe81a840783dda64c31c5a60fc7d41472c", size = 477242 }, + { url = "https://files.pythonhosted.org/packages/0a/4d/b754f59fe395ea5bd8531c090c557e161fffed1753eeb3d87c0f8eaa62c4/safetensors-0.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:239ee093b1db877c9f8fe2d71331a97f3b9c7c0d3ab9f09c4851004a11f44b65", size = 494615 }, + { url = "https://files.pythonhosted.org/packages/54/7d/b26801dab2ecb499eb1ebdb46be65600b49bb062fe12b298150695a6e23c/safetensors-0.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd574145d930cf9405a64f9923600879a5ce51d9f315443a5f706374841327b6", size = 434933 }, + { url = "https://files.pythonhosted.org/packages/e2/40/0f6627ad98e21e620a6835f02729f6b701804d3c452f8773648cbd0b9c2c/safetensors-0.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f6784eed29f9e036acb0b7769d9e78a0dc2c72c2d8ba7903005350d817e287a4", size = 457646 }, + { url = "https://files.pythonhosted.org/packages/30/1e/7f7819d1be7c36fbedcb7099a461b79e0ed19631b3ca5595e0f81501bb2c/safetensors-0.4.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:65a4a6072436bf0a4825b1c295d248cc17e5f4651e60ee62427a5bcaa8622a7a", size = 619204 }, + { url = "https://files.pythonhosted.org/packages/b1/58/e91e8c9888303919ce56f038fcad4147431fd95630890799bf8c928d1d34/safetensors-0.4.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:df81e3407630de060ae8313da49509c3caa33b1a9415562284eaf3d0c7705f9f", size = 605400 }, + { url = "https://files.pythonhosted.org/packages/dd/fd/7a760367b62752e8c6d57c3759eaa57e5b47f55524bba3d803e03f922f95/safetensors-0.4.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1d1f34c71371f0e034004a0b583284b45d233dd0b5f64a9125e16b8a01d15067", size = 393406 }, + { url = "https://files.pythonhosted.org/packages/dd/21/628d56eeae4bd0dcb5b11a9ec4001a50d2f85b726b10a864f72f34ba486f/safetensors-0.4.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a8043a33d58bc9b30dfac90f75712134ca34733ec3d8267b1bd682afe7194f5", size = 383386 }, + { url = "https://files.pythonhosted.org/packages/19/27/699124b4c6c27b7860140bac7ee6c50bde104e55951f8f5163f9ad20faa9/safetensors-0.4.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8db8f0c59c84792c12661f8efa85de160f80efe16b87a9d5de91b93f9e0bce3c", size = 442158 }, + { url = "https://files.pythonhosted.org/packages/23/01/85a621bdded944d6800f654c823a00df513263f1921a96d67d7fceb2ffb9/safetensors-0.4.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfc1fc38e37630dd12d519bdec9dcd4b345aec9930bb9ce0ed04461f49e58b52", size = 436170 }, + { url = "https://files.pythonhosted.org/packages/4f/a3/b15adfffc6c8faaae6416f5c70ee4c64e4986b630b4ada18a314228a15e2/safetensors-0.4.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5c9d86d9b13b18aafa88303e2cd21e677f5da2a14c828d2c460fe513af2e9a5", size = 458196 }, + { url = "https://files.pythonhosted.org/packages/8c/c1/ca829972be495326b5a986fe15e2ef16ecc4c35959942555091938f457af/safetensors-0.4.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:43251d7f29a59120a26f5a0d9583b9e112999e500afabcfdcb91606d3c5c89e3", size = 620510 }, + { url = "https://files.pythonhosted.org/packages/e7/50/89e5eac4120b55422450d5221c86d526ace14e222ea3f6c0c005f8f011ec/safetensors-0.4.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:2c42e9b277513b81cf507e6121c7b432b3235f980cac04f39f435b7902857f91", size = 606993 }, +] + +[[package]] +name = "scikit-learn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, + { name = "scipy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "threadpoolctl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/72/2961b9874a9ddf2b0f95f329d4e67f67c3301c1d88ba5e239ff25661bb85/scikit_learn-1.5.1.tar.gz", hash = "sha256:0ea5d40c0e3951df445721927448755d3fe1d80833b0b7308ebff5d2a45e6414", size = 6958368 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/59/d8ea8c05e61d2afa988dfcfe47526595b531e94d23babf58d2e00a35f646/scikit_learn-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:781586c414f8cc58e71da4f3d7af311e0505a683e112f2f62919e3019abd3745", size = 12102257 }, + { url = "https://files.pythonhosted.org/packages/1f/c6/ba8e5691acca616adc8f0d6f8f5e79d55b927530aa404ee712b077acf0cf/scikit_learn-1.5.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f5b213bc29cc30a89a3130393b0e39c847a15d769d6e59539cd86b75d276b1a7", size = 10975310 }, + { url = "https://files.pythonhosted.org/packages/5c/c6/e362563cc7dfe37e4699cbf2b2d22c2854be227c254976de1c4854fc6e84/scikit_learn-1.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ff4ba34c2abff5ec59c803ed1d97d61b036f659a17f55be102679e88f926fac", size = 12496508 }, + { url = "https://files.pythonhosted.org/packages/f2/60/6c589c91e474721efdcec82ea9cc5c743359e52637e46c364ee5236666ef/scikit_learn-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:161808750c267b77b4a9603cf9c93579c7a74ba8486b1336034c2f1579546d21", size = 13352348 }, + { url = "https://files.pythonhosted.org/packages/f1/13/de29b945fb28fc0c24159d3a83f1250c5232c1c9abac12434c7c3447e9cc/scikit_learn-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:10e49170691514a94bb2e03787aa921b82dbc507a4ea1f20fd95557862c98dc1", size = 10966250 }, + { url = "https://files.pythonhosted.org/packages/03/86/ab9f95e338c5ef5b4e79463ee91e55aae553213835e59bf038bc0cc21bf8/scikit_learn-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:154297ee43c0b83af12464adeab378dee2d0a700ccd03979e2b821e7dd7cc1c2", size = 12087598 }, + { url = "https://files.pythonhosted.org/packages/7d/d7/fb80c63062b60b1fa5dcb2d4dd3a4e83bd8c68cdc83cf6ff8c016228f184/scikit_learn-1.5.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b5e865e9bd59396220de49cb4a57b17016256637c61b4c5cc81aaf16bc123bbe", size = 10979067 }, + { url = "https://files.pythonhosted.org/packages/c1/f8/fd3fa610cac686952d8c78b8b44cf5263c6c03885bd8e5d5819c684b44e8/scikit_learn-1.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909144d50f367a513cee6090873ae582dba019cb3fca063b38054fa42704c3a4", size = 12485469 }, + { url = "https://files.pythonhosted.org/packages/32/63/ed228892adad313aab0d0f9261241e7bf1efe36730a2788ad424bcad00ca/scikit_learn-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:689b6f74b2c880276e365fe84fe4f1befd6a774f016339c65655eaff12e10cbf", size = 13335048 }, + { url = "https://files.pythonhosted.org/packages/5d/55/0403bf2031250ac982c8053397889fbc5a3a2b3798b913dae4f51c3af6a4/scikit_learn-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:9a07f90846313a7639af6a019d849ff72baadfa4c74c778821ae0fad07b7275b", size = 10988436 }, + { url = "https://files.pythonhosted.org/packages/b1/8d/cf392a56e24627093a467642c8b9263052372131359b570df29aaf4811ab/scikit_learn-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5944ce1faada31c55fb2ba20a5346b88e36811aab504ccafb9f0339e9f780395", size = 12102404 }, + { url = "https://files.pythonhosted.org/packages/d5/2c/734fc9269bdb6768905ac41b82d75264b26925b1e462f4ebf45fe4f17646/scikit_learn-1.5.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0828673c5b520e879f2af6a9e99eee0eefea69a2188be1ca68a6121b809055c1", size = 11037398 }, + { url = "https://files.pythonhosted.org/packages/d3/a9/15774b178bcd1cde1c470adbdb554e1504dce7c302e02ff736c90d65e014/scikit_learn-1.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:508907e5f81390e16d754e8815f7497e52139162fd69c4fdbd2dfa5d6cc88915", size = 12089887 }, + { url = "https://files.pythonhosted.org/packages/8a/5d/047cde25131eef3a38d03317fa7d25d6f60ce6e8ccfd24ac88b3e309fc00/scikit_learn-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97625f217c5c0c5d0505fa2af28ae424bd37949bb2f16ace3ff5f2f81fb4498b", size = 13079093 }, + { url = "https://files.pythonhosted.org/packages/cb/be/dec2a8d31d133034a8ec51ae68ac564ec9bde1c78a64551f1438c3690b9e/scikit_learn-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:da3f404e9e284d2b0a157e1b56b6566a34eb2798205cba35a211df3296ab7a74", size = 10945350 }, +] + +[[package]] +name = "scipy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/11/4d44a1f274e002784e4dbdb81e0ea96d2de2d1045b2132d5af62cc31fd28/scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417", size = 58620554 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/68/3bc0cfaf64ff507d82b1e5d5b64521df4c8bf7e22bc0b897827cbee9872c/scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389", size = 39069598 }, + { url = "https://files.pythonhosted.org/packages/43/a5/8d02f9c372790326ad405d94f04d4339482ec082455b9e6e288f7100513b/scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3", size = 29879676 }, + { url = "https://files.pythonhosted.org/packages/07/42/0e0bea9666fcbf2cb6ea0205db42c81b1f34d7b729ba251010edf9c80ebd/scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0", size = 23088696 }, + { url = "https://files.pythonhosted.org/packages/15/47/298ab6fef5ebf31b426560e978b8b8548421d4ed0bf99263e1eb44532306/scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3", size = 25470699 }, + { url = "https://files.pythonhosted.org/packages/d8/df/cdb6be5274bc694c4c22862ac3438cb04f360ed9df0aecee02ce0b798380/scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d", size = 35606631 }, + { url = "https://files.pythonhosted.org/packages/47/78/b0c2c23880dd1e99e938ad49ccfb011ae353758a2dc5ed7ee59baff684c3/scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69", size = 41178528 }, + { url = "https://files.pythonhosted.org/packages/5d/aa/994b45c34b897637b853ec04334afa55a85650a0d11dacfa67232260fb0a/scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad", size = 42784535 }, + { url = "https://files.pythonhosted.org/packages/e7/1c/8daa6df17a945cb1a2a1e3bae3c49643f7b3b94017ff01a4787064f03f84/scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5", size = 44772117 }, + { url = "https://files.pythonhosted.org/packages/b2/ab/070ccfabe870d9f105b04aee1e2860520460ef7ca0213172abfe871463b9/scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675", size = 39076999 }, + { url = "https://files.pythonhosted.org/packages/a7/c5/02ac82f9bb8f70818099df7e86c3ad28dae64e1347b421d8e3adf26acab6/scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2", size = 29894570 }, + { url = "https://files.pythonhosted.org/packages/ed/05/7f03e680cc5249c4f96c9e4e845acde08eb1aee5bc216eff8a089baa4ddb/scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617", size = 23103567 }, + { url = "https://files.pythonhosted.org/packages/5e/fc/9f1413bef53171f379d786aabc104d4abeea48ee84c553a3e3d8c9f96a9c/scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8", size = 25499102 }, + { url = "https://files.pythonhosted.org/packages/c2/4b/b44bee3c2ddc316b0159b3d87a3d467ef8d7edfd525e6f7364a62cd87d90/scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37", size = 35586346 }, + { url = "https://files.pythonhosted.org/packages/93/6b/701776d4bd6bdd9b629c387b5140f006185bd8ddea16788a44434376b98f/scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2", size = 41165244 }, + { url = "https://files.pythonhosted.org/packages/06/57/e6aa6f55729a8f245d8a6984f2855696c5992113a5dc789065020f8be753/scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2", size = 42817917 }, + { url = "https://files.pythonhosted.org/packages/ea/c2/5ecadc5fcccefaece775feadcd795060adf5c3b29a883bff0e678cfe89af/scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94", size = 44781033 }, + { url = "https://files.pythonhosted.org/packages/c0/04/2bdacc8ac6387b15db6faa40295f8bd25eccf33f1f13e68a72dc3c60a99e/scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d", size = 39128781 }, + { url = "https://files.pythonhosted.org/packages/c8/53/35b4d41f5fd42f5781dbd0dd6c05d35ba8aa75c84ecddc7d44756cd8da2e/scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07", size = 29939542 }, + { url = "https://files.pythonhosted.org/packages/66/67/6ef192e0e4d77b20cc33a01e743b00bc9e68fb83b88e06e636d2619a8767/scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5", size = 23148375 }, + { url = "https://files.pythonhosted.org/packages/f6/32/3a6dedd51d68eb7b8e7dc7947d5d841bcb699f1bf4463639554986f4d782/scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc", size = 25578573 }, + { url = "https://files.pythonhosted.org/packages/f0/5a/efa92a58dc3a2898705f1dc9dbaf390ca7d4fba26d6ab8cfffb0c72f656f/scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310", size = 35319299 }, + { url = "https://files.pythonhosted.org/packages/8e/ee/8a26858ca517e9c64f84b4c7734b89bda8e63bec85c3d2f432d225bb1886/scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066", size = 40849331 }, + { url = "https://files.pythonhosted.org/packages/a5/cd/06f72bc9187840f1c99e1a8750aad4216fc7dfdd7df46e6280add14b4822/scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1", size = 42544049 }, + { url = "https://files.pythonhosted.org/packages/aa/7d/43ab67228ef98c6b5dd42ab386eae2d7877036970a0d7e3dd3eb47a0d530/scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f", size = 44521212 }, + { url = "https://files.pythonhosted.org/packages/50/ef/ac98346db016ff18a6ad7626a35808f37074d25796fd0234c2bb0ed1e054/scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79", size = 39091068 }, + { url = "https://files.pythonhosted.org/packages/b9/cc/70948fe9f393b911b4251e96b55bbdeaa8cca41f37c26fd1df0232933b9e/scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e", size = 29875417 }, + { url = "https://files.pythonhosted.org/packages/3b/2e/35f549b7d231c1c9f9639f9ef49b815d816bf54dd050da5da1c11517a218/scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73", size = 23084508 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/b028e3f3e59fae61fb8c0f450db732c43dd1d836223a589a8be9f6377203/scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e", size = 25503364 }, + { url = "https://files.pythonhosted.org/packages/a7/2f/6c142b352ac15967744d62b165537a965e95d557085db4beab2a11f7943b/scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d", size = 35292639 }, + { url = "https://files.pythonhosted.org/packages/56/46/2449e6e51e0d7c3575f289f6acb7f828938eaab8874dbccfeb0cd2b71a27/scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e", size = 40798288 }, + { url = "https://files.pythonhosted.org/packages/32/cd/9d86f7ed7f4497c9fd3e39f8918dd93d9f647ba80d7e34e4946c0c2d1a7c/scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06", size = 42524647 }, + { url = "https://files.pythonhosted.org/packages/f5/1b/6ee032251bf4cdb0cc50059374e86a9f076308c1512b61c4e003e241efb7/scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84", size = 44469524 }, +] + +[[package]] +name = "semantic-kernel" +version = "1.6.0" +source = { editable = "." } +dependencies = [ + { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "defusedxml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nest-asyncio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, + { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "openapi-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "prance", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pybars4", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic-settings", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[package.optional-dependencies] +anthropic = [ + { name = "anthropic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +azure = [ + { name = "azure-ai-inference", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "azure-cosmos", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "azure-identity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "azure-search-documents", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +chroma = [ + { name = "chromadb", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +google = [ + { name = "google-cloud-aiplatform", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "google-generativeai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +hugging-face = [ + { name = "sentence-transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "torch", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "transformers", extra = ["torch"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +milvus = [ + { name = "milvus", marker = "(platform_system != 'Windows' and sys_platform == 'darwin') or (platform_system != 'Windows' and sys_platform == 'linux') or (platform_system != 'Windows' and sys_platform == 'win32')" }, + { name = "pymilvus", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +mistralai = [ + { name = "mistralai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +mongo = [ + { name = "motor", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +notebooks = [ + { name = "ipykernel", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +ollama = [ + { name = "ollama", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +pandas = [ + { name = "pandas", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +pinecone = [ + { name = "pinecone-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +postgres = [ + { name = "psycopg", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "psycopg", extra = ["binary"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "psycopg", extra = ["pool"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +qdrant = [ + { name = "qdrant-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +redis = [ + { name = "redis", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "redis", extra = ["hiredis"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "types-redis", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +usearch = [ + { name = "pyarrow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "usearch", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +weaviate = [ + { name = "weaviate-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ipykernel", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "mypy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nbconvert", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pre-commit", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest-asyncio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest-cov", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest-xdist", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest-xdist", extra = ["psutil"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "ruff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "snoop", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "types-pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = "~=3.8" }, + { name = "anthropic", marker = "extra == 'anthropic'", specifier = "~=0.32" }, + { name = "azure-ai-inference", marker = "extra == 'azure'", specifier = ">=1.0.0b3" }, + { name = "azure-cosmos", marker = "extra == 'azure'", specifier = "~=4.7" }, + { name = "azure-identity", marker = "extra == 'azure'", specifier = "~=1.13" }, + { name = "azure-search-documents", marker = "extra == 'azure'", specifier = ">=11.6.0b4" }, + { name = "chromadb", marker = "extra == 'chroma'", specifier = ">=0.4,<0.6" }, + { name = "defusedxml", specifier = "~=0.7" }, + { name = "google-cloud-aiplatform", marker = "extra == 'google'", specifier = "~=1.60" }, + { name = "google-generativeai", marker = "extra == 'google'", specifier = "~=0.7" }, + { name = "ipykernel", marker = "extra == 'notebooks'", specifier = "~=6.29" }, + { name = "jinja2", specifier = "~=3.1" }, + { name = "milvus", marker = "platform_system != 'Windows' and extra == 'milvus'", specifier = ">=2.3,<2.3.8" }, + { name = "mistralai", marker = "extra == 'mistralai'", specifier = "~=0.4" }, + { name = "motor", marker = "extra == 'mongo'", specifier = "~=3.3.2" }, + { name = "nest-asyncio", specifier = "~=1.6" }, + { name = "numpy", marker = "python_full_version < '3.12'", specifier = "~=1.25.0" }, + { name = "numpy", marker = "python_full_version >= '3.12'", specifier = "~=1.26.0" }, + { name = "ollama", marker = "extra == 'ollama'", specifier = "~=0.2" }, + { name = "openai", specifier = "~=1.0" }, + { name = "openapi-core", specifier = ">=0.18,<0.20" }, + { name = "opentelemetry-api", specifier = "~=1.24" }, + { name = "opentelemetry-sdk", specifier = "~=1.24" }, + { name = "pandas", marker = "extra == 'pandas'", specifier = "~=2.2" }, + { name = "pinecone-client", marker = "extra == 'pinecone'", specifier = "~=5.0" }, + { name = "prance", specifier = "~=23.6.21.0" }, + { name = "psycopg", extras = ["binary", "pool"], marker = "extra == 'postgres'", specifier = "~=3.2" }, + { name = "pyarrow", marker = "extra == 'usearch'", specifier = ">=12.0,<18.0" }, + { name = "pybars4", specifier = "~=0.9" }, + { name = "pydantic", specifier = "~=2.0" }, + { name = "pydantic-settings", specifier = "~=2.0" }, + { name = "pymilvus", marker = "extra == 'milvus'", specifier = ">=2.3,<2.4" }, + { name = "qdrant-client", marker = "extra == 'qdrant'", specifier = "~=1.9" }, + { name = "redis", extras = ["hiredis"], marker = "extra == 'redis'", specifier = "~=5.0" }, + { name = "sentence-transformers", marker = "extra == 'hugging-face'", specifier = "~=2.2" }, + { name = "torch", marker = "extra == 'hugging-face'", specifier = "==2.2.2" }, + { name = "transformers", extras = ["torch"], marker = "extra == 'hugging-face'", specifier = "~=4.28" }, + { name = "types-redis", marker = "extra == 'redis'", specifier = "~=4.6.0.20240425" }, + { name = "usearch", marker = "extra == 'usearch'", specifier = "~=2.9" }, + { name = "weaviate-client", marker = "extra == 'weaviate'", specifier = ">=3.18,<5.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "ipykernel", specifier = "~=6.29" }, + { name = "mypy", specifier = ">=1.10" }, + { name = "nbconvert", specifier = "~=7.16" }, + { name = "pre-commit", specifier = "~=3.7" }, + { name = "pytest", specifier = "~=8.2" }, + { name = "pytest-asyncio", specifier = "~=0.23" }, + { name = "pytest-cov", specifier = ">=5.0" }, + { name = "pytest-xdist", extras = ["psutil"], specifier = "~=3.6" }, + { name = "ruff", specifier = "~=0.5" }, + { name = "snoop", specifier = "~=0.4" }, + { name = "types-pyyaml", specifier = "~=6.0.12.20240311" }, +] + +[[package]] +name = "sentence-transformers" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, + { name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "scikit-learn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "scipy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "torch", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/8d/8d6147fdef0ed7aeff3dab487bd17619b512afab845eb295faa08b20a5d0/sentence_transformers-2.7.0.tar.gz", hash = "sha256:2f7df99d1c021dded471ed2d079e9d1e4fc8e30ecb06f957be060511b36f24ea", size = 128393 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/2c/bd95032aeb087b0706596af0a4518c4bfe0439a1bb149048ece18b617766/sentence_transformers-2.7.0-py3-none-any.whl", hash = "sha256:6a7276b05a95931581bbfa4ba49d780b2cf6904fa4a171ec7fd66c343f761c98", size = 171480 }, +] + +[[package]] +name = "setuptools" +version = "73.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/f4d4ce9bc15e61edba3179f9b0f763fc6d439474d28511b11f0d95bab7a2/setuptools-73.0.1.tar.gz", hash = "sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193", size = 2526506 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6a/0270e295bf30c37567736b7fca10167640898214ff911273af37ddb95770/setuptools-73.0.1-py3-none-any.whl", hash = "sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e", size = 2346588 }, +] + +[[package]] +name = "shapely" +version = "2.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/89/0d20bac88016be35ff7d3c0c2ae64b477908f1b1dfa540c5d69ac7af07fe/shapely-2.0.6.tar.gz", hash = "sha256:997f6159b1484059ec239cacaa53467fd8b5564dabe186cd84ac2944663b0bf6", size = 282361 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d4/f84bbbdb7771f5b9ade94db2398b256cf1471f1eb0ca8afbe0f6ca725d5a/shapely-2.0.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29a34e068da2d321e926b5073539fd2a1d4429a2c656bd63f0bd4c8f5b236d0b", size = 1449635 }, + { url = "https://files.pythonhosted.org/packages/03/10/bd6edb66ed0a845f0809f7ce653596f6fd9c6be675b3653872f47bf49f82/shapely-2.0.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c84c3f53144febf6af909d6b581bc05e8785d57e27f35ebaa5c1ab9baba13b", size = 1296756 }, + { url = "https://files.pythonhosted.org/packages/af/09/6374c11cb493a9970e8c04d7be25f578a37f6494a2fecfbed3a447b16b2c/shapely-2.0.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad2fae12dca8d2b727fa12b007e46fbc522148a584f5d6546c539f3464dccde", size = 2381960 }, + { url = "https://files.pythonhosted.org/packages/2b/a6/302e0d9c210ccf4d1ffadf7ab941797d3255dcd5f93daa73aaf116a4db39/shapely-2.0.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3304883bd82d44be1b27a9d17f1167fda8c7f5a02a897958d86c59ec69b705e", size = 2468133 }, + { url = "https://files.pythonhosted.org/packages/8c/be/e448681dc485f2931d4adee93d531fce93608a3ee59433303cc1a46e21a5/shapely-2.0.6-cp310-cp310-win32.whl", hash = "sha256:3ec3a0eab496b5e04633a39fa3d5eb5454628228201fb24903d38174ee34565e", size = 1294982 }, + { url = "https://files.pythonhosted.org/packages/cd/4c/6f4a6fc085e3be01c4c9de0117a2d373bf9fec5f0426cf4d5c94090a5a4d/shapely-2.0.6-cp310-cp310-win_amd64.whl", hash = "sha256:28f87cdf5308a514763a5c38de295544cb27429cfa655d50ed8431a4796090c4", size = 1441141 }, + { url = "https://files.pythonhosted.org/packages/37/15/269d8e1f7f658a37e61f7028683c546f520e4e7cedba1e32c77ff9d3a3c7/shapely-2.0.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aeb0f51a9db176da9a30cb2f4329b6fbd1e26d359012bb0ac3d3c7781667a9e", size = 1449578 }, + { url = "https://files.pythonhosted.org/packages/37/63/e182e43081fffa0a2d970c480f2ef91647a6ab94098f61748c23c2a485f2/shapely-2.0.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a7a78b0d51257a367ee115f4d41ca4d46edbd0dd280f697a8092dd3989867b2", size = 1296792 }, + { url = "https://files.pythonhosted.org/packages/6e/5a/d019f69449329dcd517355444fdb9ddd58bec5e080b8bdba007e8e4c546d/shapely-2.0.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f32c23d2f43d54029f986479f7c1f6e09c6b3a19353a3833c2ffb226fb63a855", size = 2443997 }, + { url = "https://files.pythonhosted.org/packages/25/aa/53f145e5a610a49af9ac49f2f1be1ec8659ebd5c393d66ac94e57c83b00e/shapely-2.0.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3dc9fb0eb56498912025f5eb352b5126f04801ed0e8bdbd867d21bdbfd7cbd0", size = 2528334 }, + { url = "https://files.pythonhosted.org/packages/64/64/0c7b0a22b416d36f6296b92bb4219d82b53d0a7c47e16fd0a4c85f2f117c/shapely-2.0.6-cp311-cp311-win32.whl", hash = "sha256:d93b7e0e71c9f095e09454bf18dad5ea716fb6ced5df3cb044564a00723f339d", size = 1294669 }, + { url = "https://files.pythonhosted.org/packages/b1/5a/6a67d929c467a1973b6bb9f0b00159cc343b02bf9a8d26db1abd2f87aa23/shapely-2.0.6-cp311-cp311-win_amd64.whl", hash = "sha256:c02eb6bf4cfb9fe6568502e85bb2647921ee49171bcd2d4116c7b3109724ef9b", size = 1442032 }, + { url = "https://files.pythonhosted.org/packages/46/77/efd9f9d4b6a762f976f8b082f54c9be16f63050389500fb52e4f6cc07c1a/shapely-2.0.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cec9193519940e9d1b86a3b4f5af9eb6910197d24af02f247afbfb47bcb3fab0", size = 1450326 }, + { url = "https://files.pythonhosted.org/packages/68/53/5efa6e7a4036a94fe6276cf7bbb298afded51ca3396b03981ad680c8cc7d/shapely-2.0.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83b94a44ab04a90e88be69e7ddcc6f332da7c0a0ebb1156e1c4f568bbec983c3", size = 1298480 }, + { url = "https://files.pythonhosted.org/packages/88/a2/1be1db4fc262e536465a52d4f19d85834724fedf2299a1b9836bc82fe8fa/shapely-2.0.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:537c4b2716d22c92036d00b34aac9d3775e3691f80c7aa517c2c290351f42cd8", size = 2439311 }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9a57e187cbf2fbbbdfd4044a4f9ce141c8d221f9963750d3b001f0ec080d/shapely-2.0.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fea108334be345c283ce74bf064fa00cfdd718048a8af7343c59eb40f59726", size = 2524835 }, + { url = "https://files.pythonhosted.org/packages/6d/0a/f407509ab56825f39bf8cfce1fb410238da96cf096809c3e404e5bc71ea1/shapely-2.0.6-cp312-cp312-win32.whl", hash = "sha256:42fd4cd4834747e4990227e4cbafb02242c0cffe9ce7ef9971f53ac52d80d55f", size = 1295613 }, + { url = "https://files.pythonhosted.org/packages/7b/b3/857afd9dfbfc554f10d683ac412eac6fa260d1f4cd2967ecb655c57e831a/shapely-2.0.6-cp312-cp312-win_amd64.whl", hash = "sha256:665990c84aece05efb68a21b3523a6b2057e84a1afbef426ad287f0796ef8a48", size = 1442539 }, + { url = "https://files.pythonhosted.org/packages/34/e8/d164ef5b0eab86088cde06dee8415519ffd5bb0dd1bd9d021e640e64237c/shapely-2.0.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:42805ef90783ce689a4dde2b6b2f261e2c52609226a0438d882e3ced40bb3013", size = 1445344 }, + { url = "https://files.pythonhosted.org/packages/ce/e2/9fba7ac142f7831757a10852bfa465683724eadbc93d2d46f74a16f9af04/shapely-2.0.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6d2cb146191a47bd0cee8ff5f90b47547b82b6345c0d02dd8b25b88b68af62d7", size = 1296182 }, + { url = "https://files.pythonhosted.org/packages/cf/dc/790d4bda27d196cd56ec66975eaae3351c65614cafd0e16ddde39ec9fb92/shapely-2.0.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3fdef0a1794a8fe70dc1f514440aa34426cc0ae98d9a1027fb299d45741c381", size = 2423426 }, + { url = "https://files.pythonhosted.org/packages/af/b0/f8169f77eac7392d41e231911e0095eb1148b4d40c50ea9e34d999c89a7e/shapely-2.0.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c665a0301c645615a107ff7f52adafa2153beab51daf34587170d85e8ba6805", size = 2513249 }, + { url = "https://files.pythonhosted.org/packages/f6/1d/a8c0e9ab49ff2f8e4dedd71b0122eafb22a18ad7e9d256025e1f10c84704/shapely-2.0.6-cp313-cp313-win32.whl", hash = "sha256:0334bd51828f68cd54b87d80b3e7cee93f249d82ae55a0faf3ea21c9be7b323a", size = 1294848 }, + { url = "https://files.pythonhosted.org/packages/23/38/2bc32dd1e7e67a471d4c60971e66df0bdace88656c47a9a728ace0091075/shapely-2.0.6-cp313-cp313-win_amd64.whl", hash = "sha256:d37d070da9e0e0f0a530a621e17c0b8c3c9d04105655132a87cfff8bd77cc4c2", size = 1441371 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "snoop" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "cheap-repr", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "executing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/c1/c93715f44b16ad7ec52a7b48ae26bdc1880c0192d6075ba3a097e7b04f3e/snoop-0.4.3.tar.gz", hash = "sha256:2e0930bb19ff0dbdaa6f5933f88e89ed5984210ea9f9de0e1d8231fa5c1c1f25", size = 139747 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/b4/5eb395a7c44f382f42cc4ce2d544223c0506e06c61534f45a2188b8fdf13/snoop-0.4.3-py2.py3-none-any.whl", hash = "sha256:b7418581889ff78b29d9dc5ad4625c4c475c74755fb5cba82c693c6e32afadc0", size = 27841 }, +] + +[[package]] +name = "soupsieve" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "executing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pure-eval", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, +] + +[[package]] +name = "starlette" +version = "0.38.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/e2/d49a94ecb665b3a1c34b40c78165a737abc384fcabc843ccb14a3bd3dc37/starlette-0.38.2.tar.gz", hash = "sha256:c7c0441065252160993a1a37cf2a73bb64d271b17303e0b0c1eb7191cfb12d75", size = 2844770 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/d976da9998e4f4a99e297cda09d61ce305919ea94cbeeb476dba4fece098/starlette-0.38.2-py3-none-any.whl", hash = "sha256:4ec6a59df6bbafdab5f567754481657f7ed90dc9d69b0c9ff017907dd54faeff", size = 72020 }, +] + +[[package]] +name = "sympy" +version = "1.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/15/4a041424c7187f41cce678f5a02189b244e9aac61a18b45cd415a3a470f3/sympy-1.13.2.tar.gz", hash = "sha256:401449d84d07be9d0c7a46a64bd54fe097667d5e7181bfe67ec777be9e01cb13", size = 7532926 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/f9/6845bf8fca0eaf847da21c5d5bc6cd92797364662824a11d3f836423a1a5/sympy-1.13.2-py3-none-any.whl", hash = "sha256:c51d75517712f1aed280d4ce58506a4a88d635d6b5dd48b39102a7ae1f3fcfe9", size = 6189289 }, +] + +[[package]] +name = "tenacity" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/94/91fccdb4b8110642462e653d5dcb27e7b674742ad68efd146367da7bdb10/tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b", size = 47421 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/cb/b86984bed139586d01532a587464b5805f12e397594f19f931c4c2fbfa61/tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539", size = 28169 }, +] + +[[package]] +name = "threadpoolctl" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b5148dcbf72f5cde221f8bfe3b6a540da7aa1842f6b491ad979a6c8b84af/threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107", size = 41936 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467", size = 18414 }, +] + +[[package]] +name = "tinycss2" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/6f/38d2335a2b70b9982d112bb177e3dbe169746423e33f718bf5e9c7b3ddd3/tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d", size = 67360 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/4d/0db5b8a613d2a59bbc29bc5bb44a2f8070eb9ceab11c50d477502a8a0092/tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7", size = 22532 }, +] + +[[package]] +name = "tokenizers" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/04/2071c150f374aab6d5e92aaec38d0f3c368d227dd9e0469a1f0966ac68d1/tokenizers-0.19.1.tar.gz", hash = "sha256:ee59e6680ed0fdbe6b724cf38bd70400a0c1dd623b07ac729087270caeac88e3", size = 321039 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/91cac8d496b304ec5a22f07606893cad35ea8e1a8406dc8909e365f97a80/tokenizers-0.19.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:952078130b3d101e05ecfc7fc3640282d74ed26bcf691400f872563fca15ac97", size = 2533301 }, + { url = "https://files.pythonhosted.org/packages/4c/12/9cb68762ff5fee1efd51aefe2f62cb225f26f060a68a3779e1060bbc7a59/tokenizers-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82c8b8063de6c0468f08e82c4e198763e7b97aabfe573fd4cf7b33930ca4df77", size = 2440223 }, + { url = "https://files.pythonhosted.org/packages/e4/03/b2020e6a78fb994cff1ec962adc157c23109172a46b4fe451d6d0dd33fdb/tokenizers-0.19.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f03727225feaf340ceeb7e00604825addef622d551cbd46b7b775ac834c1e1c4", size = 3683779 }, + { url = "https://files.pythonhosted.org/packages/50/4e/2e5549a26dc6f9e434f83bebf16c2d7dc9dc3477cc0ec8b23ede4d465b90/tokenizers-0.19.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:453e4422efdfc9c6b6bf2eae00d5e323f263fff62b29a8c9cd526c5003f3f642", size = 3569431 }, + { url = "https://files.pythonhosted.org/packages/75/79/158626bd794e75551e0c6bb93f1cd3c9ba08ba14b181b98f09e95994f609/tokenizers-0.19.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02e81bf089ebf0e7f4df34fa0207519f07e66d8491d963618252f2e0729e0b46", size = 3424739 }, + { url = "https://files.pythonhosted.org/packages/65/8e/5f4316976c26009f1ae0b6543f3d97af29afa5ba5dc145251e6a07314618/tokenizers-0.19.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b07c538ba956843833fee1190cf769c60dc62e1cf934ed50d77d5502194d63b1", size = 3965791 }, + { url = "https://files.pythonhosted.org/packages/6a/e1/5dbac9618709972434eea072670cd69fba1aa988e6200f16057722b4bf96/tokenizers-0.19.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28cab1582e0eec38b1f38c1c1fb2e56bce5dc180acb1724574fc5f47da2a4fe", size = 4049879 }, + { url = "https://files.pythonhosted.org/packages/40/4f/eb78de4af3b17b589f43a369cbf0c3a7173f25c3d2cd93068852c07689aa/tokenizers-0.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b01afb7193d47439f091cd8f070a1ced347ad0f9144952a30a41836902fe09e", size = 3607049 }, + { url = "https://files.pythonhosted.org/packages/f5/f8/141dcb0f88e9452af8d20d14dd53aab5937222a2bb4f2c04bfed6829263c/tokenizers-0.19.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7fb297edec6c6841ab2e4e8f357209519188e4a59b557ea4fafcf4691d1b4c98", size = 9634084 }, + { url = "https://files.pythonhosted.org/packages/2e/be/debb7caa3f88ed54015170db16e07aa3a5fea2d3983d0dde92f98d888dc8/tokenizers-0.19.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e8a3dd055e515df7054378dc9d6fa8c8c34e1f32777fb9a01fea81496b3f9d3", size = 9949480 }, + { url = "https://files.pythonhosted.org/packages/7a/e7/26bedf5d270d293d572a90bd66b0b030012aedb95d8ee87e8bcd446b76fb/tokenizers-0.19.1-cp310-none-win32.whl", hash = "sha256:7ff898780a155ea053f5d934925f3902be2ed1f4d916461e1a93019cc7250837", size = 2041462 }, + { url = "https://files.pythonhosted.org/packages/f4/85/d999b9a05fd101d48f1a365d68be0b109277bb25c89fb37a389d669f9185/tokenizers-0.19.1-cp310-none-win_amd64.whl", hash = "sha256:bea6f9947e9419c2fda21ae6c32871e3d398cba549b93f4a65a2d369662d9403", size = 2220036 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/6e1d728d765eb4102767f071bf7f6439ab10d7f4a975c9217db65715207a/tokenizers-0.19.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5c88d1481f1882c2e53e6bb06491e474e420d9ac7bdff172610c4f9ad3898059", size = 2533448 }, + { url = "https://files.pythonhosted.org/packages/90/79/d17a0f491d10817cd30f1121a07aa09c8e97a81114b116e473baf1577f09/tokenizers-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddf672ed719b4ed82b51499100f5417d7d9f6fb05a65e232249268f35de5ed14", size = 2440254 }, + { url = "https://files.pythonhosted.org/packages/c7/28/2d11c3ff94f9d42eceb2ea549a06e3f166fe391c5a025e5d96fac898a3ac/tokenizers-0.19.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dadc509cc8a9fe460bd274c0e16ac4184d0958117cf026e0ea8b32b438171594", size = 3684971 }, + { url = "https://files.pythonhosted.org/packages/36/c6/537f22b57e6003904d35d07962dbde2f2e9bdd791d0241da976a4c7f8194/tokenizers-0.19.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfedf31824ca4915b511b03441784ff640378191918264268e6923da48104acc", size = 3568894 }, + { url = "https://files.pythonhosted.org/packages/af/ef/3c1deed14ec59b2c8e7e2fa27b2a53f7d101181277a43b89ab17d891ef2e/tokenizers-0.19.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac11016d0a04aa6487b1513a3a36e7bee7eec0e5d30057c9c0408067345c48d2", size = 3426873 }, + { url = "https://files.pythonhosted.org/packages/06/db/c0320c4798ac6bd12d2ef895bec9d10d216a3b4d6fff10e9d68883ea7edc/tokenizers-0.19.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76951121890fea8330d3a0df9a954b3f2a37e3ec20e5b0530e9a0044ca2e11fe", size = 3965050 }, + { url = "https://files.pythonhosted.org/packages/4c/8a/a166888d6cb14db55f5eb7ce0b1d4777d145aa27cbf4f945712cf6c29935/tokenizers-0.19.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b342d2ce8fc8d00f376af068e3274e2e8649562e3bc6ae4a67784ded6b99428d", size = 4047855 }, + { url = "https://files.pythonhosted.org/packages/a7/03/fb50fc03f86016b227a967c8d474f90230c885c0d18f78acdfda7a96ce56/tokenizers-0.19.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d16ff18907f4909dca9b076b9c2d899114dd6abceeb074eca0c93e2353f943aa", size = 3608228 }, + { url = "https://files.pythonhosted.org/packages/5b/cd/0385e1026e1e03732fd398e964792a3a8433918b166748c82507e014d748/tokenizers-0.19.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:706a37cc5332f85f26efbe2bdc9ef8a9b372b77e4645331a405073e4b3a8c1c6", size = 9633115 }, + { url = "https://files.pythonhosted.org/packages/25/50/8f8ad0bbdaf09d04b15e6502d1fa1c653754ed7e016e4ae009726aa1a4e4/tokenizers-0.19.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16baac68651701364b0289979ecec728546133e8e8fe38f66fe48ad07996b88b", size = 9949062 }, + { url = "https://files.pythonhosted.org/packages/db/11/31be66710f1d14526f3588a441efadeb184e1e68458067007b20ead03c59/tokenizers-0.19.1-cp311-none-win32.whl", hash = "sha256:9ed240c56b4403e22b9584ee37d87b8bfa14865134e3e1c3fb4b2c42fafd3256", size = 2041039 }, + { url = "https://files.pythonhosted.org/packages/65/8e/6d7d72b28f22c422cff8beae10ac3c2e4376b9be721ef8167b7eecd1da62/tokenizers-0.19.1-cp311-none-win_amd64.whl", hash = "sha256:ad57d59341710b94a7d9dbea13f5c1e7d76fd8d9bcd944a7a6ab0b0da6e0cc66", size = 2220386 }, + { url = "https://files.pythonhosted.org/packages/63/90/2890cd096898dcdb596ee172cde40c0f54a9cf43b0736aa260a5501252af/tokenizers-0.19.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:621d670e1b1c281a1c9698ed89451395d318802ff88d1fc1accff0867a06f153", size = 2530580 }, + { url = "https://files.pythonhosted.org/packages/74/d1/f4e1e950adb36675dfd8f9d0f4be644f3f3aaf22a5677a4f5c81282b662e/tokenizers-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d924204a3dbe50b75630bd16f821ebda6a5f729928df30f582fb5aade90c818a", size = 2436682 }, + { url = "https://files.pythonhosted.org/packages/ed/30/89b321a16c58d233e301ec15072c0d3ed5014825e72da98604cd3ab2fba1/tokenizers-0.19.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f3fefdc0446b1a1e6d81cd4c07088ac015665d2e812f6dbba4a06267d1a2c95", size = 3693494 }, + { url = "https://files.pythonhosted.org/packages/05/40/fa899f32de483500fbc78befd378fd7afba4270f17db707d1a78c0a4ddc3/tokenizers-0.19.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9620b78e0b2d52ef07b0d428323fb34e8ea1219c5eac98c2596311f20f1f9266", size = 3566541 }, + { url = "https://files.pythonhosted.org/packages/67/14/e7da32ae5fb4971830f1ef335932fae3fa57e76b537e852f146c850aefdf/tokenizers-0.19.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04ce49e82d100594715ac1b2ce87d1a36e61891a91de774755f743babcd0dd52", size = 3430792 }, + { url = "https://files.pythonhosted.org/packages/f2/4b/aae61bdb6ab584d2612170801703982ee0e35f8b6adacbeefe5a3b277621/tokenizers-0.19.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5c2ff13d157afe413bf7e25789879dd463e5a4abfb529a2d8f8473d8042e28f", size = 3962812 }, + { url = "https://files.pythonhosted.org/packages/0a/b6/f7b7ef89c4da7b20256e6eab23d3835f05d1ca8f451d31c16cbfe3cd9eb6/tokenizers-0.19.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3174c76efd9d08f836bfccaca7cfec3f4d1c0a4cf3acbc7236ad577cc423c840", size = 4024688 }, + { url = "https://files.pythonhosted.org/packages/80/54/12047a69f5b382d7ee72044dc89151a2dd0d13b2c9bdcc22654883704d31/tokenizers-0.19.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9d5b6c0e7a1e979bec10ff960fae925e947aab95619a6fdb4c1d8ff3708ce3", size = 3610961 }, + { url = "https://files.pythonhosted.org/packages/52/b7/1e8a913d18ac28feeda42d4d2d51781874398fb59cd1c1e2653a4b5742ed/tokenizers-0.19.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a179856d1caee06577220ebcfa332af046d576fb73454b8f4d4b0ba8324423ea", size = 9631367 }, + { url = "https://files.pythonhosted.org/packages/ac/3d/2284f6d99f8f21d09352b88b8cfefa24ab88468d962aeb0aa15c20d76b32/tokenizers-0.19.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:952b80dac1a6492170f8c2429bd11fcaa14377e097d12a1dbe0ef2fb2241e16c", size = 9950121 }, + { url = "https://files.pythonhosted.org/packages/2a/94/ec3369dbc9b7200c14c8c7a1a04c78b7a7398d0c001e1b7d1ffe30eb93a0/tokenizers-0.19.1-cp312-none-win32.whl", hash = "sha256:01d62812454c188306755c94755465505836fd616f75067abcae529c35edeb57", size = 2044069 }, + { url = "https://files.pythonhosted.org/packages/0c/97/80bff6937e0c67d30c0facacd4f0bcf4254e581aa4995c73cef8c8640e56/tokenizers-0.19.1-cp312-none-win_amd64.whl", hash = "sha256:b70bfbe3a82d3e3fb2a5e9b22a39f8d1740c96c68b6ace0086b39074f08ab89a", size = 2214527 }, + { url = "https://files.pythonhosted.org/packages/cf/7b/38fb7207cde3d1dc5272411cd18178e6437cdc1ef08cac5d0e8cfd57f38c/tokenizers-0.19.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b11853f17b54c2fe47742c56d8a33bf49ce31caf531e87ac0d7d13d327c9334", size = 2532668 }, + { url = "https://files.pythonhosted.org/packages/1d/0d/2c452fe17fc17f0cdb713acb811eebb1f714b8c21d497c4672af4f491229/tokenizers-0.19.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d26194ef6c13302f446d39972aaa36a1dda6450bc8949f5eb4c27f51191375bd", size = 2438321 }, + { url = "https://files.pythonhosted.org/packages/19/e0/f9e915d028b45798723eab59c253da28040aa66b9f31dcb7cfc3be88fa37/tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e8d1ed93beda54bbd6131a2cb363a576eac746d5c26ba5b7556bc6f964425594", size = 3682304 }, + { url = "https://files.pythonhosted.org/packages/ce/2b/db8a94608c392752681c2ca312487b7cd5bcc4f77e24a90daa4916138271/tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca407133536f19bdec44b3da117ef0d12e43f6d4b56ac4c765f37eca501c7bda", size = 3566208 }, + { url = "https://files.pythonhosted.org/packages/d8/58/2e998462677c4c0eb5123ce386bcb488a155664d273d0283122866515f09/tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce05fde79d2bc2e46ac08aacbc142bead21614d937aac950be88dc79f9db9022", size = 3605791 }, + { url = "https://files.pythonhosted.org/packages/83/ac/26bc2e2bb2a054dc2e51699628936f5474e093b68da6ccdde04b2fc39ab8/tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:35583cd46d16f07c054efd18b5d46af4a2f070a2dd0a47914e66f3ff5efb2b1e", size = 9632867 }, + { url = "https://files.pythonhosted.org/packages/45/b6/36c1bb106bbe96012c9367df89ed01599cada036c0b96d38fbbdbeb75c9f/tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:43350270bfc16b06ad3f6f07eab21f089adb835544417afda0f83256a8bf8b75", size = 9945103 }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, +] + +[[package]] +name = "torch" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "fsspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "networkx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cublas-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-cupti-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-runtime-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" }, + { name = "nvidia-cudnn-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" }, + { name = "nvidia-cufft-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" }, + { name = "nvidia-curand-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" }, + { name = "nvidia-cusolver-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" }, + { name = "nvidia-nccl-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" }, + { name = "nvidia-nvtx-cu12", marker = "(platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" }, + { name = "sympy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "triton", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'darwin') or (python_full_version < '3.12' and platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'linux') or (python_full_version < '3.12' and platform_machine == 'x86_64' and platform_system == 'Linux' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/b3/1fcc3bccfddadfd6845dcbfe26eb4b099f1dfea5aa0e5cfb92b3c98dba5b/torch-2.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:bc889d311a855dd2dfd164daf8cc903a6b7273a747189cebafdd89106e4ad585", size = 755526581 }, + { url = "https://files.pythonhosted.org/packages/c3/7c/aeb0c5789a3f10cf909640530cd75b314959b9d9914a4996ed2c7bf8779d/torch-2.2.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:15dffa4cc3261fa73d02f0ed25f5fa49ecc9e12bf1ae0a4c1e7a88bbfaad9030", size = 86623646 }, + { url = "https://files.pythonhosted.org/packages/3a/81/684d99e536b20e869a7c1222cf1dd233311fb05d3628e9570992bfb65760/torch-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:11e8fe261233aeabd67696d6b993eeb0896faa175c6b41b9a6c9f0334bdad1c5", size = 198579616 }, + { url = "https://files.pythonhosted.org/packages/3b/55/7192974ab13e5e5577f45d14ce70d42f5a9a686b4f57bbe8c9ab45c4a61a/torch-2.2.2-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:b2e2200b245bd9f263a0d41b6a2dab69c4aca635a01b30cca78064b0ef5b109e", size = 150788930 }, + { url = "https://files.pythonhosted.org/packages/33/6b/21496316c9b8242749ee2a9064406271efdf979e91d440e8a3806b5e84bf/torch-2.2.2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:877b3e6593b5e00b35bbe111b7057464e76a7dd186a287280d941b564b0563c2", size = 59707286 }, + { url = "https://files.pythonhosted.org/packages/c3/33/d7a6123231bd4d04c7005dde8507235772f3bc4622a25f3a88c016415d49/torch-2.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:ad4c03b786e074f46606f4151c0a1e3740268bcf29fbd2fdf6666d66341c1dcb", size = 755555407 }, + { url = "https://files.pythonhosted.org/packages/02/af/81abea3d73fddfde26afd1ce52a4ddfa389cd2b684c89d6c4d0d5d8d0dfa/torch-2.2.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:32827fa1fbe5da8851686256b4cd94cc7b11be962862c2293811c94eea9457bf", size = 86642063 }, + { url = "https://files.pythonhosted.org/packages/5c/01/5ab75f138bf32d7a69df61e4997e24eccad87cc009f5fb7e2a31af8a4036/torch-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:f9ef0a648310435511e76905f9b89612e45ef2c8b023bee294f5e6f7e73a3e7c", size = 198584125 }, + { url = "https://files.pythonhosted.org/packages/3f/14/e105b8ef6d324e789c1589e95cb0ab63f3e07c2216d68b1178b7c21b7d2a/torch-2.2.2-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:95b9b44f3bcebd8b6cd8d37ec802048c872d9c567ba52c894bba90863a439059", size = 150796474 }, + { url = "https://files.pythonhosted.org/packages/96/23/18b9c16c18a77755e7f15173821c7100f11e6b3b7717bea8d729bdeb92c0/torch-2.2.2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:49aa4126ede714c5aeef7ae92969b4b0bbe67f19665106463c39f22e0a1860d1", size = 59714938 }, + { url = "https://files.pythonhosted.org/packages/4c/0c/d8f77363a7a3350c96e6c9db4ffb101d1c0487cc0b8cdaae1e4bfb2800ad/torch-2.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:cf12cdb66c9c940227ad647bc9cf5dba7e8640772ae10dfe7569a0c1e2a28aca", size = 755466713 }, + { url = "https://files.pythonhosted.org/packages/05/9b/e5c0df26435f3d55b6699e1c61f07652b8c8a3ac5058a75d0e991f92c2b0/torch-2.2.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:89ddac2a8c1fb6569b90890955de0c34e1724f87431cacff4c1979b5f769203c", size = 86515814 }, + { url = "https://files.pythonhosted.org/packages/72/ce/beca89dcdcf4323880d3b959ef457a4c61a95483af250e6892fec9174162/torch-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:451331406b760f4b1ab298ddd536486ab3cfb1312614cfe0532133535be60bea", size = 198528804 }, + { url = "https://files.pythonhosted.org/packages/79/78/29dcab24a344ffd9ee9549ec0ab2c7885c13df61cde4c65836ee275efaeb/torch-2.2.2-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:eb4d6e9d3663e26cd27dc3ad266b34445a16b54908e74725adb241aa56987533", size = 150797270 }, + { url = "https://files.pythonhosted.org/packages/4a/0e/e4e033371a7cba9da0db5ccb507a9174e41b9c29189a932d01f2f61ecfc0/torch-2.2.2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:bf9558da7d2bf7463390b3b2a61a6a3dbb0b45b161ee1dd5ec640bf579d479fc", size = 59678388 }, +] + +[[package]] +name = "tornado" +version = "6.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/66/398ac7167f1c7835406888a386f6d0d26ee5dbf197d8a571300be57662d3/tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9", size = 500623 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/d9/c33be3c1a7564f7d42d87a8d186371a75fd142097076767a5c27da941fef/tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8", size = 435924 }, + { url = "https://files.pythonhosted.org/packages/2e/0f/721e113a2fac2f1d7d124b3279a1da4c77622e104084f56119875019ffab/tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14", size = 433883 }, + { url = "https://files.pythonhosted.org/packages/13/cf/786b8f1e6fe1c7c675e79657448178ad65e41c1c9765ef82e7f6f765c4c5/tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4", size = 437224 }, + { url = "https://files.pythonhosted.org/packages/e4/8e/a6ce4b8d5935558828b0f30f3afcb2d980566718837b3365d98e34f6067e/tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842", size = 436597 }, + { url = "https://files.pythonhosted.org/packages/22/d4/54f9d12668b58336bd30defe0307e6c61589a3e687b05c366f804b7faaf0/tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3", size = 436797 }, + { url = "https://files.pythonhosted.org/packages/cf/3f/2c792e7afa7dd8b24fad7a2ed3c2f24a5ec5110c7b43a64cb6095cc106b8/tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f", size = 437516 }, + { url = "https://files.pythonhosted.org/packages/71/63/c8fc62745e669ac9009044b889fc531b6f88ac0f5f183cac79eaa950bb23/tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4", size = 436958 }, + { url = "https://files.pythonhosted.org/packages/94/d4/f8ac1f5bd22c15fad3b527e025ce219bd526acdbd903f52053df2baecc8b/tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698", size = 436882 }, + { url = "https://files.pythonhosted.org/packages/4b/3e/a8124c21cc0bbf144d7903d2a0cadab15cadaf683fa39a0f92bc567f0d4d/tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d", size = 438092 }, + { url = "https://files.pythonhosted.org/packages/d9/2f/3f2f05e84a7aff787a96d5fb06821323feb370fe0baed4db6ea7b1088f32/tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7", size = 438532 }, +] + +[[package]] +name = "tqdm" +version = "4.66.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "(platform_system == 'Windows' and sys_platform == 'darwin') or (platform_system == 'Windows' and sys_platform == 'linux') or (platform_system == 'Windows' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/83/6ba9844a41128c62e810fddddd72473201f3eacde02046066142a2d96cc5/tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad", size = 169504 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/5d/acf5905c36149bbaec41ccf7f2b68814647347b72075ac0b1fe3022fdc73/tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", size = 78351 }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, +] + +[[package]] +name = "transformers" +version = "4.44.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "safetensors", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/46/62e914365ab463addb0357a88f8d2614aae02f1a2b2b5c24c7ee005ff157/transformers-4.44.1.tar.gz", hash = "sha256:3b9a1a07ca65c665c7bf6109b7da76182184d10bb58d9ab14e6892e7b9e073a2", size = 8110315 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/ab/c42556ba7c5aed687256466d472abb9a1b9cbff5730aa42a884d892e061a/transformers-4.44.1-py3-none-any.whl", hash = "sha256:bd2642da18b4e6d29b135c17650cd7ca8e874f2d092d2eddd3ed6b71a93a155c", size = 9465379 }, +] + +[package.optional-dependencies] +torch = [ + { name = "accelerate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "torch", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "triton" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/05/ed974ce87fe8c8843855daa2136b3409ee1c126707ab54a8b72815c08b49/triton-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2294514340cfe4e8f4f9e5c66c702744c4a117d25e618bd08469d0bfed1e2e5", size = 167900779 }, + { url = "https://files.pythonhosted.org/packages/bd/ac/3974caaa459bf2c3a244a84be8d17561f631f7d42af370fc311defeca2fb/triton-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da58a152bddb62cafa9a857dd2bc1f886dbf9f9c90a2b5da82157cd2b34392b0", size = 167928356 }, + { url = "https://files.pythonhosted.org/packages/0e/49/2e1bbae4542b8f624e409540b4197e37ab22a88e8685e99debe721cc2b50/triton-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af58716e721460a61886668b205963dc4d1e4ac20508cc3f623aef0d70283d5", size = 167933985 }, +] + +[[package]] +name = "typer" +version = "0.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "shellingham", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/f7/f174a1cae84848ae8b27170a96187b91937b743f0580ff968078fe16930a/typer-0.12.4.tar.gz", hash = "sha256:c9c1613ed6a166162705b3347b8d10b661ccc5d95692654d0fb628118f2c34e6", size = 97945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/cc/15083dcde1252a663398b1b2a173637a3ec65adadfb95137dc95df1e6adc/typer-0.12.4-py3-none-any.whl", hash = "sha256:819aa03699f438397e876aa12b0d63766864ecba1b579092cc9fe35d886e34b6", size = 47402 }, +] + +[[package]] +name = "types-cffi" +version = "1.16.0.20240331" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/c8/81e5699160b91f0f91eea852d84035c412bfb4b3a29389701044400ab379/types-cffi-1.16.0.20240331.tar.gz", hash = "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee", size = 11318 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/7a/98f5d2493a652cec05d3b09be59202d202004a41fca9c70d224782611365/types_cffi-1.16.0.20240331-py3-none-any.whl", hash = "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0", size = 14550 }, +] + +[[package]] +name = "types-pyopenssl" +version = "24.1.0.20240722" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "types-cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499 }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20240808" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/08/6f5737f645571b7a0b1ebd2fe8b5cf1ee4ec3e707866ca96042a86fc1d10/types-PyYAML-6.0.12.20240808.tar.gz", hash = "sha256:b8f76ddbd7f65440a8bda5526a9607e4c7a322dc2f8e1a8c405644f9a6f4b9af", size = 12359 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/ad/ffbad24e2bc8f20bf047ec22af0c0a92f6ce2071eb21c9103df600cda6de/types_PyYAML-6.0.12.20240808-py3-none-any.whl", hash = "sha256:deda34c5c655265fc517b546c902aa6eed2ef8d3e921e4765fe606fe2afe8d35", size = 15298 }, +] + +[[package]] +name = "types-redis" +version = "4.6.0.20240819" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "types-pyopenssl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/2b/a09204d0901d9d319b38f26434c5544f400b2a551df9ecad9ad0437987a0/types-redis-4.6.0.20240819.tar.gz", hash = "sha256:08f51f550ad41d0152bd98d77ac9d6d8f761369121710a213642f6036b9a7183", size = 49539 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/5d/f636b3bb65d52705abc9eb5832864dbd99d26ad1b1c2b5c2ff24af12249d/types_redis-4.6.0.20240819-py3-none-any.whl", hash = "sha256:86db9af6f0033154e12bc22c77236cef0907b995fda8c9f0f0eacd59943ed2fc", size = 58720 }, +] + +[[package]] +name = "types-setuptools" +version = "72.2.0.20240821" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/db/7b2c5190db1c74e42aebefa081f1f82741a2591a77e4e6471d52938ea15a/types-setuptools-72.2.0.20240821.tar.gz", hash = "sha256:e349b8015608879939f27ee370672f801287c46f5caa2d188d416336172c4965", size = 42123 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/d0/ff619a3202824e54d3b048fe7c34e5904b8e985196c6bcd5496e843e01b7/types_setuptools-72.2.0.20240821-py3-none-any.whl", hash = "sha256:260e89d6d3b42cc35f9f0f382d030713b7b547344a664c05c9175e6ba124fac7", size = 66596 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "tzdata" +version = "2024.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/5b/e025d02cb3b66b7b76093404392d4b44343c69101cc85f4d180dd5784717/tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", size = 190559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252", size = 345370 }, +] + +[[package]] +name = "ujson" +version = "5.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/00/3110fd566786bfa542adb7932d62035e0c0ef662a8ff6544b6643b3d6fd7/ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1", size = 7154885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/91/91678e49a9194f527e60115db84368c237ac7824992224fac47dcb23a5c6/ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd", size = 55354 }, + { url = "https://files.pythonhosted.org/packages/de/2f/1ed8c9b782fa4f44c26c1c4ec686d728a4865479da5712955daeef0b2e7b/ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf", size = 51808 }, + { url = "https://files.pythonhosted.org/packages/51/bf/a3a38b2912288143e8e613c6c4c3f798b5e4e98c542deabf94c60237235f/ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6", size = 51995 }, + { url = "https://files.pythonhosted.org/packages/b4/6d/0df8f7a6f1944ba619d93025ce468c9252aa10799d7140e07014dfc1a16c/ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569", size = 53566 }, + { url = "https://files.pythonhosted.org/packages/d5/ec/370741e5e30d5f7dc7f31a478d5bec7537ce6bfb7f85e72acefbe09aa2b2/ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770", size = 58499 }, + { url = "https://files.pythonhosted.org/packages/fe/29/72b33a88f7fae3c398f9ba3e74dc2e5875989b25f1c1f75489c048a2cf4e/ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1", size = 997881 }, + { url = "https://files.pythonhosted.org/packages/70/5c/808fbf21470e7045d56a282cf5e85a0450eacdb347d871d4eb404270ee17/ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5", size = 1140631 }, + { url = "https://files.pythonhosted.org/packages/8f/6a/e1e8281408e6270d6ecf2375af14d9e2f41c402ab6b161ecfa87a9727777/ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51", size = 1043511 }, + { url = "https://files.pythonhosted.org/packages/cb/ca/e319acbe4863919ec62498bc1325309f5c14a3280318dca10fe1db3cb393/ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518", size = 38626 }, + { url = "https://files.pythonhosted.org/packages/78/ec/dc96ca379de33f73b758d72e821ee4f129ccc32221f4eb3f089ff78d8370/ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f", size = 42076 }, + { url = "https://files.pythonhosted.org/packages/23/ec/3c551ecfe048bcb3948725251fb0214b5844a12aa60bee08d78315bb1c39/ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00", size = 55353 }, + { url = "https://files.pythonhosted.org/packages/8d/9f/4731ef0671a0653e9f5ba18db7c4596d8ecbf80c7922dd5fe4150f1aea76/ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126", size = 51813 }, + { url = "https://files.pythonhosted.org/packages/1f/2b/44d6b9c1688330bf011f9abfdb08911a9dc74f76926dde74e718d87600da/ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8", size = 51988 }, + { url = "https://files.pythonhosted.org/packages/29/45/f5f5667427c1ec3383478092a414063ddd0dfbebbcc533538fe37068a0a3/ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b", size = 53561 }, + { url = "https://files.pythonhosted.org/packages/26/21/a0c265cda4dd225ec1be595f844661732c13560ad06378760036fc622587/ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9", size = 58497 }, + { url = "https://files.pythonhosted.org/packages/28/36/8fde862094fd2342ccc427a6a8584fed294055fdee341661c78660f7aef3/ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f", size = 997877 }, + { url = "https://files.pythonhosted.org/packages/90/37/9208e40d53baa6da9b6a1c719e0670c3f474c8fc7cc2f1e939ec21c1bc93/ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4", size = 1140632 }, + { url = "https://files.pythonhosted.org/packages/89/d5/2626c87c59802863d44d19e35ad16b7e658e4ac190b0dead17ff25460b4c/ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1", size = 1043513 }, + { url = "https://files.pythonhosted.org/packages/2f/ee/03662ce9b3f16855770f0d70f10f0978ba6210805aa310c4eebe66d36476/ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f", size = 38616 }, + { url = "https://files.pythonhosted.org/packages/3e/20/952dbed5895835ea0b82e81a7be4ebb83f93b079d4d1ead93fcddb3075af/ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720", size = 42071 }, + { url = "https://files.pythonhosted.org/packages/e8/a6/fd3f8bbd80842267e2d06c3583279555e8354c5986c952385199d57a5b6c/ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5", size = 55642 }, + { url = "https://files.pythonhosted.org/packages/a8/47/dd03fd2b5ae727e16d5d18919b383959c6d269c7b948a380fdd879518640/ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e", size = 51807 }, + { url = "https://files.pythonhosted.org/packages/25/23/079a4cc6fd7e2655a473ed9e776ddbb7144e27f04e8fc484a0fb45fe6f71/ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043", size = 51972 }, + { url = "https://files.pythonhosted.org/packages/04/81/668707e5f2177791869b624be4c06fb2473bf97ee33296b18d1cf3092af7/ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1", size = 53686 }, + { url = "https://files.pythonhosted.org/packages/bd/50/056d518a386d80aaf4505ccf3cee1c40d312a46901ed494d5711dd939bc3/ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3", size = 58591 }, + { url = "https://files.pythonhosted.org/packages/fc/d6/aeaf3e2d6fb1f4cfb6bf25f454d60490ed8146ddc0600fae44bfe7eb5a72/ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21", size = 997853 }, + { url = "https://files.pythonhosted.org/packages/f8/d5/1f2a5d2699f447f7d990334ca96e90065ea7f99b142ce96e85f26d7e78e2/ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2", size = 1140689 }, + { url = "https://files.pythonhosted.org/packages/f2/2c/6990f4ccb41ed93744aaaa3786394bca0875503f97690622f3cafc0adfde/ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e", size = 1043576 }, + { url = "https://files.pythonhosted.org/packages/14/f5/a2368463dbb09fbdbf6a696062d0c0f62e4ae6fa65f38f829611da2e8fdd/ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e", size = 38764 }, + { url = "https://files.pythonhosted.org/packages/59/2d/691f741ffd72b6c84438a93749ac57bf1a3f217ac4b0ea4fd0e96119e118/ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc", size = 42211 }, + { url = "https://files.pythonhosted.org/packages/0d/69/b3e3f924bb0e8820bb46671979770c5be6a7d51c77a66324cdb09f1acddb/ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287", size = 55646 }, + { url = "https://files.pythonhosted.org/packages/32/8a/9b748eb543c6cabc54ebeaa1f28035b1bd09c0800235b08e85990734c41e/ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e", size = 51806 }, + { url = "https://files.pythonhosted.org/packages/39/50/4b53ea234413b710a18b305f465b328e306ba9592e13a791a6a6b378869b/ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557", size = 51975 }, + { url = "https://files.pythonhosted.org/packages/b4/9d/8061934f960cdb6dd55f0b3ceeff207fcc48c64f58b43403777ad5623d9e/ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988", size = 53693 }, + { url = "https://files.pythonhosted.org/packages/f5/be/7bfa84b28519ddbb67efc8410765ca7da55e6b93aba84d97764cd5794dbc/ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816", size = 58594 }, + { url = "https://files.pythonhosted.org/packages/48/eb/85d465abafb2c69d9699cfa5520e6e96561db787d36c677370e066c7e2e7/ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20", size = 997853 }, + { url = "https://files.pythonhosted.org/packages/9f/76/2a63409fc05d34dd7d929357b7a45e3a2c96f22b4225cd74becd2ba6c4cb/ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0", size = 1140694 }, + { url = "https://files.pythonhosted.org/packages/45/ed/582c4daba0f3e1688d923b5cb914ada1f9defa702df38a1916c899f7c4d1/ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f", size = 1043580 }, + { url = "https://files.pythonhosted.org/packages/d7/0c/9837fece153051e19c7bade9f88f9b409e026b9525927824cdf16293b43b/ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165", size = 38766 }, + { url = "https://files.pythonhosted.org/packages/d7/72/6cb6728e2738c05bbe9bd522d6fc79f86b9a28402f38663e85a28fddd4a0/ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539", size = 42212 }, + { url = "https://files.pythonhosted.org/packages/95/53/e5f5e733fc3525e65f36f533b0dbece5e5e2730b760e9beacf7e3d9d8b26/ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64", size = 51846 }, + { url = "https://files.pythonhosted.org/packages/59/1f/f7bc02a54ea7b47f3dc2d125a106408f18b0f47b14fc737f0913483ae82b/ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3", size = 48103 }, + { url = "https://files.pythonhosted.org/packages/1a/3a/d3921b6f29bc744d8d6c56db5f8bbcbe55115fd0f2b79c3c43ff292cc7c9/ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a", size = 47257 }, + { url = "https://files.pythonhosted.org/packages/f1/04/f4e3883204b786717038064afd537389ba7d31a72b437c1372297cb651ea/ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746", size = 48468 }, + { url = "https://files.pythonhosted.org/packages/17/cd/9c6547169eb01a22b04cbb638804ccaeb3c2ec2afc12303464e0f9b2ee5a/ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88", size = 54266 }, + { url = "https://files.pythonhosted.org/packages/70/bf/ecd14d3cf6127f8a990b01f0ad20e257f5619a555f47d707c57d39934894/ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b", size = 42224 }, +] + +[[package]] +name = "uritemplate" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/5a/4742fdba39cd02a56226815abfa72fe0aa81c33bed16ed045647d6000eba/uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", size = 273898 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c0/7461b49cd25aeece13766f02ee576d1db528f1c37ce69aee300e075b485b/uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e", size = 10356 }, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/6d/fa469ae21497ddc8bc93e5877702dca7cb8f911e337aca7452b5724f1bb6/urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168", size = 292266 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", size = 121444 }, +] + +[[package]] +name = "usearch" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform == 'darwin') or (python_full_version < '3.12' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform == 'win32')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" }, + { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/7e/230db02465a09c70af49c71d8443e049a00647c8c35867b2d893b2e2610e/usearch-2.14.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7f875f160463ccfcd120a636555f0821b3bd7bdb304ae936f30d603ec31ab0f7", size = 700510 }, + { url = "https://files.pythonhosted.org/packages/71/f5/024f1598a820cc94a106485e8d8a8a67b147465347ad655e6812cda0d313/usearch-2.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cfbdbae7f80e1e4cccd3ee1e9091d0c5894ed29d42ec0977286020c0f19854d8", size = 375928 }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7e30169dc73efcf13b3d96bcfe067657d5aa5faf9550fbeccc43e297bfee/usearch-2.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5edc4de8c71e860f52d449672e89be22fa2c8bbd318caaac747ec05bfdc413b", size = 363694 }, + { url = "https://files.pythonhosted.org/packages/95/73/dda1b3baa8cd877466ece35a25ff9a3e9a0e593ceb2d9f20ddb289a1d39c/usearch-2.14.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7319f70f0e98b2a08afa9e3c4c8acb01b48691388702bf6bc5c10fa534d4fddf", size = 1262813 }, + { url = "https://files.pythonhosted.org/packages/be/3e/8eae798e4a4b38cb064fcbb6090f389f8ff27576b84eb3586db7788f51d6/usearch-2.14.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2b619b6e84a3ba16d14932dbe3c0af2d03b21cc57e12695b871cea1735e42f98", size = 1455375 }, + { url = "https://files.pythonhosted.org/packages/2c/7a/100de85aa1ee36e11dce3f794f7b4e72f0b3d68bd14555fdd9c15665892b/usearch-2.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdc09e961b281ed7622e4202cf4260ae043df10b27be2537f7b4d5ae1d4de7c3", size = 2196879 }, + { url = "https://files.pythonhosted.org/packages/d5/1a/14397058ee4a02669445fa6f7cb4eb9fa8816be10fd3b9c581613f9cd46e/usearch-2.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d73a4c63688696f217992e00962a7c93a46f823b9fc99b2733f8a44470b87b0d", size = 2325029 }, + { url = "https://files.pythonhosted.org/packages/e3/ac/e6e749271041b218f1929867a1fa70c7f59e4e4f4f0d9d1feb8dcd59e873/usearch-2.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:78f0acff1ca99eeda1304f3bd919b6fa42c5d7864ac424f3195846f1eff1bb33", size = 281028 }, + { url = "https://files.pythonhosted.org/packages/6f/c4/481aebb1481590a2fccfe6312457fa0f9f6258ace50ebaf17025741ed995/usearch-2.14.0-cp310-cp310-win_arm64.whl", hash = "sha256:1de0bc99cca6e8ff67f6fed85bc9a82ab8542e4eeed32a754bf76700bdfa32bc", size = 261013 }, + { url = "https://files.pythonhosted.org/packages/b6/e3/a1c25e540fa805d33562e7919481a5df901a406fe905c8e280fb45edb658/usearch-2.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6cbc5a2e511f212dea10e799b60b9639fae4c0eddd3bef36b62a24d03b3157ac", size = 704460 }, + { url = "https://files.pythonhosted.org/packages/da/d6/f53f7706fe2c2022d5a3716689de280338fec4332c37266ccb018f5794f9/usearch-2.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2454f7b73b3ba955e01e3500477aaf45636527f8ef41c90064e042f0e503c884", size = 377627 }, + { url = "https://files.pythonhosted.org/packages/97/7f/68b1e53438ceb88bf933a392f9c7a6752e87008d65e6bd0642cb5286dd7f/usearch-2.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:53db1ab686c0a70cf828d1566cc79d65e0fbe10637fc0db1e176d4cfe586a27c", size = 364731 }, + { url = "https://files.pythonhosted.org/packages/72/b4/de152d16f0ea8b9051392f9438c0f9319ee42f1aee3dcf84dfbc3e33eb82/usearch-2.14.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:170ff8d97285a0679f5a14f32c72fc507f3d9227073c874d80b5040fa42c23ba", size = 1268817 }, + { url = "https://files.pythonhosted.org/packages/3f/db/f99d1e6a31be07e4037878326f4cffffa93e6315586b51f85c1fc30a4182/usearch-2.14.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:dd16b87467088785062b4491aa428e549cea18efe2d0c2122aaedd3540e61969", size = 1461166 }, + { url = "https://files.pythonhosted.org/packages/22/3d/0733928b43de8dd37788fde6bec4ea98255ed98b25cbda68a6e1321bdabd/usearch-2.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fef2b58ce1891ebdf8c5a10ea330a5eb62ead482dfc85fb68c4ef2f7045a3b0c", size = 2199208 }, + { url = "https://files.pythonhosted.org/packages/87/72/fd55703fdf0f47f644a80aac15418d3893233f4ffa64f0a01361ddbca015/usearch-2.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:688b8693378c39fcfec54593a08a6ffa905cb4c60cf7bc7be0f05bef8fd5cd79", size = 2327971 }, + { url = "https://files.pythonhosted.org/packages/44/1d/ab643bb6c801248c41df66ea216e81714d51749c51a38eab70d34c60ae5f/usearch-2.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:ae5af33960cbf46f7133417bacaf35273b71b3974e40a259ad18f8c1580b77c0", size = 281791 }, + { url = "https://files.pythonhosted.org/packages/3a/51/f943fb593e215b8bc9c03cafbfc21973de489397f66809911f51ccd4e8a7/usearch-2.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:0f4343d30b936280dea119fecca2ab825c4118ae42bf7bf682f0dc64e870e716", size = 262136 }, + { url = "https://files.pythonhosted.org/packages/ba/2d/7b281e8a47cd6a9bf46554ed8d2a9117f2b5dd1abf7922dfb18dc8c556a9/usearch-2.14.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:290f7040833b4ff4e0182cd4d7f9c26ba98d43f54a8df512ff925f0cf636051b", size = 710202 }, + { url = "https://files.pythonhosted.org/packages/24/66/16a5f91707e2faf3ac7ab27d742b53247647e4a8c05cbc87cb32e1c04b67/usearch-2.14.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c2c68e6d205c6a8c16ee6af4c8e09a65cfbef52a6fe6b9aa2c9b1946dcc9970", size = 381509 }, + { url = "https://files.pythonhosted.org/packages/14/f4/dc2fe99e9a1fb786edb9a7b76748a05d0e4d05c09619f88d79bc8ddf6348/usearch-2.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ff38f82dd452875d9b2e07d774f86a47cadcf910b4caa76a78c05084efdfd06", size = 366262 }, + { url = "https://files.pythonhosted.org/packages/de/4b/aab1a4270abea0daa7a8f025d27bbfb34da41aaaa4b3487d407389387711/usearch-2.14.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c27acd1ae6d2cc79e7909058562ae55ceb919a97af6f9fd5d769e3dc929aa60", size = 1269220 }, + { url = "https://files.pythonhosted.org/packages/ad/6b/2d225adc6c4cc06c3fb7f0cd0a0600122f117c32e3dc41c92735cbe07c02/usearch-2.14.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:51f4a8c88c0f23cd9e8e2da56cb63e1e605918af8cd3bea5e9b0200188c1c1b6", size = 1465916 }, + { url = "https://files.pythonhosted.org/packages/f9/b1/f177a828256e2595bdfdc658fdd73ffad840d26bcbc0191f6d5d1062f824/usearch-2.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5c298f73a60f96a50661e7815e9f1917b80cfd3cb3ba17724cd2a7974bf3db4e", size = 2197827 }, + { url = "https://files.pythonhosted.org/packages/86/38/495db24f48ceab3028dbd4584a7abd265c68f9a568f003b7860cef591049/usearch-2.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afb98f7cf8a6e8a849150a39d767c25c5e74d049d03dc96dc630863902163a84", size = 2334400 }, + { url = "https://files.pythonhosted.org/packages/d6/05/3154e5d365fb518fd846ba88fc85f9cd87ad96541bdcf2ce55fce6652003/usearch-2.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:610d886732f3bbafbe5e840525ab10711c376d686a703240e3ce721a636fc12b", size = 283005 }, + { url = "https://files.pythonhosted.org/packages/ba/ca/31efa3416a04c18e24cc05aa06c1e09487dd13a4e17e757800606809bce6/usearch-2.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:f59a50964f1a2bd457adea1b35644fad4e15d05499d86a5f323d7c92e50185b6", size = 263231 }, +] + +[[package]] +name = "uvicorn" +version = "0.30.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "h11", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/01/5e637e7aa9dd031be5376b9fb749ec20b86f5a5b6a49b87fabd374d5fa9f/uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788", size = 42825 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/8e/cdc7d6263db313030e4c257dd5ba3909ebc4e4fb53ad62d5f09b1a2f5458/uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5", size = 62835 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "uvloop", marker = "(platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_python_implementation != 'PyPy' and sys_platform == 'linux')" }, + { name = "watchfiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "websockets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "uvloop" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/f1/dc9577455e011ad43d9379e836ee73f40b4f99c02946849a44f7ae64835e/uvloop-0.20.0.tar.gz", hash = "sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469", size = 2329938 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/69/cc1ad125ea8ce4a4d3ba7d9836062c3fc9063cf163ddf0f168e73f3268e3/uvloop-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996", size = 1363922 }, + { url = "https://files.pythonhosted.org/packages/f7/45/5a3f7a32372e4a90dfd83f30507183ec38990b8c5930ed7e36c6a15af47b/uvloop-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b", size = 760386 }, + { url = "https://files.pythonhosted.org/packages/9e/a5/9e973b25ade12c938940751bce71d0cb36efee3489014471f7d9c0a3c379/uvloop-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10", size = 3432586 }, + { url = "https://files.pythonhosted.org/packages/a9/e0/0bec8a25b2e9cf14fdfcf0229637b437c923b4e5ca22f8e988363c49bb51/uvloop-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae", size = 3431802 }, + { url = "https://files.pythonhosted.org/packages/95/3b/14cef46dcec6237d858666a4a1fdb171361528c70fcd930bfc312920e7a9/uvloop-0.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006", size = 4144444 }, + { url = "https://files.pythonhosted.org/packages/9d/5a/0ac516562ff783f760cab3b061f10fdeb4a9f985ad4b44e7e4564ff11691/uvloop-0.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73", size = 4147039 }, + { url = "https://files.pythonhosted.org/packages/64/bf/45828beccf685b7ed9638d9b77ef382b470c6ca3b5bff78067e02ffd5663/uvloop-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037", size = 1320593 }, + { url = "https://files.pythonhosted.org/packages/27/c0/3c24e50bee7802a2add96ca9f0d5eb0ebab07e0a5615539d38aeb89499b9/uvloop-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9", size = 736676 }, + { url = "https://files.pythonhosted.org/packages/83/ce/ffa3c72954eae36825acfafd2b6a9221d79abd2670c0d25e04d6ef4a2007/uvloop-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e", size = 3494573 }, + { url = "https://files.pythonhosted.org/packages/46/6d/4caab3a36199ba52b98d519feccfcf48921d7a6649daf14a93c7e77497e9/uvloop-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82edbfd3df39fb3d108fc079ebc461330f7c2e33dbd002d146bf7c445ba6e756", size = 3489932 }, + { url = "https://files.pythonhosted.org/packages/e4/4f/49c51595bd794945c88613df88922c38076eae2d7653f4624aa6f4980b07/uvloop-0.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80dc1b139516be2077b3e57ce1cb65bfed09149e1d175e0478e7a987863b68f0", size = 4185596 }, + { url = "https://files.pythonhosted.org/packages/b8/94/7e256731260d313f5049717d1c4582d52a3b132424c95e16954a50ab95d3/uvloop-0.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f44af67bf39af25db4c1ac27e82e9665717f9c26af2369c404be865c8818dcf", size = 4185746 }, + { url = "https://files.pythonhosted.org/packages/2d/64/31cbd379d6e260ac8de3f672f904e924f09715c3f192b09f26cc8e9f574c/uvloop-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4b75f2950ddb6feed85336412b9a0c310a2edbcf4cf931aa5cfe29034829676d", size = 1324302 }, + { url = "https://files.pythonhosted.org/packages/1e/6b/9207e7177ff30f78299401f2e1163ea41130d4fd29bcdc6d12572c06b728/uvloop-0.20.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:77fbc69c287596880ecec2d4c7a62346bef08b6209749bf6ce8c22bbaca0239e", size = 738105 }, + { url = "https://files.pythonhosted.org/packages/c1/ba/b64b10f577519d875992dc07e2365899a1a4c0d28327059ce1e1bdfb6854/uvloop-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6462c95f48e2d8d4c993a2950cd3d31ab061864d1c226bbf0ee2f1a8f36674b9", size = 4090658 }, + { url = "https://files.pythonhosted.org/packages/0a/f8/5ceea6876154d926604f10c1dd896adf9bce6d55a55911364337b8a5ed8d/uvloop-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649c33034979273fa71aa25d0fe120ad1777c551d8c4cd2c0c9851d88fcb13ab", size = 4173357 }, + { url = "https://files.pythonhosted.org/packages/18/b2/117ab6bfb18274753fbc319607bf06e216bd7eea8be81d5bac22c912d6a7/uvloop-0.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a609780e942d43a275a617c0839d85f95c334bad29c4c0918252085113285b5", size = 4029868 }, + { url = "https://files.pythonhosted.org/packages/6f/52/deb4be09060637ef4752adaa0b75bf770c20c823e8108705792f99cd4a6f/uvloop-0.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00", size = 4115980 }, +] + +[[package]] +name = "validators" +version = "0.33.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/af/5ad4fed95276e3eb7628d858c88cd205799bcad847e46223760a3129cbb1/validators-0.33.0.tar.gz", hash = "sha256:535867e9617f0100e676a1257ba1e206b9bfd847ddc171e4d44811f07ff0bfbf", size = 70741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/22/91b4bd36df27e651daedd93d03d5d3bb6029fdb0b55494e45ee46c36c570/validators-0.33.0-py3-none-any.whl", hash = "sha256:134b586a98894f8139865953899fc2daeb3d0c35569552c5518f089ae43ed075", size = 43298 }, +] + +[[package]] +name = "virtualenv" +version = "20.26.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/60/db9f95e6ad456f1872486769c55628c7901fb4de5a72c2f7bdd912abf0c1/virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a", size = 9057588 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/4d/410156100224c5e2f0011d435e477b57aed9576fc7fe137abcf14ec16e11/virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589", size = 5684792 }, +] + +[[package]] +name = "watchfiles" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/1a/b06613ef620d7f5ca712a3d4928ec1c07182159a64277fcdf7738edb0b32/watchfiles-0.23.0.tar.gz", hash = "sha256:9338ade39ff24f8086bb005d16c29f8e9f19e55b18dcb04dfa26fcbc09da497b", size = 37384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/08/c0a09fc63a6b75fccd3e99b21f07ddb812e64a78da10703397b39653263e/watchfiles-0.23.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bee8ce357a05c20db04f46c22be2d1a2c6a8ed365b325d08af94358e0688eeb4", size = 374424 }, + { url = "https://files.pythonhosted.org/packages/8c/ff/2b338016e96ab592e8d9cece0260b9fca54d8bed7b36940c46112eda2e49/watchfiles-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ccd3011cc7ee2f789af9ebe04745436371d36afe610028921cab9f24bb2987b", size = 369288 }, + { url = "https://files.pythonhosted.org/packages/43/cf/747f412b75ea4bb5419e659ae8b2713a327b6f879e3f2e0695c7d7275cf3/watchfiles-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb02d41c33be667e6135e6686f1bb76104c88a312a18faa0ef0262b5bf7f1a0f", size = 441212 }, + { url = "https://files.pythonhosted.org/packages/c6/64/07f4c50883f1406e5a4187651b4d3d2495582df9f70d320ee3c9e7ed19be/watchfiles-0.23.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf12ac34c444362f3261fb3ff548f0037ddd4c5bb85f66c4be30d2936beb3c5", size = 437765 }, + { url = "https://files.pythonhosted.org/packages/d8/e9/4af3cfb2eb161003ce79518eb0cbfd014313e30dc842209776fbee3a64e6/watchfiles-0.23.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0b2c25040a3c0ce0e66c7779cc045fdfbbb8d59e5aabfe033000b42fe44b53e", size = 456151 }, + { url = "https://files.pythonhosted.org/packages/b4/45/d97e61c893fc59d4b0c4154fdf26449e103a3783db4d981bb8e7301af532/watchfiles-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf2be4b9eece4f3da8ba5f244b9e51932ebc441c0867bd6af46a3d97eb068d6", size = 472320 }, + { url = "https://files.pythonhosted.org/packages/f2/55/7266cd63e736abbde902cbd99fb70a8eddfac7e0e52ed52fbbee6eed5f95/watchfiles-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40cb8fa00028908211eb9f8d47744dca21a4be6766672e1ff3280bee320436f1", size = 480442 }, + { url = "https://files.pythonhosted.org/packages/22/ec/c756c012b174ccf5f2ee32202603e66b33b93a54cf16c69a7440c764d7f9/watchfiles-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f48c917ffd36ff9a5212614c2d0d585fa8b064ca7e66206fb5c095015bc8207", size = 427729 }, + { url = "https://files.pythonhosted.org/packages/78/94/97ac8d7a19f5439ab5cc28d0b5d648760358e43097f6acb8cb7165c4c1b7/watchfiles-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9d183e3888ada88185ab17064079c0db8c17e32023f5c278d7bf8014713b1b5b", size = 616359 }, + { url = "https://files.pythonhosted.org/packages/fc/c9/568a54e07245a068819572a7d51c7d2f6ff8e7018102e956156fadae408c/watchfiles-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9837edf328b2805346f91209b7e660f65fb0e9ca18b7459d075d58db082bf981", size = 597955 }, + { url = "https://files.pythonhosted.org/packages/fa/2c/ba3e9d54c17a4014996555a0b31f4be1c8920fdfe067942f60873ac8931a/watchfiles-0.23.0-cp310-none-win32.whl", hash = "sha256:296e0b29ab0276ca59d82d2da22cbbdb39a23eed94cca69aed274595fb3dfe42", size = 264290 }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d7aa3f23c78b2c9cac5d1cf46fed5f74340d0ffd47c0f485b76419ec6597/watchfiles-0.23.0-cp310-none-win_amd64.whl", hash = "sha256:4ea756e425ab2dfc8ef2a0cb87af8aa7ef7dfc6fc46c6f89bcf382121d4fff75", size = 275914 }, + { url = "https://files.pythonhosted.org/packages/14/5f/787386438d895145099e1415d1fbd3ff047a4f5e329134fd30677fe83f1f/watchfiles-0.23.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e397b64f7aaf26915bf2ad0f1190f75c855d11eb111cc00f12f97430153c2eab", size = 374801 }, + { url = "https://files.pythonhosted.org/packages/76/6f/3075cd9c69fdce2544fb13cb9e3c8ad51424cb2c552b019514799a14966e/watchfiles-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4ac73b02ca1824ec0a7351588241fd3953748d3774694aa7ddb5e8e46aef3e3", size = 368210 }, + { url = "https://files.pythonhosted.org/packages/ab/6b/cd4faa27088a8b612ffdfa25e3d413e676a6173b8b02a33e7fec152d75ca/watchfiles-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130a896d53b48a1cecccfa903f37a1d87dbb74295305f865a3e816452f6e49e4", size = 441356 }, + { url = "https://files.pythonhosted.org/packages/39/ba/d361135dac6cd0fb4449f4f058c053eb9b42f70ff4d9a13767808e18851c/watchfiles-0.23.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5e7803a65eb2d563c73230e9d693c6539e3c975ccfe62526cadde69f3fda0cf", size = 437615 }, + { url = "https://files.pythonhosted.org/packages/34/2c/c279de01628f467d16b444bdcedf9c4ce3bc5242cb23f9bfb8fbff8522ee/watchfiles-0.23.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1aa4cc85202956d1a65c88d18c7b687b8319dbe6b1aec8969784ef7a10e7d1a", size = 456227 }, + { url = "https://files.pythonhosted.org/packages/a4/9f/a3c9f1fbcd1099554e4f707e14473ff23f0e05013d553755b98c2d86716d/watchfiles-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87f889f6e58849ddb7c5d2cb19e2e074917ed1c6e3ceca50405775166492cca8", size = 472219 }, + { url = "https://files.pythonhosted.org/packages/22/ee/06a0a6cbde8ac6fff57c33da9e428f42dd0989e60a6ad72ca6534f650a47/watchfiles-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37fd826dac84c6441615aa3f04077adcc5cac7194a021c9f0d69af20fb9fa788", size = 479948 }, + { url = "https://files.pythonhosted.org/packages/b9/f0/76ad5227da9461b1190de2f9dd21fece09660a9a44607de9c728f3d3e93f/watchfiles-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee7db6e36e7a2c15923072e41ea24d9a0cf39658cb0637ecc9307b09d28827e1", size = 427559 }, + { url = "https://files.pythonhosted.org/packages/e1/15/daf4361e0a6e6b27f516aaaacbb16baa8d1a266657b2314862fc73f2deaf/watchfiles-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2368c5371c17fdcb5a2ea71c5c9d49f9b128821bfee69503cc38eae00feb3220", size = 616447 }, + { url = "https://files.pythonhosted.org/packages/b3/e4/2647ca9aaa072e139a4cc6c83c8a15d2f8fa6740913903ab998917a5ed97/watchfiles-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:857af85d445b9ba9178db95658c219dbd77b71b8264e66836a6eba4fbf49c320", size = 598031 }, + { url = "https://files.pythonhosted.org/packages/3d/02/f223537cd0e3c22df45629710b27b7f89fdf4114be2f3399b83faedf1446/watchfiles-0.23.0-cp311-none-win32.whl", hash = "sha256:1d636c8aeb28cdd04a4aa89030c4b48f8b2954d8483e5f989774fa441c0ed57b", size = 264354 }, + { url = "https://files.pythonhosted.org/packages/03/31/c1b5ea92100d9774f5a8a89115a43ef1c4fb169b643b6cc930e0cd2c5728/watchfiles-0.23.0-cp311-none-win_amd64.whl", hash = "sha256:46f1d8069a95885ca529645cdbb05aea5837d799965676e1b2b1f95a4206313e", size = 275821 }, + { url = "https://files.pythonhosted.org/packages/23/9c/810ede8d4dff7e65393b50cbb1a3ef10b6cdb1312a97d8106712175355c8/watchfiles-0.23.0-cp311-none-win_arm64.whl", hash = "sha256:e495ed2a7943503766c5d1ff05ae9212dc2ce1c0e30a80d4f0d84889298fa304", size = 266906 }, + { url = "https://files.pythonhosted.org/packages/61/52/85cdf326a53f1ae3fbe5dcab13f5729ca91ec2d61140e095a2a4cdf6a9ca/watchfiles-0.23.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1db691bad0243aed27c8354b12d60e8e266b75216ae99d33e927ff5238d270b5", size = 373314 }, + { url = "https://files.pythonhosted.org/packages/20/5e/a97417a6544615b21c7960a45aeea13e3b42779e0ed3ebdd2d76ad62ab50/watchfiles-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62d2b18cb1edaba311fbbfe83fb5e53a858ba37cacb01e69bc20553bb70911b8", size = 368915 }, + { url = "https://files.pythonhosted.org/packages/bc/82/537945ed624af6248c9820a99cbfd5902bb5e6a71a01a5b3de0c00f1872e/watchfiles-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e087e8fdf1270d000913c12e6eca44edd02aad3559b3e6b8ef00f0ce76e0636f", size = 441495 }, + { url = "https://files.pythonhosted.org/packages/28/24/060b064f28083866d916052fcced5c3547c5081a8e27b0702434666aa9a0/watchfiles-0.23.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd41d5c72417b87c00b1b635738f3c283e737d75c5fa5c3e1c60cd03eac3af77", size = 437357 }, + { url = "https://files.pythonhosted.org/packages/b6/00/ac760f3fa8d8975dbeaef9af99b21077e7c38898ac5051c8601649d86d99/watchfiles-0.23.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5f3ca0ff47940ce0a389457b35d6df601c317c1e1a9615981c474452f98de1", size = 456584 }, + { url = "https://files.pythonhosted.org/packages/f7/52/2f7bbedc5f524d2ba0e9d792dab01ef4418d0f5045a9f5f4e5aca142a30d/watchfiles-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6991e3a78f642368b8b1b669327eb6751439f9f7eaaa625fae67dd6070ecfa0b", size = 471863 }, + { url = "https://files.pythonhosted.org/packages/b1/64/a80f51cb55c967629930682bf120d5ca9d1c65077c38328be635ed0d567c/watchfiles-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f7252f52a09f8fa5435dc82b6af79483118ce6bd51eb74e6269f05ee22a7b9f", size = 478307 }, + { url = "https://files.pythonhosted.org/packages/03/f1/fdacfdbffb0635a7d0140ecca6ef7b5bce6566a085f76a65eb796ee54ddd/watchfiles-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e01bcb8d767c58865207a6c2f2792ad763a0fe1119fb0a430f444f5b02a5ea0", size = 427117 }, + { url = "https://files.pythonhosted.org/packages/d1/23/89b2bef692c350de8a4c2bde501fdf6087889a55f52a3201f0c53b616087/watchfiles-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8e56fbcdd27fce061854ddec99e015dd779cae186eb36b14471fc9ae713b118c", size = 616352 }, + { url = "https://files.pythonhosted.org/packages/2c/35/a683945181a527083a1146620997b5d6ffe06d716c4497d388bfea813f0c/watchfiles-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bd3e2d64500a6cad28bcd710ee6269fbeb2e5320525acd0cfab5f269ade68581", size = 597165 }, + { url = "https://files.pythonhosted.org/packages/9e/9b/ec2eabc996e5332fc89c633fbe762e08a58a7df6b5e595dd458c5f7778a4/watchfiles-0.23.0-cp312-none-win32.whl", hash = "sha256:eb99c954291b2fad0eff98b490aa641e128fbc4a03b11c8a0086de8b7077fb75", size = 264293 }, + { url = "https://files.pythonhosted.org/packages/e0/3a/62add8d90070f4b17f8bbfd66c9eaa9e08af3bc4020c07a9400d1b959aaf/watchfiles-0.23.0-cp312-none-win_amd64.whl", hash = "sha256:dccc858372a56080332ea89b78cfb18efb945da858fabeb67f5a44fa0bcb4ebb", size = 275514 }, + { url = "https://files.pythonhosted.org/packages/e8/9a/2792d4c24105104bfaf959bffefb09e02d14050913a83242ce4eb1e3f2ff/watchfiles-0.23.0-cp312-none-win_arm64.whl", hash = "sha256:6c21a5467f35c61eafb4e394303720893066897fca937bade5b4f5877d350ff8", size = 266607 }, + { url = "https://files.pythonhosted.org/packages/f6/5b/1a1d9bca4eae8cf191e74b62cd970f4a010f56f897c11dd2e6caef3ce7e3/watchfiles-0.23.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ba31c32f6b4dceeb2be04f717811565159617e28d61a60bb616b6442027fd4b9", size = 372999 }, + { url = "https://files.pythonhosted.org/packages/98/e1/76ad010c0a2bb6efbb80383c0bba56db065238f12b0da6e6026b4e69f6aa/watchfiles-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85042ab91814fca99cec4678fc063fb46df4cbb57b4835a1cc2cb7a51e10250e", size = 368511 }, + { url = "https://files.pythonhosted.org/packages/a1/13/d2d59d545b84fd3cf4f08b69da358209b4276c2c932d060d94a421015074/watchfiles-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24655e8c1c9c114005c3868a3d432c8aa595a786b8493500071e6a52f3d09217", size = 441063 }, + { url = "https://files.pythonhosted.org/packages/4b/d1/dab28bed3bc9172d44100e5fae8107bd01ef85fc6bddb80d223d0d9f709f/watchfiles-0.23.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b1a950ab299a4a78fd6369a97b8763732bfb154fdb433356ec55a5bce9515c1", size = 436805 }, + { url = "https://files.pythonhosted.org/packages/06/9c/46e0d17853b62b5d4bf8095e7b9bb0b0ad4babb6c6133138929473f161f3/watchfiles-0.23.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8d3c5cd327dd6ce0edfc94374fb5883d254fe78a5e9d9dfc237a1897dc73cd1", size = 456411 }, + { url = "https://files.pythonhosted.org/packages/2c/ff/e891b230bcf3a648352a00b920d4a1142a938f0b97c9e8e27c2eaaeda221/watchfiles-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ff785af8bacdf0be863ec0c428e3288b817e82f3d0c1d652cd9c6d509020dd0", size = 471563 }, + { url = "https://files.pythonhosted.org/packages/0b/07/f5b54afa8b7c33386c5778d92e681562939900f4ee1c6de9bffc49e7221f/watchfiles-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02b7ba9d4557149410747353e7325010d48edcfe9d609a85cb450f17fd50dc3d", size = 478385 }, + { url = "https://files.pythonhosted.org/packages/a3/b6/243c1dd351ac9b8258a3ea99c33d04ecdc9766e6c7f13a43452883e92a7a/watchfiles-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a1b05c0afb2cd2f48c1ed2ae5487b116e34b93b13074ed3c22ad5c743109f0", size = 427485 }, + { url = "https://files.pythonhosted.org/packages/28/8a/6d00aa4aa9a9938de645c1d411e3af82e74db8d25a0c05427b7a88b4d8d3/watchfiles-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:109a61763e7318d9f821b878589e71229f97366fa6a5c7720687d367f3ab9eef", size = 615839 }, + { url = "https://files.pythonhosted.org/packages/5a/d9/120d212d2952342e2c9673096f5c17cd48e90a7c9ff203ab1ad2f974befe/watchfiles-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9f8e6bb5ac007d4a4027b25f09827ed78cbbd5b9700fd6c54429278dacce05d1", size = 596603 }, + { url = "https://files.pythonhosted.org/packages/3b/25/ec3676b140a93ac256d058a6f82810cf5e0e42fd444b948c62bc56f57f52/watchfiles-0.23.0-cp313-none-win32.whl", hash = "sha256:f46c6f0aec8d02a52d97a583782d9af38c19a29900747eb048af358a9c1d8e5b", size = 263898 }, + { url = "https://files.pythonhosted.org/packages/1a/c6/bf3b8cbe6944499fbe0d400175560a200cdecadccbacc8ace74486565d74/watchfiles-0.23.0-cp313-none-win_amd64.whl", hash = "sha256:f449afbb971df5c6faeb0a27bca0427d7b600dd8f4a068492faec18023f0dcff", size = 275220 }, + { url = "https://files.pythonhosted.org/packages/f7/7c/135a60260dd055227eb3b38f0be5fc16409ad58c5c6636467b27991fd863/watchfiles-0.23.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6a9265cf87a5b70147bfb2fec14770ed5b11a5bb83353f0eee1c25a81af5abfe", size = 376161 }, + { url = "https://files.pythonhosted.org/packages/5c/25/6511ed7bc826ddc2a4e879cf469621a1184719e97d63e7f723e95991ebd3/watchfiles-0.23.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f02a259fcbbb5fcfe7a0805b1097ead5ba7a043e318eef1db59f93067f0b49b", size = 369829 }, + { url = "https://files.pythonhosted.org/packages/73/d3/00d561a66aa000251ed598f576e8bfd1c4102f9956fc06310e9b53258d3e/watchfiles-0.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebaebb53b34690da0936c256c1cdb0914f24fb0e03da76d185806df9328abed", size = 443386 }, + { url = "https://files.pythonhosted.org/packages/00/24/1c089457e39a0e6a142df8cb795a690b71f05c948bc60df4ec12359956b8/watchfiles-0.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd257f98cff9c6cb39eee1a83c7c3183970d8a8d23e8cf4f47d9a21329285cee", size = 429214 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + +[[package]] +name = "weaviate-client" +version = "4.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "grpcio-health-checking", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "grpcio-tools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "validators", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/1e/68cd8306f0c9bf617c79096316e6a4a59edc3a6c2e1171daab661fd0cf0e/weaviate_client-4.7.1.tar.gz", hash = "sha256:af99ac4e53613d2ff5b797372e95d004d0c8a1dd10a7f592068bcb423a30af30", size = 676797 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/75/5c90f12d228ed9d02a3a4447f5f769e966d9885b6d0d0276ecf1f73a4609/weaviate_client-4.7.1-py3-none-any.whl", hash = "sha256:342f5c67b126cee4dc3a60467ad1ae74971cd5614e27af6fb13d687a345352c4", size = 368293 }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, +] + +[[package]] +name = "websockets" +version = "13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/b0/e53bdd53d86447d211694f3cf66f163d077c5d68e6bcaa726bf64e88ae3a/websockets-13.0.tar.gz", hash = "sha256:b7bf950234a482b7461afdb2ec99eee3548ec4d53f418c7990bb79c620476602", size = 147622 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/cc/8b3007ecf2d4e423251b2b3606c276e3fe85298982fc4fd0785a17b73ffb/websockets-13.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad4fa707ff9e2ffee019e946257b5300a45137a58f41fbd9a4db8e684ab61528", size = 150919 }, + { url = "https://files.pythonhosted.org/packages/1b/17/44553bd98608378b0d17432431a0f8f4633a6799826418f93ac036125000/websockets-13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6fd757f313c13c34dae9f126d3ba4cf97175859c719e57c6a614b781c86b617e", size = 148574 }, + { url = "https://files.pythonhosted.org/packages/ee/38/ac6d8f50dc8ac81c29036d6d26aafae3fcbb43cfe88e8bc35a0e6af24525/websockets-13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cbac2eb7ce0fac755fb983c9247c4a60c4019bcde4c0e4d167aeb17520cc7ef1", size = 148830 }, + { url = "https://files.pythonhosted.org/packages/6e/6e/b831097bb1843200d8636245f45fb8daaf4512329e8036f0f0b7ecd80f1c/websockets-13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4b83cf7354cbbc058e97b3e545dceb75b8d9cf17fd5a19db419c319ddbaaf7a", size = 157909 }, + { url = "https://files.pythonhosted.org/packages/e0/d9/4ceef7fb370eca3c33d02966e972c08ef49073199ad02ef9f0f9f2f6f107/websockets-13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9202c0010c78fad1041e1c5285232b6508d3633f92825687549540a70e9e5901", size = 156920 }, + { url = "https://files.pythonhosted.org/packages/b0/b1/8fb8bfad33f01d9085934c39bf5171c372edebed4c5440b28cb3270c0d56/websockets-13.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6566e79c8c7cbea75ec450f6e1828945fc5c9a4769ceb1c7b6e22470539712", size = 157233 }, + { url = "https://files.pythonhosted.org/packages/ad/0a/1dbe4f15cb2fc6d2efea9e7c55651102dc52a10d34d322c0af8d332592be/websockets-13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e7fcad070dcd9ad37a09d89a4cbc2a5e3e45080b88977c0da87b3090f9f55ead", size = 157632 }, + { url = "https://files.pythonhosted.org/packages/9f/97/4e7e98b694ef3db9a9776cbc4f72121cf408c47d7bc1ec582cfd9fa16de1/websockets-13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a8f7d65358a25172db00c69bcc7df834155ee24229f560d035758fd6613111a", size = 157049 }, + { url = "https://files.pythonhosted.org/packages/4f/8c/50c0b58e99a7dc19282b706b99316327380065d8b2325aa0c7ae0479a98a/websockets-13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63b702fb31e3f058f946ccdfa551f4d57a06f7729c369e8815eb18643099db37", size = 157001 }, + { url = "https://files.pythonhosted.org/packages/a4/6f/cf602a9addf38396a2543bcd2c120651324169c0e88aa68a86a9b1e1f648/websockets-13.0-cp310-cp310-win32.whl", hash = "sha256:3a20cf14ba7b482c4a1924b5e061729afb89c890ca9ed44ac4127c6c5986e424", size = 151754 }, + { url = "https://files.pythonhosted.org/packages/d0/bf/4a0bab951456884638c9bbf4a6b5314e755217632be8da831f8b3c6d3954/websockets-13.0-cp310-cp310-win_amd64.whl", hash = "sha256:587245f0704d0bb675f919898d7473e8827a6d578e5a122a21756ca44b811ec8", size = 152192 }, + { url = "https://files.pythonhosted.org/packages/12/29/9fdf8a7f1ced2bac55d36e0b879991498c9858f1e524763434025948d254/websockets-13.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06df8306c241c235075d2ae77367038e701e53bc8c1bb4f6644f4f53aa6dedd0", size = 150915 }, + { url = "https://files.pythonhosted.org/packages/b9/27/723276e7fcb41a3e0859e347014e3e24637982a29222132746b98095ec02/websockets-13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85a1f92a02f0b8c1bf02699731a70a8a74402bb3f82bee36e7768b19a8ed9709", size = 148575 }, + { url = "https://files.pythonhosted.org/packages/04/54/39b1f809e34f78ebb1dcb9cf57465db9705bbf59f30bd1b3b381272dff2b/websockets-13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9ed02c604349068d46d87ef4c2012c112c791f2bec08671903a6bb2bd9c06784", size = 148825 }, + { url = "https://files.pythonhosted.org/packages/fe/df/0a8a90162c32ceb9f28415291c1d689310b503288d29169302964105a351/websockets-13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b89849171b590107f6724a7b0790736daead40926ddf47eadf998b4ff51d6414", size = 158482 }, + { url = "https://files.pythonhosted.org/packages/20/05/227dbb1861cd1e2eb04ac79b136da841dbf6f196e4dc0bd1e67edb4ee69d/websockets-13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:939a16849d71203628157a5e4a495da63967c744e1e32018e9b9e2689aca64d4", size = 157478 }, + { url = "https://files.pythonhosted.org/packages/fe/dd/3384d3eb26022703895d6ed65aec2d3af6976c3d9aed06200a322e7192cb/websockets-13.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad818cdac37c0ad4c58e51cb4964eae4f18b43c4a83cb37170b0d90c31bd80cf", size = 157855 }, + { url = "https://files.pythonhosted.org/packages/93/ad/0320a24cd8309e1a257d43d762a732162f2956b769c1ad950b70d4d4d15a/websockets-13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cbfe82a07596a044de78bb7a62519e71690c5812c26c5f1d4b877e64e4f46309", size = 158160 }, + { url = "https://files.pythonhosted.org/packages/d0/33/acc24e576228301d1dc23ce9d3f7d20f51dfe6c16d1b241e6ba4b2904d3e/websockets-13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e07e76c49f39c5b45cbd7362b94f001ae209a3ea4905ae9a09cfd53b3c76373d", size = 157598 }, + { url = "https://files.pythonhosted.org/packages/83/47/01645a0ea041e32a9d8946a324845beb8daba2e2f00ee4fd2d04d3ceb598/websockets-13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:372f46a0096cfda23c88f7e42349a33f8375e10912f712e6b496d3a9a557290f", size = 157548 }, + { url = "https://files.pythonhosted.org/packages/73/89/ea73bc41934eb3ea3f0c04fa7b16455ec5925b8b72aa5e016bd22df5feb5/websockets-13.0-cp311-cp311-win32.whl", hash = "sha256:376a43a4fd96725f13450d3d2e98f4f36c3525c562ab53d9a98dd2950dca9a8a", size = 151756 }, + { url = "https://files.pythonhosted.org/packages/9b/b1/81f655476532b31c39814d55a1dc1e97ecedc5a1b4f9517ee665aec398f6/websockets-13.0-cp311-cp311-win_amd64.whl", hash = "sha256:2be1382a4daa61e2f3e2be3b3c86932a8db9d1f85297feb6e9df22f391f94452", size = 152200 }, + { url = "https://files.pythonhosted.org/packages/ad/0a/baeea2931827e73ebe3d958fad9df74ec66d08341d0cf701ced0381adc91/websockets-13.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5407c34776b9b77bd89a5f95eb0a34aaf91889e3f911c63f13035220eb50107", size = 150928 }, + { url = "https://files.pythonhosted.org/packages/6d/f7/306e2940829db34c5866e869eb5b1a08dd04d1c6d25c71327a028d124871/websockets-13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4782ec789f059f888c1e8fdf94383d0e64b531cffebbf26dd55afd53ab487ca4", size = 148585 }, + { url = "https://files.pythonhosted.org/packages/2b/3c/183a4f79e0ce6be8733f824e0a48db3771a373a7206aef900bc1ae4c176e/websockets-13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8feb8e19ef65c9994e652c5b0324abd657bedd0abeb946fb4f5163012c1e730", size = 148821 }, + { url = "https://files.pythonhosted.org/packages/03/32/37e1c9dd9aa1e7fa6fb3147d6992d61a20ba63ffee2adc88a392e1ae7376/websockets-13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f3d2e20c442b58dbac593cb1e02bc02d149a86056cc4126d977ad902472e3b", size = 158746 }, + { url = "https://files.pythonhosted.org/packages/6c/da/0cace6358289c7de1ee02ed0d572dfe92e5cb97270bda60f04a4e49ac5c5/websockets-13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e39d393e0ab5b8bd01717cc26f2922026050188947ff54fe6a49dc489f7750b7", size = 157699 }, + { url = "https://files.pythonhosted.org/packages/c7/ab/b763b0e8598c4251ec6e17d18f46cbced157772b991200fb0d32550844c5/websockets-13.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f661a4205741bdc88ac9c2b2ec003c72cee97e4acd156eb733662ff004ba429", size = 158124 }, + { url = "https://files.pythonhosted.org/packages/d0/2d/40b8c3ba08792c2ecdb81613671a4b9bd33b83c50519b235e8eeb0ae21a0/websockets-13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:384129ad0490e06bab2b98c1da9b488acb35bb11e2464c728376c6f55f0d45f3", size = 158415 }, + { url = "https://files.pythonhosted.org/packages/4c/5e/9a42db20f6c38d247a900bfb8633953df93d8873a99ed9432645a4d5e185/websockets-13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:df5c0eff91f61b8205a6c9f7b255ff390cdb77b61c7b41f79ca10afcbb22b6cb", size = 157795 }, + { url = "https://files.pythonhosted.org/packages/87/52/7fb5f052eefaa5d2b42da06b314c2af0467fadbd7f360716a1a4d4f7ab67/websockets-13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02cc9bb1a887dac0e08bf657c5d00aa3fac0d03215d35a599130c2034ae6663a", size = 157791 }, + { url = "https://files.pythonhosted.org/packages/9c/8b/4b7064d1a40fcb85f64bc051d8bdc8a9e388572eb5bec5cb85ffb2c43e01/websockets-13.0-cp312-cp312-win32.whl", hash = "sha256:d9726d2c9bd6aed8cb994d89b3910ca0079406edce3670886ec828a73e7bdd53", size = 151765 }, + { url = "https://files.pythonhosted.org/packages/8b/a3/297207726b292e85b9a8ce24ef6ab16a056c457100e915a67b6928a58fa9/websockets-13.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0839f35322f7b038d8adcf679e2698c3a483688cc92e3bd15ee4fb06669e9a", size = 152202 }, + { url = "https://files.pythonhosted.org/packages/03/b6/778678e1ff104df3a869dacb0bc845df34d74f2ff7451f99babccd212203/websockets-13.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:da7e501e59857e8e3e9d10586139dc196b80445a591451ca9998aafba1af5278", size = 150936 }, + { url = "https://files.pythonhosted.org/packages/fa/25/28609b2555f11e4913a4021147b7a7c5117b5c41da5d26a604a91bae85b9/websockets-13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a00e1e587c655749afb5b135d8d3edcfe84ec6db864201e40a882e64168610b3", size = 148590 }, + { url = "https://files.pythonhosted.org/packages/cb/1f/e06fb15fde90683fd98e6ca44fb54fe579161ce553d54fdbb578014ae1a7/websockets-13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a7fbf2a8fe7556a8f4e68cb3e736884af7bf93653e79f6219f17ebb75e97d8f0", size = 148826 }, + { url = "https://files.pythonhosted.org/packages/22/00/9892eee346f44cd814c18888bc1a05880e3f8091e4eb999e6b34634cd278/websockets-13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ea9c9c7443a97ea4d84d3e4d42d0e8c4235834edae652993abcd2aff94affd7", size = 158717 }, + { url = "https://files.pythonhosted.org/packages/dc/ad/2bdc3a5dd60b639e0f8e76ee4a57fda27abaf05f604708c61c6fd7f8ad88/websockets-13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35c2221b539b360203f3f9ad168e527bf16d903e385068ae842c186efb13d0ea", size = 157660 }, + { url = "https://files.pythonhosted.org/packages/0c/14/5585de16939608b77a37f8b88e1bd1d430d95ec19d3a8c26ec42a91f2815/websockets-13.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:358d37c5c431dd050ffb06b4b075505aae3f4f795d7fff9794e5ed96ce99b998", size = 158104 }, + { url = "https://files.pythonhosted.org/packages/7b/1e/6cd9063fd34fe7f649ed9a56d3c91e80dea95cf3ab3344203ee774d51a56/websockets-13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:038e7a0f1bfafc7bf52915ab3506b7a03d1e06381e9f60440c856e8918138151", size = 158463 }, + { url = "https://files.pythonhosted.org/packages/d9/4d/c3282f8e54103f3d38b5e56851d00911dafd0c37c8d03a9ecc7a25f2a9da/websockets-13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fd038bc9e2c134847f1e0ce3191797fad110756e690c2fdd9702ed34e7a43abb", size = 157850 }, + { url = "https://files.pythonhosted.org/packages/a1/08/af4f67b74cc6891ee1c34a77b47a3cb77081b824c3df92c1196980df9a4f/websockets-13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93b8c2008f372379fb6e5d2b3f7c9ec32f7b80316543fd3a5ace6610c5cde1b0", size = 157843 }, + { url = "https://files.pythonhosted.org/packages/b4/b7/2c991e51d48b1b98847d0a0b608508a3b687f215a2390f99cf0ee7dd2777/websockets-13.0-cp313-cp313-win32.whl", hash = "sha256:851fd0afb3bc0b73f7c5b5858975d42769a5fdde5314f4ef2c106aec63100687", size = 151763 }, + { url = "https://files.pythonhosted.org/packages/bc/0f/f06ed6485cf9cdea7d89c2f6e9d19f1be963ba5d26fb79760bfd17dd4aa5/websockets-13.0-cp313-cp313-win_amd64.whl", hash = "sha256:7d14901fdcf212804970c30ab9ee8f3f0212e620c7ea93079d6534863444fb4e", size = 152197 }, + { url = "https://files.pythonhosted.org/packages/e0/1e/f7260a625b210f8242d0d858a3006a54b632843b796db39d9deb90068031/websockets-13.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:602cbd010d8c21c8475f1798b705bb18567eb189c533ab5ef568bc3033fdf417", size = 148603 }, + { url = "https://files.pythonhosted.org/packages/b7/b6/3462a3a2688a62ee52aa1555fd47c61ffad0b12d0ed6ccdefd1ef8c3eef4/websockets-13.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:bf8eb5dca4f484a60f5327b044e842e0d7f7cdbf02ea6dc4a4f811259f1f1f0b", size = 148837 }, + { url = "https://files.pythonhosted.org/packages/ca/74/9f7c4669c5b5e154384eace44a5a3e24609c230f1428fea6b9af257a66c5/websockets-13.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d795c1802d99a643bf689b277e8604c14b5af1bc0a31dade2cd7a678087212", size = 150200 }, + { url = "https://files.pythonhosted.org/packages/c0/33/a307018b358f5cca141497e95f9af19c3e8be748219773afc4fcd4791123/websockets-13.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:788bc841d250beccff67a20a5a53a15657a60111ef9c0c0a97fbdd614fae0fe2", size = 149804 }, + { url = "https://files.pythonhosted.org/packages/d9/62/c514d5b087f7b2cab8d97c80213d7ee8196b5954f8466886146c09d4fc46/websockets-13.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7334752052532c156d28b8eaf3558137e115c7871ea82adff69b6d94a7bee273", size = 149754 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/b11dd0a18b9bd876158c463ac1a6cab7b1b38093866fce22d03ab5462258/websockets-13.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7a1963302947332c3039e3f66209ec73b1626f8a0191649e0713c391e9f5b0d", size = 152235 }, + { url = "https://files.pythonhosted.org/packages/b2/89/c0be9f09eea478659e9d936210ff03e6a2a3a8d4b8dfac6b1143ff646ded/websockets-13.0-py3-none-any.whl", hash = "sha256:dbbac01e80aee253d44c4f098ab3cc17c822518519e869b284cfbb8cd16cc9de", size = 142957 }, +] + +[[package]] +name = "werkzeug" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/51/2e0fc149e7a810d300422ab543f87f2bcf64d985eb6f1228c4efd6e4f8d4/werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18", size = 803342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/6e/e792999e816d19d7fcbfa94c730936750036d65656a76a5a688b57a656c4/werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8", size = 227274 }, +] + +[[package]] +name = "wrapt" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", size = 53972 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/c6/5375258add3777494671d8cec27cdf5402abd91016dee24aa2972c61fedf/wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4", size = 37315 }, + { url = "https://files.pythonhosted.org/packages/32/12/e11adfde33444986135d8881b401e4de6cbb4cced046edc6b464e6ad7547/wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020", size = 38160 }, + { url = "https://files.pythonhosted.org/packages/70/7d/3dcc4a7e96f8d3e398450ec7703db384413f79bd6c0196e0e139055ce00f/wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440", size = 80419 }, + { url = "https://files.pythonhosted.org/packages/d1/c4/8dfdc3c2f0b38be85c8d9fdf0011ebad2f54e40897f9549a356bebb63a97/wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487", size = 72669 }, + { url = "https://files.pythonhosted.org/packages/49/83/b40bc1ad04a868b5b5bcec86349f06c1ee1ea7afe51dc3e46131e4f39308/wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf", size = 80271 }, + { url = "https://files.pythonhosted.org/packages/19/d4/cd33d3a82df73a064c9b6401d14f346e1d2fb372885f0295516ec08ed2ee/wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72", size = 84748 }, + { url = "https://files.pythonhosted.org/packages/ef/58/2fde309415b5fa98fd8f5f4a11886cbf276824c4c64d45a39da342fff6fe/wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0", size = 77522 }, + { url = "https://files.pythonhosted.org/packages/07/44/359e4724a92369b88dbf09878a7cde7393cf3da885567ea898e5904049a3/wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136", size = 84780 }, + { url = "https://files.pythonhosted.org/packages/88/8f/706f2fee019360cc1da652353330350c76aa5746b4e191082e45d6838faf/wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d", size = 35335 }, + { url = "https://files.pythonhosted.org/packages/19/2b/548d23362e3002ebbfaefe649b833fa43f6ca37ac3e95472130c4b69e0b4/wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2", size = 37528 }, + { url = "https://files.pythonhosted.org/packages/fd/03/c188ac517f402775b90d6f312955a5e53b866c964b32119f2ed76315697e/wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", size = 37313 }, + { url = "https://files.pythonhosted.org/packages/0f/16/ea627d7817394db04518f62934a5de59874b587b792300991b3c347ff5e0/wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", size = 38164 }, + { url = "https://files.pythonhosted.org/packages/7f/a7/f1212ba098f3de0fd244e2de0f8791ad2539c03bef6c05a9fcb03e45b089/wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", size = 80890 }, + { url = "https://files.pythonhosted.org/packages/b7/96/bb5e08b3d6db003c9ab219c487714c13a237ee7dcc572a555eaf1ce7dc82/wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", size = 73118 }, + { url = "https://files.pythonhosted.org/packages/6e/52/2da48b35193e39ac53cfb141467d9f259851522d0e8c87153f0ba4205fb1/wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", size = 80746 }, + { url = "https://files.pythonhosted.org/packages/11/fb/18ec40265ab81c0e82a934de04596b6ce972c27ba2592c8b53d5585e6bcd/wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", size = 85668 }, + { url = "https://files.pythonhosted.org/packages/0f/ef/0ecb1fa23145560431b970418dce575cfaec555ab08617d82eb92afc7ccf/wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", size = 78556 }, + { url = "https://files.pythonhosted.org/packages/25/62/cd284b2b747f175b5a96cbd8092b32e7369edab0644c45784871528eb852/wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", size = 85712 }, + { url = "https://files.pythonhosted.org/packages/e5/a7/47b7ff74fbadf81b696872d5ba504966591a3468f1bc86bca2f407baef68/wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", size = 35327 }, + { url = "https://files.pythonhosted.org/packages/cf/c3/0084351951d9579ae83a3d9e38c140371e4c6b038136909235079f2e6e78/wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", size = 37523 }, + { url = "https://files.pythonhosted.org/packages/92/17/224132494c1e23521868cdd57cd1e903f3b6a7ba6996b7b8f077ff8ac7fe/wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", size = 37614 }, + { url = "https://files.pythonhosted.org/packages/6a/d7/cfcd73e8f4858079ac59d9db1ec5a1349bc486ae8e9ba55698cc1f4a1dff/wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", size = 38316 }, + { url = "https://files.pythonhosted.org/packages/7e/79/5ff0a5c54bda5aec75b36453d06be4f83d5cd4932cc84b7cb2b52cee23e2/wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", size = 86322 }, + { url = "https://files.pythonhosted.org/packages/c4/81/e799bf5d419f422d8712108837c1d9bf6ebe3cb2a81ad94413449543a923/wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", size = 79055 }, + { url = "https://files.pythonhosted.org/packages/62/62/30ca2405de6a20448ee557ab2cd61ab9c5900be7cbd18a2639db595f0b98/wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", size = 87291 }, + { url = "https://files.pythonhosted.org/packages/49/4e/5d2f6d7b57fc9956bf06e944eb00463551f7d52fc73ca35cfc4c2cdb7aed/wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", size = 90374 }, + { url = "https://files.pythonhosted.org/packages/a6/9b/c2c21b44ff5b9bf14a83252a8b973fb84923764ff63db3e6dfc3895cf2e0/wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", size = 83896 }, + { url = "https://files.pythonhosted.org/packages/14/26/93a9fa02c6f257df54d7570dfe8011995138118d11939a4ecd82cb849613/wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", size = 91738 }, + { url = "https://files.pythonhosted.org/packages/a2/5b/4660897233eb2c8c4de3dc7cefed114c61bacb3c28327e64150dc44ee2f6/wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", size = 35568 }, + { url = "https://files.pythonhosted.org/packages/5c/cc/8297f9658506b224aa4bd71906447dea6bb0ba629861a758c28f67428b91/wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", size = 37653 }, + { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362 }, +] + +[[package]] +name = "yarl" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "multidict", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/ad/bedcdccbcbf91363fd425a948994f3340924145c2bc8ccb296f4a1e52c28/yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", size = 141869 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/27/cda5a927df3a894eddfee4efacdd230c2d8486e322fc672194fd651f82c5/yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", size = 129061 }, + { url = "https://files.pythonhosted.org/packages/d5/fc/40b85bea1f5686092ea37f472c94c023d6347266852ffd55baa01c40f596/yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", size = 81246 }, + { url = "https://files.pythonhosted.org/packages/81/c6/06938036ea48fa74521713499fba1459b0eb60af9b9afbe8e0e9e1a96c36/yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", size = 79176 }, + { url = "https://files.pythonhosted.org/packages/30/b5/215d586d5cb17ca9748d7a2d597c07147f210c0c0785257492094d083b65/yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", size = 297669 }, + { url = "https://files.pythonhosted.org/packages/dd/90/2958ae9f2e12084d616eef95b6a48c8e6d96448add04367c20dc53a33ff2/yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", size = 311909 }, + { url = "https://files.pythonhosted.org/packages/0b/58/dd3c69651381a57ac991dba54b20ae2da359eb4b03a661e71c451d6525c6/yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", size = 308690 }, + { url = "https://files.pythonhosted.org/packages/c3/a0/0ade1409d184cbc9e85acd403a386a7c0563b92ff0f26d138ff9e86e48b4/yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", size = 301580 }, + { url = "https://files.pythonhosted.org/packages/6d/a1/db0bdf8cc48515e9c02daf04ae2916fc27ce6498eca21432fc9ffa63f71b/yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", size = 291231 }, + { url = "https://files.pythonhosted.org/packages/b2/4f/796b0c73e9ff30a1047a7ee3390e157ab8424d4401b9f32a2624013a5b39/yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", size = 301079 }, + { url = "https://files.pythonhosted.org/packages/0b/a3/7774786ec6e2dca0bb38b286f12a11af97957546e5fbcce71752a8d2cf07/yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", size = 295202 }, + { url = "https://files.pythonhosted.org/packages/70/a9/ef6d69ce9a4e82080290bcb6db735bb8a6d6db92f2bbb92b6951bde97e7c/yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", size = 311784 }, + { url = "https://files.pythonhosted.org/packages/44/ae/fdbc9965ef69e650c3b5b04d60badef90ff0cde21a30770f0700e148b12f/yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", size = 311134 }, + { url = "https://files.pythonhosted.org/packages/cc/2a/abbaf1460becba856e163f2a1274f5d34b1969d476da8e68a8fc2aeb5661/yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", size = 304584 }, + { url = "https://files.pythonhosted.org/packages/a3/73/dd7ced8d9731bd2ef4fdff5da23ce2f772ca04e8ddee886a6b15248d9e65/yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", size = 70175 }, + { url = "https://files.pythonhosted.org/packages/31/d4/2085272a5ccf87af74d4e02787c242c5d60367840a4637b2835565264302/yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", size = 76402 }, + { url = "https://files.pythonhosted.org/packages/12/65/4c7f3676209a569405c9f0f492df2bc3a387c253f5d906e36944fdd12277/yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", size = 132836 }, + { url = "https://files.pythonhosted.org/packages/3b/c5/81e3dbf5271ab1510860d2ae7a704ef43f93f7cb9326bf7ebb1949a7260b/yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", size = 83215 }, + { url = "https://files.pythonhosted.org/packages/20/3d/7dabf580dfc0b588e48830486b488858122b10a61f33325e0d7cf1d6180b/yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", size = 81237 }, + { url = "https://files.pythonhosted.org/packages/38/45/7c669999f5d350f4f8f74369b94e0f6705918eee18e38610bfe44af93d4f/yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", size = 324181 }, + { url = "https://files.pythonhosted.org/packages/50/49/aa04effe2876cced8867bf9d89b620acf02b733c62adfe22a8218c35d70b/yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", size = 339412 }, + { url = "https://files.pythonhosted.org/packages/7d/95/4310771fb9c71599d8466f43347ac18fafd501621e65b93f4f4f16899b1d/yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", size = 337973 }, + { url = "https://files.pythonhosted.org/packages/9f/ea/94ad7d8299df89844e666e4aa8a0e9b88e02416cd6a7dd97969e9eae5212/yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", size = 328126 }, + { url = "https://files.pythonhosted.org/packages/6d/be/9d4885e2725f5860833547c9e4934b6e0f44a355b24ffc37957264761e3e/yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", size = 316677 }, + { url = "https://files.pythonhosted.org/packages/4a/70/5c744d67cad3d093e233cb02f37f2830cb89abfcbb7ad5b5af00ff21d14d/yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", size = 324243 }, + { url = "https://files.pythonhosted.org/packages/c2/80/8b38d8fed958ac37afb8b81a54bf4f767b107e2c2004dab165edb58fc51b/yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", size = 318099 }, + { url = "https://files.pythonhosted.org/packages/59/50/715bbc7bda65291f9295e757f67854206f4d8be9746d39187724919ac14d/yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", size = 334924 }, + { url = "https://files.pythonhosted.org/packages/a8/af/ca9962488027576d7162878a1864cbb1275d298af986ce96bdfd4807d7b2/yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", size = 335060 }, + { url = "https://files.pythonhosted.org/packages/28/c7/249a3a903d500ca7369eb542e2847a14f12f249638dcc10371db50cd17ff/yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", size = 326689 }, + { url = "https://files.pythonhosted.org/packages/ec/0c/f02dd0b875a7a460f95dc7cf18983ed43c693283d6ab92e0ad71b9e0de8f/yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", size = 70407 }, + { url = "https://files.pythonhosted.org/packages/27/41/945ae9a80590e4fb0be166863c6e63d75e4b35789fa3a61ff1dbdcdc220f/yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", size = 76719 }, + { url = "https://files.pythonhosted.org/packages/7b/cd/a921122610dedfed94e494af18e85aae23e93274c00ca464cfc591c8f4fb/yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", size = 129561 }, + { url = "https://files.pythonhosted.org/packages/7c/a0/887c93020c788f249c24eaab288c46e5fed4d2846080eaf28ed3afc36e8d/yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", size = 81595 }, + { url = "https://files.pythonhosted.org/packages/54/99/ed3c92c38f421ba6e36caf6aa91c34118771d252dce800118fa2f44d7962/yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", size = 79400 }, + { url = "https://files.pythonhosted.org/packages/ea/45/65801be625ef939acc8b714cf86d4a198c0646e80dc8970359d080c47204/yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", size = 317397 }, + { url = "https://files.pythonhosted.org/packages/06/91/9696601a8ba674c8f0c15035cc9e94ca31f541330364adcfd5a399f598bf/yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", size = 327246 }, + { url = "https://files.pythonhosted.org/packages/da/3e/bf25177b3618889bf067aacf01ef54e910cd569d14e2f84f5e7bec23bb82/yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", size = 327321 }, + { url = "https://files.pythonhosted.org/packages/28/1c/bdb3411467b805737dd2720b85fd082e49f59bf0cc12dc1dfcc80ab3d274/yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", size = 322424 }, + { url = "https://files.pythonhosted.org/packages/41/e9/53bc89f039df2824a524a2aa03ee0bfb8f0585b08949e7521f5eab607085/yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", size = 310868 }, + { url = "https://files.pythonhosted.org/packages/79/cd/a78c3b0304a4a970b5ae3993f4f5f649443bc8bfa5622f244aed44c810ed/yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", size = 323452 }, + { url = "https://files.pythonhosted.org/packages/2e/5e/1c78eb05ae0efae08498fd7ab939435a29f12c7f161732e7fe327e5b8ca1/yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", size = 313554 }, + { url = "https://files.pythonhosted.org/packages/04/e0/0029563a8434472697aebb269fdd2ffc8a19e3840add1d5fa169ec7c56e3/yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", size = 331029 }, + { url = "https://files.pythonhosted.org/packages/de/1b/7e6b1ad42ccc0ed059066a7ae2b6fd4bce67795d109a99ccce52e9824e96/yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", size = 333839 }, + { url = "https://files.pythonhosted.org/packages/85/8a/c364d6e2eeb4e128a5ee9a346fc3a09aa76739c0c4e2a7305989b54f174b/yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", size = 328251 }, + { url = "https://files.pythonhosted.org/packages/ec/9d/0da94b33b9fb89041e10f95a14a55b0fef36c60b6a1d5ff85a0c2ecb1a97/yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", size = 70195 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/2fdc5a11503bc61818243653d836061c9ce0370e2dd9ac5917258a007675/yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", size = 76397 }, + { url = "https://files.pythonhosted.org/packages/4d/05/4d79198ae568a92159de0f89e710a8d19e3fa267b719a236582eee921f4a/yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", size = 31638 }, +] + +[[package]] +name = "zipp" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/af/9f2de5bd32549a1b705af7a7c054af3878816a1267cb389c03cc4f342a51/zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31", size = 23244 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/cc/b9958af9f9c86b51f846d8487440af495ecf19b16e426fce1ed0b0796175/zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d", size = 9432 }, +] From 09190d071bac38056e058e92b9bfd4b04561c549 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:57:01 -0700 Subject: [PATCH 76/87] .Net: [Feature Branch] .Net: Increase RC package version to 1.18.1 (#8400) --- dotnet/nuget/nuget-package.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 107443170bc2..a777640ea773 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -1,7 +1,7 @@ - 1.18.0 + 1.18.1 $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix) From 742aa4711e6c3dabf7ac2d5cafd274a6be718904 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:12:16 +0100 Subject: [PATCH 77/87] Python: .Net: OpenAI V2 - `main` MergeFix (#8405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Motivation and Context Merge fixes --------- Signed-off-by: dependabot[bot] Co-authored-by: Dr. Artificial曾小健 <875100501@qq.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Co-authored-by: Tao Chen Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Co-authored-by: Maurycy Markowski Co-authored-by: gparmigiani Co-authored-by: Atiqur Rahman Foyshal <113086917+atiq-bs23@users.noreply.github.com> Co-authored-by: Eduard van Valkenburg Co-authored-by: Andrew Desousa <33275002+andrewldesousa@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- python/.cspell.json | 4 +- python/.pre-commit-config.yaml | 1 + python/LICENSE | 21 ++ python/Makefile | 8 +- python/pyproject.toml | 8 +- .../.env.example | 4 +- .../getting_started/00-getting-started.ipynb | 7 +- .../01-basic-loading-the-kernel.ipynb | 9 +- .../02-running-prompts-from-file.ipynb | 7 +- .../03-prompt-function-inline.ipynb | 7 +- .../04-kernel-arguments-chat.ipynb | 7 +- .../05-using-the-planner.ipynb | 7 +- .../06-memory-and-embeddings.ipynb | 11 +- .../07-hugging-face-for-plugins.ipynb | 7 +- .../08-native-function-inline.ipynb | 7 +- .../09-groundedness-checking.ipynb | 7 +- .../10-multiple-results-per-prompt.ipynb | 9 +- .../11-streaming-completions.ipynb | 7 +- .../weaviate-persistent-memory.ipynb | 6 +- python/semantic_kernel/__init__.py | 2 +- .../services/anthropic_chat_completion.py | 82 +++--- .../services/azure_ai_inference_base.py | 3 + .../azure_ai_inference_chat_completion.py | 3 + .../google_ai/services/google_ai_base.py | 3 + .../services/google_ai_chat_completion.py | 2 + .../services/google_ai_text_completion.py | 2 + .../vertex_ai/services/vertex_ai_base.py | 3 + .../services/vertex_ai_chat_completion.py | 2 + .../services/vertex_ai_text_completion.py | 2 + .../ai/mistral_ai/services/mistral_ai_base.py | 16 ++ .../services/mistral_ai_chat_completion.py | 60 ++-- .../services/mistral_ai_text_embedding.py | 34 +-- .../ollama_prompt_execution_settings.py | 3 + .../ai/ollama/services/ollama_base.py | 3 + .../ollama/services/ollama_chat_completion.py | 5 + .../ollama/services/ollama_text_completion.py | 3 + .../services/open_ai_chat_completion_base.py | 2 +- .../services/open_ai_text_completion_base.py | 2 +- .../utils/telemetry/decorators.py | 265 ------------------ .../telemetry/model_diagnostics/__init__.py | 8 + .../telemetry/model_diagnostics/decorators.py | 232 +++++++++++++++ .../gen_ai_attributes.py} | 7 +- .../model_diagnostics_settings.py | 31 ++ python/tests/conftest.py | 10 +- .../test_mistralai_text_embeddings.py | 22 +- .../unit/utils/model_diagnostics/conftest.py | 91 ++++++ .../test_trace_chat_completion.py | 172 ++++++++++++ .../test_trace_text_completion.py | 150 ++++++++++ python/tests/unit/utils/test_tracing.py | 241 ---------------- 49 files changed, 956 insertions(+), 649 deletions(-) create mode 100644 python/LICENSE create mode 100644 python/semantic_kernel/connectors/ai/mistral_ai/services/mistral_ai_base.py delete mode 100644 python/semantic_kernel/utils/telemetry/decorators.py create mode 100644 python/semantic_kernel/utils/telemetry/model_diagnostics/__init__.py create mode 100644 python/semantic_kernel/utils/telemetry/model_diagnostics/decorators.py rename python/semantic_kernel/utils/telemetry/{const.py => model_diagnostics/gen_ai_attributes.py} (83%) create mode 100644 python/semantic_kernel/utils/telemetry/model_diagnostics/model_diagnostics_settings.py create mode 100644 python/tests/unit/utils/model_diagnostics/conftest.py create mode 100644 python/tests/unit/utils/model_diagnostics/test_trace_chat_completion.py create mode 100644 python/tests/unit/utils/model_diagnostics/test_trace_text_completion.py delete mode 100644 python/tests/unit/utils/test_tracing.py diff --git a/python/.cspell.json b/python/.cspell.json index 804c4ebfa4c6..949cce6e3c9a 100644 --- a/python/.cspell.json +++ b/python/.cspell.json @@ -55,6 +55,8 @@ "huggingface", "pytestmark", "contoso", - "opentelemetry" + "opentelemetry", + "SEMANTICKERNEL", + "OTEL" ] } \ No newline at end of file diff --git a/python/.pre-commit-config.yaml b/python/.pre-commit-config.yaml index e964cebe7613..56ae96fa7e1e 100644 --- a/python/.pre-commit-config.yaml +++ b/python/.pre-commit-config.yaml @@ -43,6 +43,7 @@ repos: hooks: # Update the uv lockfile - id: uv-lock + files: ^\./python/(uv\.lock|pyproject\.toml)$ - repo: local hooks: - id: mypy diff --git a/python/LICENSE b/python/LICENSE new file mode 100644 index 000000000000..9e841e7a26e4 --- /dev/null +++ b/python/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/python/Makefile b/python/Makefile index a4eb077db5d7..1e165a1539ba 100644 --- a/python/Makefile +++ b/python/Makefile @@ -1,6 +1,6 @@ SHELL = /bin/bash -.PHONY: help install clean +.PHONY: help install clean build .SILENT: all: install @@ -22,6 +22,7 @@ help: echo " install-sk - install Semantic Kernel and all dependencies" echo " install-pre-commit - install pre-commit hooks" echo " clean - remove the virtualenvs" + echo " build - build the project" echo "" echo -e "\033[1mVARIABLES:\033[0m" echo " PYTHON_VERSION - Python version to use. Default is 3.10" @@ -67,4 +68,7 @@ install-sk: .ONESHELL: clean: # Remove the virtualenv - rm -rf .venv \ No newline at end of file + rm -rf .venv + +build: + uvx --from build pyproject-build --installer uv diff --git a/python/pyproject.toml b/python/pyproject.toml index c012607968bc..f59e68580514 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -5,9 +5,8 @@ authors = [{ name = "Microsoft", email = "SK-Support@microsoft.com"}] readme = "pip/README.md" # Version read from __version__ field in __init__.py by Flit dynamic = ["version"] -packages = [{include = "semantic_kernel"}] requires-python = ">=3.10,<3.13" -license = {file = "../LICENSE"} +license = {file = "LICENSE"} urls.homepage = "https://learn.microsoft.com/en-us/semantic-kernel/overview/" urls.source = "https://github.com/microsoft/semantic-kernel/tree/main/python" urls.release_notes = "https://github.com/microsoft/semantic-kernel/releases?q=tag%3Apython-1&expanded=true" @@ -191,7 +190,10 @@ min-file-size = 1 targets = ["semantic_kernel"] exclude_dirs = ["tests"] +[tool.flit.module] +name = "semantic_kernel" + [build-system] -requires = ["flit-core >= 3.8"] +requires = ["flit-core >= 3.9,<4.0"] build-backend = "flit_core.buildapi" diff --git a/python/samples/demos/telemetry_with_application_insights/.env.example b/python/samples/demos/telemetry_with_application_insights/.env.example index 3ee18ae9e6b0..be404f15d7ff 100644 --- a/python/samples/demos/telemetry_with_application_insights/.env.example +++ b/python/samples/demos/telemetry_with_application_insights/.env.example @@ -1 +1,3 @@ -TELEMETRY_SAMPLE_CONNECTION_STRING="..." \ No newline at end of file +TELEMETRY_SAMPLE_CONNECTION_STRING="..." +SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS=true +SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE=true \ No newline at end of file diff --git a/python/samples/getting_started/00-getting-started.ipynb b/python/samples/getting_started/00-getting-started.ipynb index 950d9ce88a32..d6bcc8a6adf8 100644 --- a/python/samples/getting_started/00-getting-started.ipynb +++ b/python/samples/getting_started/00-getting-started.ipynb @@ -16,8 +16,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.8.0" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { diff --git a/python/samples/getting_started/01-basic-loading-the-kernel.ipynb b/python/samples/getting_started/01-basic-loading-the-kernel.ipynb index 24cae548a047..d74ac91859e5 100644 --- a/python/samples/getting_started/01-basic-loading-the-kernel.ipynb +++ b/python/samples/getting_started/01-basic-loading-the-kernel.ipynb @@ -23,8 +23,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.8.0" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { @@ -196,7 +199,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.10.12" }, "polyglot_notebook": { "kernelInfo": { diff --git a/python/samples/getting_started/02-running-prompts-from-file.ipynb b/python/samples/getting_started/02-running-prompts-from-file.ipynb index 4346ee3479d9..3d975adca1c2 100644 --- a/python/samples/getting_started/02-running-prompts-from-file.ipynb +++ b/python/samples/getting_started/02-running-prompts-from-file.ipynb @@ -34,8 +34,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.8.0" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { diff --git a/python/samples/getting_started/03-prompt-function-inline.ipynb b/python/samples/getting_started/03-prompt-function-inline.ipynb index 1b2fd84dede3..27ce5177424d 100644 --- a/python/samples/getting_started/03-prompt-function-inline.ipynb +++ b/python/samples/getting_started/03-prompt-function-inline.ipynb @@ -24,8 +24,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.8.0" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { diff --git a/python/samples/getting_started/04-kernel-arguments-chat.ipynb b/python/samples/getting_started/04-kernel-arguments-chat.ipynb index bd41cf5be190..3e9ad0861860 100644 --- a/python/samples/getting_started/04-kernel-arguments-chat.ipynb +++ b/python/samples/getting_started/04-kernel-arguments-chat.ipynb @@ -26,8 +26,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.8.0" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { diff --git a/python/samples/getting_started/05-using-the-planner.ipynb b/python/samples/getting_started/05-using-the-planner.ipynb index 07869285daa2..961a37591f01 100644 --- a/python/samples/getting_started/05-using-the-planner.ipynb +++ b/python/samples/getting_started/05-using-the-planner.ipynb @@ -31,8 +31,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.8.0" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { diff --git a/python/samples/getting_started/06-memory-and-embeddings.ipynb b/python/samples/getting_started/06-memory-and-embeddings.ipynb index 637f0ce70496..37ad963e983f 100644 --- a/python/samples/getting_started/06-memory-and-embeddings.ipynb +++ b/python/samples/getting_started/06-memory-and-embeddings.ipynb @@ -36,10 +36,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.8.0\n", - "%pip install azure-core==1.30.1\n", - "%pip install azure-search-documents==11.8.0b4" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel[azure]\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { @@ -611,7 +612,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/python/samples/getting_started/07-hugging-face-for-plugins.ipynb b/python/samples/getting_started/07-hugging-face-for-plugins.ipynb index c9f6a1a01263..22e27fd6cdf0 100644 --- a/python/samples/getting_started/07-hugging-face-for-plugins.ipynb +++ b/python/samples/getting_started/07-hugging-face-for-plugins.ipynb @@ -20,8 +20,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel[hugging_face]==1.8.0" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { diff --git a/python/samples/getting_started/08-native-function-inline.ipynb b/python/samples/getting_started/08-native-function-inline.ipynb index fbe2012bb967..0ce818d6f757 100644 --- a/python/samples/getting_started/08-native-function-inline.ipynb +++ b/python/samples/getting_started/08-native-function-inline.ipynb @@ -54,8 +54,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.8.0" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { diff --git a/python/samples/getting_started/09-groundedness-checking.ipynb b/python/samples/getting_started/09-groundedness-checking.ipynb index 367d1949d6a7..5e87acce7e68 100644 --- a/python/samples/getting_started/09-groundedness-checking.ipynb +++ b/python/samples/getting_started/09-groundedness-checking.ipynb @@ -35,8 +35,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.8.0" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { diff --git a/python/samples/getting_started/10-multiple-results-per-prompt.ipynb b/python/samples/getting_started/10-multiple-results-per-prompt.ipynb index 14d893a24b8b..c3edd2fd5c45 100644 --- a/python/samples/getting_started/10-multiple-results-per-prompt.ipynb +++ b/python/samples/getting_started/10-multiple-results-per-prompt.ipynb @@ -33,8 +33,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.8.0" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { @@ -501,7 +504,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.10.12" } }, "nbformat": 4, diff --git a/python/samples/getting_started/11-streaming-completions.ipynb b/python/samples/getting_started/11-streaming-completions.ipynb index 25dff07897d1..ca26d21ead5c 100644 --- a/python/samples/getting_started/11-streaming-completions.ipynb +++ b/python/samples/getting_started/11-streaming-completions.ipynb @@ -26,8 +26,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Note: if using a Poetry virtual environment, do not run this cell\n", - "%pip install semantic-kernel==1.8.0" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { diff --git a/python/samples/getting_started/third_party/weaviate-persistent-memory.ipynb b/python/samples/getting_started/third_party/weaviate-persistent-memory.ipynb index 3b7821605e7b..a9b2635e5442 100644 --- a/python/samples/getting_started/third_party/weaviate-persistent-memory.ipynb +++ b/python/samples/getting_started/third_party/weaviate-persistent-memory.ipynb @@ -156,7 +156,11 @@ "metadata": {}, "outputs": [], "source": [ - "%pip install semantic-kernel[weaviate]==1.8.0" + "# Note: if using a virtual environment, do not run this cell\n", + "%pip install -U semantic-kernel[weaviate]\n", + "from semantic_kernel import __version__\n", + "\n", + "__version__" ] }, { diff --git a/python/semantic_kernel/__init__.py b/python/semantic_kernel/__init__.py index 08cd98c223d2..003edae59a41 100644 --- a/python/semantic_kernel/__init__.py +++ b/python/semantic_kernel/__init__.py @@ -2,5 +2,5 @@ from semantic_kernel.kernel import Kernel -__version__ = "1.8.0" +__version__ = "1.8.1" __all__ = ["Kernel", "__version__"] diff --git a/python/semantic_kernel/connectors/ai/anthropic/services/anthropic_chat_completion.py b/python/semantic_kernel/connectors/ai/anthropic/services/anthropic_chat_completion.py index 3b9a7de99182..94c4c77d98ea 100644 --- a/python/semantic_kernel/connectors/ai/anthropic/services/anthropic_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/anthropic/services/anthropic_chat_completion.py @@ -1,8 +1,14 @@ # Copyright (c) Microsoft. All rights reserved. import logging +import sys from collections.abc import AsyncGenerator -from typing import Any +from typing import Any, ClassVar + +if sys.version_info >= (3, 12): + from typing import override # pragma: no cover +else: + from typing_extensions import override # pragma: no cover from anthropic import AsyncAnthropic from anthropic.types import ( @@ -29,11 +35,9 @@ from semantic_kernel.contents.text_content import TextContent from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.contents.utils.finish_reason import FinishReason as SemanticKernelFinishReason -from semantic_kernel.exceptions.service_exceptions import ( - ServiceInitializationError, - ServiceResponseException, -) +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceResponseException from semantic_kernel.utils.experimental_decorator import experimental_class +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import trace_chat_completion # map finish reasons from Anthropic to Semantic Kernel ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP = { @@ -49,8 +53,10 @@ class AnthropicChatCompletion(ChatCompletionClientBase): """Antropic ChatCompletion class.""" + MODEL_PROVIDER_NAME: ClassVar[str] = "anthropic" + async_client: AsyncAnthropic - + def __init__( self, ai_model_id: str | None = None, @@ -68,10 +74,10 @@ def __init__( service_id: Service ID tied to the execution settings. api_key: The optional API key to use. If provided will override, the env vars or .env file value. - async_client: An existing client to use. + async_client: An existing client to use. env_file_path: Use the environment settings file as a fallback - to environment variables. - env_file_encoding: The encoding of the environment settings file. + to environment variables. + env_file_encoding: The encoding of the environment settings file. """ try: anthropic_settings = AnthropicSettings.create( @@ -82,7 +88,7 @@ def __init__( ) except ValidationError as ex: raise ServiceInitializationError("Failed to create Anthropic settings.", ex) from ex - + if not anthropic_settings.chat_model_id: raise ServiceInitializationError("The Anthropic chat model ID is required.") @@ -97,12 +103,14 @@ def __init__( ai_model_id=anthropic_settings.chat_model_id, ) + @override + @trace_chat_completion(MODEL_PROVIDER_NAME) async def get_chat_message_contents( self, chat_history: "ChatHistory", settings: "PromptExecutionSettings", **kwargs: Any, - ) -> list["ChatMessageContent"]: + ) -> list["ChatMessageContent"]: """Executes a chat completion request and returns the result. Args: @@ -127,22 +135,23 @@ async def get_chat_message_contents( raise ServiceResponseException( f"{type(self)} service failed to complete the prompt", ex, - ) from ex - + ) from ex + metadata: dict[str, Any] = {"id": response.id} # Check if usage exists and has a value, then add it to the metadata if hasattr(response, "usage") and response.usage is not None: metadata["usage"] = response.usage - return [self._create_chat_message_content(response, content_block, metadata) - for content_block in response.content] - + return [ + self._create_chat_message_content(response, content_block, metadata) for content_block in response.content + ] + async def get_streaming_chat_message_contents( self, chat_history: ChatHistory, - settings: PromptExecutionSettings, + settings: PromptExecutionSettings, **kwargs: Any, - ) -> AsyncGenerator[list[StreamingChatMessageContent], Any]: + ) -> AsyncGenerator[list[StreamingChatMessageContent], Any]: """Executes a streaming chat completion request and returns the result. Args: @@ -166,17 +175,18 @@ async def get_streaming_chat_message_contents( author_role = None metadata: dict[str, Any] = {"usage": {}, "id": None} content_block_idx = 0 - + async for stream_event in stream: if isinstance(stream_event, RawMessageStartEvent): author_role = stream_event.message.role metadata["usage"]["input_tokens"] = stream_event.message.usage.input_tokens metadata["id"] = stream_event.message.id elif isinstance(stream_event, (RawContentBlockDeltaEvent, RawMessageDeltaEvent)): - yield [self._create_streaming_chat_message_content(stream_event, - content_block_idx, - author_role, - metadata)] + yield [ + self._create_streaming_chat_message_content( + stream_event, content_block_idx, author_role, metadata + ) + ] elif isinstance(stream_event, ContentBlockStopEvent): content_block_idx += 1 @@ -187,21 +197,18 @@ async def get_streaming_chat_message_contents( ) from ex def _create_chat_message_content( - self, - response: Message, - content: TextBlock, - response_metadata: dict[str, Any] + self, response: Message, content: TextBlock, response_metadata: dict[str, Any] ) -> "ChatMessageContent": """Create a chat message content object.""" items: list[ITEM_TYPES] = [] - + if content.text: items.append(TextContent(text=content.text)) finish_reason = None if response.stop_reason: finish_reason = ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP[response.stop_reason] - + return ChatMessageContent( inner_content=response, ai_model_id=self.ai_model_id, @@ -212,20 +219,20 @@ def _create_chat_message_content( ) def _create_streaming_chat_message_content( - self, - stream_event: RawContentBlockDeltaEvent | RawMessageDeltaEvent, - content_block_idx: int, - role: str | None = None, - metadata: dict[str, Any] = {} + self, + stream_event: RawContentBlockDeltaEvent | RawMessageDeltaEvent, + content_block_idx: int, + role: str | None = None, + metadata: dict[str, Any] = {}, ) -> StreamingChatMessageContent: """Create a streaming chat message content object from a choice.""" text_content = "" - + if stream_event.delta and hasattr(stream_event.delta, "text"): text_content = stream_event.delta.text - + items: list[STREAMING_ITEM_TYPES] = [StreamingTextContent(choice_index=content_block_idx, text=text_content)] - + finish_reason = None if isinstance(stream_event, RawMessageDeltaEvent): if stream_event.delta.stop_reason: @@ -246,4 +253,3 @@ def _create_streaming_chat_message_content( def get_prompt_execution_settings_class(self) -> "type[AnthropicChatPromptExecutionSettings]": """Create a request settings object.""" return AnthropicChatPromptExecutionSettings - diff --git a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_base.py b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_base.py index 32550fa71697..3d64c38ce5bc 100644 --- a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_base.py +++ b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_base.py @@ -3,6 +3,7 @@ import asyncio import contextlib from abc import ABC +from typing import ClassVar from azure.ai.inference.aio import ChatCompletionsClient, EmbeddingsClient @@ -14,6 +15,8 @@ class AzureAIInferenceBase(KernelBaseModel, ABC): """Azure AI Inference Chat Completion Service.""" + MODEL_PROVIDER_NAME: ClassVar[str] = "azureai" + client: ChatCompletionsClient | EmbeddingsClient def __del__(self) -> None: diff --git a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py index 92fe5bb2af71..b56562fc8a35 100644 --- a/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/azure_ai_inference/services/azure_ai_inference_chat_completion.py @@ -7,6 +7,7 @@ from functools import reduce from typing import TYPE_CHECKING, Any +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import trace_chat_completion from semantic_kernel.utils.telemetry.user_agent import SEMANTIC_KERNEL_USER_AGENT if sys.version_info >= (3, 12): @@ -119,6 +120,8 @@ def __init__( ) # region Non-streaming + @override + @trace_chat_completion(AzureAIInferenceBase.MODEL_PROVIDER_NAME) async def get_chat_message_contents( self, chat_history: ChatHistory, diff --git a/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_base.py b/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_base.py index 91446835302d..5bbc19568bc1 100644 --- a/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_base.py +++ b/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_base.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. from abc import ABC +from typing import ClassVar from semantic_kernel.connectors.ai.google.google_ai.google_ai_settings import GoogleAISettings from semantic_kernel.kernel_pydantic import KernelBaseModel @@ -9,4 +10,6 @@ class GoogleAIBase(KernelBaseModel, ABC): """Google AI Service.""" + MODEL_PROVIDER_NAME: ClassVar[str] = "googleai" + service_settings: GoogleAISettings diff --git a/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_chat_completion.py b/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_chat_completion.py index 2b65dc298111..c33affe047cb 100644 --- a/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_chat_completion.py @@ -40,6 +40,7 @@ from semantic_kernel.contents.utils.finish_reason import FinishReason from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.kernel import Kernel +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import trace_chat_completion if sys.version_info >= (3, 12): from typing import override # pragma: no cover @@ -109,6 +110,7 @@ def __init__( # region Non-streaming @override + @trace_chat_completion(GoogleAIBase.MODEL_PROVIDER_NAME) async def get_chat_message_contents( self, chat_history: ChatHistory, diff --git a/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_text_completion.py b/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_text_completion.py index a38201db6b67..3590b8b4d51c 100644 --- a/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_text_completion.py +++ b/python/semantic_kernel/connectors/ai/google/google_ai/services/google_ai_text_completion.py @@ -15,6 +15,7 @@ ) from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_base import GoogleAIBase from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import trace_text_completion if sys.version_info >= (3, 12): from typing import override # pragma: no cover @@ -78,6 +79,7 @@ def __init__( # region Non-streaming @override + @trace_text_completion(GoogleAIBase.MODEL_PROVIDER_NAME) async def get_text_contents( self, prompt: str, diff --git a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_base.py b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_base.py index e17b1994424d..29e5d2502b63 100644 --- a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_base.py +++ b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_base.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. from abc import ABC +from typing import ClassVar from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_settings import VertexAISettings from semantic_kernel.kernel_pydantic import KernelBaseModel @@ -9,4 +10,6 @@ class VertexAIBase(KernelBaseModel, ABC): """Vertex AI Service.""" + MODEL_PROVIDER_NAME: ClassVar[str] = "vertexai" + service_settings: VertexAISettings diff --git a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_chat_completion.py b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_chat_completion.py index b2519d1e5edc..53116630b632 100644 --- a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_chat_completion.py @@ -45,6 +45,7 @@ ) from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.kernel import Kernel +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import trace_chat_completion if sys.version_info >= (3, 12): from typing import override # pragma: no cover @@ -103,6 +104,7 @@ def __init__( # region Non-streaming @override + @trace_chat_completion(VertexAIBase.MODEL_PROVIDER_NAME) async def get_chat_message_contents( self, chat_history: ChatHistory, diff --git a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_text_completion.py b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_text_completion.py index 6919b6ba521e..e874ba21f254 100644 --- a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_text_completion.py +++ b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/vertex_ai_text_completion.py @@ -19,6 +19,7 @@ from semantic_kernel.contents.streaming_text_content import StreamingTextContent from semantic_kernel.contents.text_content import TextContent from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import trace_text_completion if sys.version_info >= (3, 12): from typing import override # pragma: no cover @@ -74,6 +75,7 @@ def __init__( # region Non-streaming @override + @trace_text_completion(VertexAIBase.MODEL_PROVIDER_NAME) async def get_text_contents( self, prompt: str, diff --git a/python/semantic_kernel/connectors/ai/mistral_ai/services/mistral_ai_base.py b/python/semantic_kernel/connectors/ai/mistral_ai/services/mistral_ai_base.py new file mode 100644 index 000000000000..0e18409f9e08 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/mistral_ai/services/mistral_ai_base.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft. All rights reserved. + +from abc import ABC +from typing import ClassVar + +from mistralai.async_client import MistralAsyncClient + +from semantic_kernel.kernel_pydantic import KernelBaseModel + + +class MistralAIBase(KernelBaseModel, ABC): + """Mistral AI service base.""" + + MODEL_PROVIDER_NAME: ClassVar[str] = "mistralai" + + async_client: MistralAsyncClient diff --git a/python/semantic_kernel/connectors/ai/mistral_ai/services/mistral_ai_chat_completion.py b/python/semantic_kernel/connectors/ai/mistral_ai/services/mistral_ai_chat_completion.py index ffd6bc2594ad..fc23b451d253 100644 --- a/python/semantic_kernel/connectors/ai/mistral_ai/services/mistral_ai_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/mistral_ai/services/mistral_ai_chat_completion.py @@ -1,9 +1,15 @@ # Copyright (c) Microsoft. All rights reserved. import logging +import sys from collections.abc import AsyncGenerator from typing import Any +if sys.version_info >= (3, 12): + from typing import override # pragma: no cover +else: + from typing_extensions import override # pragma: no cover + from mistralai.async_client import MistralAsyncClient from mistralai.models.chat_completion import ( ChatCompletionResponse, @@ -19,6 +25,7 @@ from semantic_kernel.connectors.ai.mistral_ai.prompt_execution_settings.mistral_ai_prompt_execution_settings import ( MistralAIChatPromptExecutionSettings, ) +from semantic_kernel.connectors.ai.mistral_ai.services.mistral_ai_base import MistralAIBase from semantic_kernel.connectors.ai.mistral_ai.settings.mistral_ai_settings import MistralAISettings from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.contents.chat_history import ChatHistory @@ -29,23 +36,20 @@ from semantic_kernel.contents.text_content import TextContent from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.contents.utils.finish_reason import FinishReason -from semantic_kernel.exceptions.service_exceptions import ( - ServiceInitializationError, - ServiceResponseException, -) +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceResponseException from semantic_kernel.utils.experimental_decorator import experimental_class +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import trace_chat_completion logger: logging.Logger = logging.getLogger(__name__) @experimental_class -class MistralAIChatCompletion(ChatCompletionClientBase): +class MistralAIChatCompletion(MistralAIBase, ChatCompletionClientBase): """Mistral Chat completion class.""" prompt_tokens: int = 0 completion_tokens: int = 0 total_tokens: int = 0 - async_client: MistralAsyncClient def __init__( self, @@ -64,10 +68,10 @@ def __init__( service_id (str | None): Service ID tied to the execution settings. api_key (str | None): The optional API key to use. If provided will override, the env vars or .env file value. - async_client (MistralAsyncClient | None) : An existing client to use. + async_client (MistralAsyncClient | None) : An existing client to use. env_file_path (str | None): Use the environment settings file as a fallback - to environment variables. - env_file_encoding (str | None): The encoding of the environment settings file. + to environment variables. + env_file_encoding (str | None): The encoding of the environment settings file. """ try: mistralai_settings = MistralAISettings.create( @@ -78,7 +82,7 @@ def __init__( ) except ValidationError as ex: raise ServiceInitializationError("Failed to create MistralAI settings.", ex) from ex - + if not mistralai_settings.chat_model_id: raise ServiceInitializationError("The MistralAI chat model ID is required.") @@ -93,12 +97,14 @@ def __init__( ai_model_id=ai_model_id or mistralai_settings.chat_model_id, ) + @override + @trace_chat_completion(MistralAIBase.MODEL_PROVIDER_NAME) async def get_chat_message_contents( self, chat_history: "ChatHistory", settings: "PromptExecutionSettings", **kwargs: Any, - ) -> list["ChatMessageContent"]: + ) -> list["ChatMessageContent"]: """Executes a chat completion request and returns the result. Args: @@ -124,18 +130,18 @@ async def get_chat_message_contents( raise ServiceResponseException( f"{type(self)} service failed to complete the prompt", ex, - ) from ex - + ) from ex + self.store_usage(response) response_metadata = self._get_metadata_from_response(response) return [self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices] - + async def get_streaming_chat_message_contents( self, chat_history: ChatHistory, - settings: PromptExecutionSettings, + settings: PromptExecutionSettings, **kwargs: Any, - ) -> AsyncGenerator[list[StreamingChatMessageContent], Any]: + ) -> AsyncGenerator[list[StreamingChatMessageContent], Any]: """Executes a streaming chat completion request and returns the result. Args: @@ -181,7 +187,7 @@ def _create_chat_message_content( metadata.update(response_metadata) items: list[Any] = self._get_tool_calls_from_chat_choice(choice) - + if choice.message.content: items.append(TextContent(text=choice.message.content)) @@ -220,8 +226,7 @@ def _create_streaming_chat_message_content( ) def _get_metadata_from_response( - self, - response: ChatCompletionResponse | ChatCompletionStreamResponse + self, response: ChatCompletionResponse | ChatCompletionStreamResponse ) -> dict[str, Any]: """Get metadata from a chat response.""" metadata: dict[str, Any] = { @@ -231,27 +236,26 @@ def _get_metadata_from_response( # Check if usage exists and has a value, then add it to the metadata if hasattr(response, "usage") and response.usage is not None: metadata["usage"] = response.usage - + return metadata def _get_metadata_from_chat_choice( - self, - choice: ChatCompletionResponseChoice | ChatCompletionResponseStreamChoice + self, choice: ChatCompletionResponseChoice | ChatCompletionResponseStreamChoice ) -> dict[str, Any]: """Get metadata from a chat choice.""" return { "logprobs": getattr(choice, "logprobs", None), } - - def _get_tool_calls_from_chat_choice(self, - choice: ChatCompletionResponseChoice | ChatCompletionResponseStreamChoice + + def _get_tool_calls_from_chat_choice( + self, choice: ChatCompletionResponseChoice | ChatCompletionResponseStreamChoice ) -> list[FunctionCallContent]: """Get tool calls from a chat choice.""" - content: ChatMessage | DeltaMessage + content: ChatMessage | DeltaMessage content = choice.message if isinstance(choice, ChatCompletionResponseChoice) else choice.delta if content.tool_calls is None: return [] - + return [ FunctionCallContent( id=tool.id, @@ -267,7 +271,7 @@ def _get_tool_calls_from_chat_choice(self, def get_prompt_execution_settings_class(self) -> "type[MistralAIChatPromptExecutionSettings]": """Create a request settings object.""" return MistralAIChatPromptExecutionSettings - + def store_usage(self, response): """Store the usage information from the response.""" if not isinstance(response, AsyncGenerator): diff --git a/python/semantic_kernel/connectors/ai/mistral_ai/services/mistral_ai_text_embedding.py b/python/semantic_kernel/connectors/ai/mistral_ai/services/mistral_ai_text_embedding.py index 24b2905b1587..8bf76e5303b1 100644 --- a/python/semantic_kernel/connectors/ai/mistral_ai/services/mistral_ai_text_embedding.py +++ b/python/semantic_kernel/connectors/ai/mistral_ai/services/mistral_ai_text_embedding.py @@ -6,6 +6,7 @@ from typing import Any, override # pragma: no cover else: from typing_extensions import Any, override # pragma: no cover + import logging from mistralai.async_client import MistralAsyncClient @@ -14,6 +15,7 @@ from pydantic import ValidationError from semantic_kernel.connectors.ai.embeddings.embedding_generator_base import EmbeddingGeneratorBase +from semantic_kernel.connectors.ai.mistral_ai.services.mistral_ai_base import MistralAIBase from semantic_kernel.connectors.ai.mistral_ai.settings.mistral_ai_settings import MistralAISettings from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceResponseException @@ -23,19 +25,17 @@ @experimental_class -class MistralAITextEmbedding(EmbeddingGeneratorBase): +class MistralAITextEmbedding(MistralAIBase, EmbeddingGeneratorBase): """Mistral AI Inference Text Embedding Service.""" - client: MistralAsyncClient - def __init__( self, ai_model_id: str | None = None, api_key: str | None = None, service_id: str | None = None, + async_client: MistralAsyncClient | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, - client: MistralAsyncClient | None = None, ) -> None: """Initialize the Mistral AI Text Embedding service. @@ -45,12 +45,12 @@ def __init__( - MISTRALAI_EMBEDDING_MODEL_ID Args: - ai_model_id: (str | None): A string that is used to identify the model such as the model name. - api_key (str | None): The API key for the Mistral AI service deployment. - service_id (str | None): Service ID for the embedding completion service. - env_file_path (str | None): The path to the environment file. - env_file_encoding (str | None): The encoding of the environment file. - client (MistralAsyncClient | None): The Mistral AI client to use. + ai_model_id: (str | None): A string that is used to identify the model such as the model name. + api_key (str | None): The API key for the Mistral AI service deployment. + service_id (str | None): Service ID for the embedding completion service. + async_client (MistralAsyncClient | None): The Mistral AI client to use. + env_file_path (str | None): The path to the environment file. + env_file_encoding (str | None): The encoding of the environment file. Raises: ServiceInitializationError: If an error occurs during initialization. @@ -68,15 +68,13 @@ def __init__( if not mistralai_settings.embedding_model_id: raise ServiceInitializationError("The MistralAI embedding model ID is required.") - if not client: - client = MistralAsyncClient( - api_key=mistralai_settings.api_key.get_secret_value() - ) + if not async_client: + async_client = MistralAsyncClient(api_key=mistralai_settings.api_key.get_secret_value()) super().__init__( service_id=service_id or mistralai_settings.embedding_model_id, ai_model_id=ai_model_id or mistralai_settings.embedding_model_id, - client=client, + async_client=async_client, ) @override @@ -98,10 +96,8 @@ async def generate_raw_embeddings( ) -> Any: """Generate embeddings from the Mistral AI service.""" try: - - embedding_response: EmbeddingResponse = await self.client.embeddings( - model=self.ai_model_id, - input=texts + embedding_response: EmbeddingResponse = await self.async_client.embeddings( + model=self.ai_model_id, input=texts ) except Exception as ex: raise ServiceResponseException( diff --git a/python/semantic_kernel/connectors/ai/ollama/ollama_prompt_execution_settings.py b/python/semantic_kernel/connectors/ai/ollama/ollama_prompt_execution_settings.py index 7e365bea3d5c..6ff69be7dc12 100644 --- a/python/semantic_kernel/connectors/ai/ollama/ollama_prompt_execution_settings.py +++ b/python/semantic_kernel/connectors/ai/ollama/ollama_prompt_execution_settings.py @@ -11,6 +11,9 @@ class OllamaPromptExecutionSettings(PromptExecutionSettings): format: Literal["json"] | None = None options: dict[str, Any] | None = None + # TODO(@taochen): Add individual properties for execution settings and + # convert them to the appropriate types in the options dictionary. + class OllamaTextPromptExecutionSettings(OllamaPromptExecutionSettings): """Settings for Ollama text prompt execution.""" diff --git a/python/semantic_kernel/connectors/ai/ollama/services/ollama_base.py b/python/semantic_kernel/connectors/ai/ollama/services/ollama_base.py index ceffb48d9dbf..f03ad0e994d1 100644 --- a/python/semantic_kernel/connectors/ai/ollama/services/ollama_base.py +++ b/python/semantic_kernel/connectors/ai/ollama/services/ollama_base.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. from abc import ABC +from typing import ClassVar from ollama import AsyncClient @@ -14,4 +15,6 @@ class OllamaBase(KernelBaseModel, ABC): client [AsyncClient]: An Ollama client to use for the service. """ + MODEL_PROVIDER_NAME: ClassVar[str] = "ollama" + client: AsyncClient diff --git a/python/semantic_kernel/connectors/ai/ollama/services/ollama_chat_completion.py b/python/semantic_kernel/connectors/ai/ollama/services/ollama_chat_completion.py index 1c3ffe3080b7..2e9adb09fddb 100644 --- a/python/semantic_kernel/connectors/ai/ollama/services/ollama_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/ollama/services/ollama_chat_completion.py @@ -25,6 +25,7 @@ from semantic_kernel.contents.streaming_text_content import StreamingTextContent from semantic_kernel.contents.text_content import TextContent from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceInvalidResponseError +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import trace_chat_completion, trace_text_completion if TYPE_CHECKING: from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings @@ -74,6 +75,8 @@ def __init__( client=client or AsyncClient(host=ollama_settings.host), ) + @override + @trace_chat_completion(OllamaBase.MODEL_PROVIDER_NAME) async def get_chat_message_contents( self, chat_history: ChatHistory, @@ -162,6 +165,8 @@ async def get_streaming_chat_message_contents( ) ] + @override + @trace_text_completion(OllamaBase.MODEL_PROVIDER_NAME) async def get_text_contents( self, prompt: str, diff --git a/python/semantic_kernel/connectors/ai/ollama/services/ollama_text_completion.py b/python/semantic_kernel/connectors/ai/ollama/services/ollama_text_completion.py index 351c4e768fea..e02f98723d96 100644 --- a/python/semantic_kernel/connectors/ai/ollama/services/ollama_text_completion.py +++ b/python/semantic_kernel/connectors/ai/ollama/services/ollama_text_completion.py @@ -20,6 +20,7 @@ from semantic_kernel.contents.streaming_text_content import StreamingTextContent from semantic_kernel.contents.text_content import TextContent from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceInvalidResponseError +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import trace_text_completion if TYPE_CHECKING: from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings @@ -69,6 +70,8 @@ def __init__( client=client or AsyncClient(host=ollama_settings.host), ) + @override + @trace_text_completion(OllamaBase.MODEL_PROVIDER_NAME) async def get_text_contents( self, prompt: str, diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py index e23cd9799e61..786be4efb996 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py @@ -43,7 +43,7 @@ from semantic_kernel.filters.auto_function_invocation.auto_function_invocation_context import ( AutoFunctionInvocationContext, ) -from semantic_kernel.utils.telemetry.decorators import trace_chat_completion +from semantic_kernel.utils.telemetry.model_diagnostics import trace_chat_completion if TYPE_CHECKING: from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings diff --git a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py index fbcb90767e46..40b445cce480 100644 --- a/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py +++ b/python/semantic_kernel/connectors/ai/open_ai/services/open_ai_text_completion_base.py @@ -26,7 +26,7 @@ from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase from semantic_kernel.contents.streaming_text_content import StreamingTextContent from semantic_kernel.contents.text_content import TextContent -from semantic_kernel.utils.telemetry.decorators import trace_text_completion +from semantic_kernel.utils.telemetry.model_diagnostics import trace_text_completion if TYPE_CHECKING: from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings diff --git a/python/semantic_kernel/utils/telemetry/decorators.py b/python/semantic_kernel/utils/telemetry/decorators.py deleted file mode 100644 index 366168ae3938..000000000000 --- a/python/semantic_kernel/utils/telemetry/decorators.py +++ /dev/null @@ -1,265 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. -# -# Code to trace model activities with the OTel semantic conventions. -# This code contains experimental features and may change in the future. -# To enable these features, set one of the following senvironment variables to true: -# SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS -# SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE - -import functools -import json -import os -from collections.abc import Callable -from typing import Any - -from opentelemetry.trace import Span, StatusCode, get_tracer, use_span - -from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings -from semantic_kernel.contents.chat_history import ChatHistory -from semantic_kernel.contents.chat_message_content import ChatMessageContent -from semantic_kernel.contents.text_content import TextContent -from semantic_kernel.utils.telemetry.const import ( - CHAT_COMPLETION_OPERATION, - COMPLETION_EVENT, - COMPLETION_EVENT_COMPLETION, - COMPLETION_TOKENS, - ERROR_TYPE, - FINISH_REASON, - MAX_TOKENS, - MODEL, - OPERATION, - PROMPT_EVENT, - PROMPT_EVENT_PROMPT, - PROMPT_TOKENS, - RESPONSE_ID, - SYSTEM, - TEMPERATURE, - TEXT_COMPLETION_OPERATION, - TOP_P, -) - -OTEL_ENABLED_ENV_VAR = "SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS" -OTEL_SENSITIVE_ENABLED_ENV_VAR = "SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE" - - -_enable_diagnostics = os.getenv(OTEL_ENABLED_ENV_VAR, "false").lower() in ("true", "1", "t") -_enable_sensitive_events = os.getenv(OTEL_SENSITIVE_ENABLED_ENV_VAR, "false").lower() in ("true", "1", "t") - -# Creates a tracer from the global tracer provider -tracer = get_tracer(__name__) - - -def are_model_diagnostics_enabled() -> bool: - """Check if model diagnostics are enabled. - - Model diagnostics are enabled if either _enable_diagnostics or _enable_sensitive_events is set. - """ - return _enable_diagnostics or _enable_sensitive_events - - -def are_sensitive_events_enabled() -> bool: - """Check if sensitive events are enabled. - - Sensitive events are enabled if _enable_sensitive_events is set. - """ - return _enable_sensitive_events - - -def trace_chat_completion(model_provider: str) -> Callable: - """Decorator to trace chat completion activities.""" - - def inner_trace_chat_completion(completion_func: Callable) -> Callable: - @functools.wraps(completion_func) - async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[ChatMessageContent]: - chat_history: ChatHistory = kwargs["chat_history"] - settings: PromptExecutionSettings = kwargs["settings"] - - model_name = getattr(settings, "ai_model_id", None) or getattr(args[0], "ai_model_id", None) or "unknown" - - formatted_messages = ( - _messages_to_openai_format(chat_history.messages) if are_sensitive_events_enabled() else None - ) - span = _start_completion_activity( - CHAT_COMPLETION_OPERATION, model_name, model_provider, formatted_messages, settings - ) - - try: - completions: list[ChatMessageContent] = await completion_func(*args, **kwargs) - except Exception as exception: - if span: - _set_completion_error(span, exception) - span.end() - raise - - if span and completions: - with use_span(span, end_on_exit=True): - first_completion = completions[0] - response_id = first_completion.metadata.get("id") or (first_completion.inner_content or {}).get( - "id" - ) - usage = first_completion.metadata.get("usage", None) - prompt_tokens = getattr(usage, "prompt_tokens", None) - completion_tokens = getattr(usage, "completion_tokens", None) - - completion_text: str | None = ( - _messages_to_openai_format(completions) if are_sensitive_events_enabled() else None - ) - - finish_reasons: list[str] = [str(completion.finish_reason) for completion in completions] - - _set_completion_response( - span, - completion_text, - finish_reasons, - response_id or "unknown", - prompt_tokens, - completion_tokens, - ) - - return completions - - return wrapper_decorator - - return inner_trace_chat_completion - - -def trace_text_completion(model_provider: str) -> Callable: - """Decorator to trace text completion activities.""" - - def inner_trace_text_completion(completion_func: Callable) -> Callable: - @functools.wraps(completion_func) - async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[TextContent]: - prompt: str = kwargs["prompt"] - settings: PromptExecutionSettings = kwargs["settings"] - - model_name = getattr(settings, "ai_model_id", None) or getattr(args[0], "ai_model_id", None) or "unknown" - - span = _start_completion_activity(TEXT_COMPLETION_OPERATION, model_name, model_provider, prompt, settings) - - try: - completions: list[TextContent] = await completion_func(*args, **kwargs) - except Exception as exception: - if span: - _set_completion_error(span, exception) - span.end() - raise - - if span and completions: - with use_span(span, end_on_exit=True): - first_completion = completions[0] - response_id = first_completion.metadata.get("id") or (first_completion.inner_content or {}).get( - "id" - ) - usage = first_completion.metadata.get("usage", None) - prompt_tokens = getattr(usage, "prompt_tokens", None) - completion_tokens = getattr(usage, "completion_tokens", None) - - completion_text: str | None = ( - json.dumps([completion.text for completion in completions]) - if are_sensitive_events_enabled() - else None - ) - - _set_completion_response( - span, - completion_text, - None, - response_id or "unknown", - prompt_tokens, - completion_tokens, - ) - - return completions - - return wrapper_decorator - - return inner_trace_text_completion - - -def _start_completion_activity( - operation_name: str, - model_name: str, - model_provider: str, - prompt: str | None, - execution_settings: PromptExecutionSettings | None, -) -> Span | None: - """Start a text or chat completion activity for a given model.""" - if not are_model_diagnostics_enabled(): - return None - - span = tracer.start_span(f"{operation_name} {model_name}") - - # Set attributes on the span - span.set_attributes( - { - OPERATION: operation_name, - SYSTEM: model_provider, - MODEL: model_name, - } - ) - - # TODO(@glahaye): we'll need to have a way to get these attributes from model - # providers other than OpenAI (for example if the attributes are named differently) - if execution_settings: - attribute = execution_settings.extension_data.get("max_tokens") - if attribute: - span.set_attribute(MAX_TOKENS, attribute) - - attribute = execution_settings.extension_data.get("temperature") - if attribute: - span.set_attribute(TEMPERATURE, attribute) - - attribute = execution_settings.extension_data.get("top_p") - if attribute: - span.set_attribute(TOP_P, attribute) - - if are_sensitive_events_enabled() and prompt: - span.add_event(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: prompt}) - - return span - - -def _set_completion_response( - span: Span, - completion_text: str | None, - finish_reasons: list[str] | None, - response_id: str, - prompt_tokens: int | None = None, - completion_tokens: int | None = None, -) -> None: - """Set the a text or chat completion response for a given activity.""" - if not are_model_diagnostics_enabled(): - return - - span.set_attribute(RESPONSE_ID, response_id) - - if finish_reasons: - span.set_attribute(FINISH_REASON, ",".join(finish_reasons)) - - if prompt_tokens: - span.set_attribute(PROMPT_TOKENS, prompt_tokens) - - if completion_tokens: - span.set_attribute(COMPLETION_TOKENS, completion_tokens) - - if are_sensitive_events_enabled() and completion_text: - span.add_event(COMPLETION_EVENT, {COMPLETION_EVENT_COMPLETION: completion_text}) - - -def _set_completion_error(span: Span, error: Exception) -> None: - """Set an error for a text or chat completion .""" - if not are_model_diagnostics_enabled(): - return - - span.set_attribute(ERROR_TYPE, str(type(error))) - - span.set_status(StatusCode.ERROR, repr(error)) - - -def _messages_to_openai_format(messages: list[ChatMessageContent]) -> str: - """Convert a list of ChatMessageContent to a string in the OpenAI format. - - OpenTelemetry recommends formatting the messages in the OpenAI format - regardless of the actual model being used. - """ - return json.dumps([message.to_dict() for message in messages]) diff --git a/python/semantic_kernel/utils/telemetry/model_diagnostics/__init__.py b/python/semantic_kernel/utils/telemetry/model_diagnostics/__init__.py new file mode 100644 index 000000000000..c873a5770a80 --- /dev/null +++ b/python/semantic_kernel/utils/telemetry/model_diagnostics/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import trace_chat_completion, trace_text_completion + +__all__ = [ + "trace_chat_completion", + "trace_text_completion", +] diff --git a/python/semantic_kernel/utils/telemetry/model_diagnostics/decorators.py b/python/semantic_kernel/utils/telemetry/model_diagnostics/decorators.py new file mode 100644 index 000000000000..b3dd0faf5f82 --- /dev/null +++ b/python/semantic_kernel/utils/telemetry/model_diagnostics/decorators.py @@ -0,0 +1,232 @@ +# Copyright (c) Microsoft. All rights reserved. + +import functools +import json +from collections.abc import Callable +from typing import Any + +from opentelemetry.trace import Span, StatusCode, get_tracer, use_span + +from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase +from semantic_kernel.contents.chat_history import ChatHistory +from semantic_kernel.contents.chat_message_content import ChatMessageContent +from semantic_kernel.contents.text_content import TextContent +from semantic_kernel.utils.experimental_decorator import experimental_function +from semantic_kernel.utils.telemetry.model_diagnostics import gen_ai_attributes +from semantic_kernel.utils.telemetry.model_diagnostics.model_diagnostics_settings import ModelDiagnosticSettings + +# Module to instrument GenAI models using OpenTelemetry and OpenTelemetry Semantic Conventions. +# These are experimental features and may change in the future. + +# To enable these features, set one of the following environment variables to true: +# SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS +# SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE +MODEL_DIAGNOSTICS_SETTINGS = ModelDiagnosticSettings.create() + +# Operation names +CHAT_COMPLETION_OPERATION = "chat.completions" +TEXT_COMPLETION_OPERATION = "text.completions" + +# Creates a tracer from the global tracer provider +tracer = get_tracer(__name__) + + +@experimental_function +def are_model_diagnostics_enabled() -> bool: + """Check if model diagnostics are enabled. + + Model diagnostics are enabled if either diagnostic is enabled or diagnostic with sensitive events is enabled. + """ + return ( + MODEL_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics + or MODEL_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics_sensitive + ) + + +@experimental_function +def are_sensitive_events_enabled() -> bool: + """Check if sensitive events are enabled. + + Sensitive events are enabled if the diagnostic with sensitive events is enabled. + """ + return MODEL_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics_sensitive + + +@experimental_function +def trace_chat_completion(model_provider: str) -> Callable: + """Decorator to trace chat completion activities. + + Args: + model_provider (str): The model provider should describe a family of + GenAI models with specific model identified by ai_model_id. For example, + model_provider could be "openai" and ai_model_id could be "gpt-3.5-turbo". + Sometimes the model provider is unknown at runtime, in which case it can be + set to the most specific known provider. For example, while using local models + hosted by Ollama, the model provider could be set to "ollama". + """ + + def inner_trace_chat_completion(completion_func: Callable) -> Callable: + @functools.wraps(completion_func) + async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[ChatMessageContent]: + if not are_model_diagnostics_enabled(): + # If model diagnostics are not enabled, just return the completion + return await completion_func(*args, **kwargs) + + completion_service: ChatCompletionClientBase = args[0] + chat_history: ChatHistory = kwargs["chat_history"] + settings: PromptExecutionSettings = kwargs["settings"] + + with use_span( + _start_completion_activity( + CHAT_COMPLETION_OPERATION, + completion_service.ai_model_id, + model_provider, + chat_history, + settings, + ), + end_on_exit=True, + ) as current_span: + try: + completions: list[ChatMessageContent] = await completion_func(*args, **kwargs) + _set_completion_response(current_span, completions) + return completions + except Exception as exception: + _set_completion_error(current_span, exception) + raise + + return wrapper_decorator + + return inner_trace_chat_completion + + +@experimental_function +def trace_text_completion(model_provider: str) -> Callable: + """Decorator to trace text completion activities.""" + + def inner_trace_text_completion(completion_func: Callable) -> Callable: + @functools.wraps(completion_func) + async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[TextContent]: + if not are_model_diagnostics_enabled(): + # If model diagnostics are not enabled, just return the completion + return await completion_func(*args, **kwargs) + + completion_service: TextCompletionClientBase = args[0] + prompt: str = kwargs["prompt"] + settings: PromptExecutionSettings = kwargs["settings"] + + with use_span( + _start_completion_activity( + TEXT_COMPLETION_OPERATION, + completion_service.ai_model_id, + model_provider, + prompt, + settings, + ), + end_on_exit=True, + ) as current_span: + try: + completions: list[TextContent] = await completion_func(*args, **kwargs) + _set_completion_response(current_span, completions) + return completions + except Exception as exception: + _set_completion_error(current_span, exception) + raise + + return wrapper_decorator + + return inner_trace_text_completion + + +def _start_completion_activity( + operation_name: str, + model_name: str, + model_provider: str, + prompt: str | ChatHistory, + execution_settings: PromptExecutionSettings | None, +) -> Span: + """Start a text or chat completion activity for a given model.""" + span = tracer.start_span(f"{operation_name} {model_name}") + + # Set attributes on the span + span.set_attributes({ + gen_ai_attributes.OPERATION: operation_name, + gen_ai_attributes.SYSTEM: model_provider, + gen_ai_attributes.MODEL: model_name, + }) + + # TODO(@glahaye): we'll need to have a way to get these attributes from model + # providers other than OpenAI (for example if the attributes are named differently) + if execution_settings: + attribute = execution_settings.extension_data.get("max_tokens") + if attribute: + span.set_attribute(gen_ai_attributes.MAX_TOKENS, attribute) + + attribute = execution_settings.extension_data.get("temperature") + if attribute: + span.set_attribute(gen_ai_attributes.TEMPERATURE, attribute) + + attribute = execution_settings.extension_data.get("top_p") + if attribute: + span.set_attribute(gen_ai_attributes.TOP_P, attribute) + + if are_sensitive_events_enabled(): + if isinstance(prompt, ChatHistory): + prompt = _messages_to_openai_format(prompt.messages) + span.add_event(gen_ai_attributes.PROMPT_EVENT, {gen_ai_attributes.PROMPT_EVENT_PROMPT: prompt}) + + return span + + +def _set_completion_response( + current_span: Span, + completions: list[ChatMessageContent] | list[TextContent], +) -> None: + """Set the a text or chat completion response for a given activity.""" + first_completion = completions[0] + + # Set the response ID + response_id = first_completion.metadata.get("id") or (first_completion.inner_content or {}).get("id") + if response_id: + current_span.set_attribute(gen_ai_attributes.RESPONSE_ID, response_id) + + # Set the finish reason + finish_reasons = [ + str(completion.finish_reason) for completion in completions if isinstance(completion, ChatMessageContent) + ] + if finish_reasons: + current_span.set_attribute(gen_ai_attributes.FINISH_REASON, ",".join(finish_reasons)) + + # Set usage attributes + usage = first_completion.metadata.get("usage", None) + + prompt_tokens = getattr(usage, "prompt_tokens", None) + if prompt_tokens: + current_span.set_attribute(gen_ai_attributes.PROMPT_TOKENS, prompt_tokens) + + completion_tokens = getattr(usage, "completion_tokens", None) + if completion_tokens: + current_span.set_attribute(gen_ai_attributes.COMPLETION_TOKENS, completion_tokens) + + # Set the completion event + if are_sensitive_events_enabled(): + completion_text: str = _messages_to_openai_format(completions) + current_span.add_event( + gen_ai_attributes.COMPLETION_EVENT, {gen_ai_attributes.COMPLETION_EVENT_COMPLETION: completion_text} + ) + + +def _set_completion_error(span: Span, error: Exception) -> None: + """Set an error for a text or chat completion .""" + span.set_attribute(gen_ai_attributes.ERROR_TYPE, str(type(error))) + span.set_status(StatusCode.ERROR, repr(error)) + + +def _messages_to_openai_format(messages: list[ChatMessageContent] | list[TextContent]) -> str: + """Convert a list of ChatMessageContent to a string in the OpenAI format. + + OpenTelemetry recommends formatting the messages in the OpenAI format + regardless of the actual model being used. + """ + return json.dumps([message.to_dict() for message in messages]) diff --git a/python/semantic_kernel/utils/telemetry/const.py b/python/semantic_kernel/utils/telemetry/model_diagnostics/gen_ai_attributes.py similarity index 83% rename from python/semantic_kernel/utils/telemetry/const.py rename to python/semantic_kernel/utils/telemetry/model_diagnostics/gen_ai_attributes.py index 5c74f708b986..cca37908e466 100644 --- a/python/semantic_kernel/utils/telemetry/const.py +++ b/python/semantic_kernel/utils/telemetry/model_diagnostics/gen_ai_attributes.py @@ -1,12 +1,13 @@ # Copyright (c) Microsoft. All rights reserved. -# + # Constants for tracing activities with semantic conventions. +# Ideally, we should use the attributes from the semcov package. +# However, many of the attributes are not yet available in the package, +# so we define them here for now. # Activity tags SYSTEM = "gen_ai.system" OPERATION = "gen_ai.operation.name" -CHAT_COMPLETION_OPERATION = "chat.completions" -TEXT_COMPLETION_OPERATION = "text.completions" MODEL = "gen_ai.request.model" MAX_TOKENS = "gen_ai.request.max_tokens" # nosec TEMPERATURE = "gen_ai.request.temperature" diff --git a/python/semantic_kernel/utils/telemetry/model_diagnostics/model_diagnostics_settings.py b/python/semantic_kernel/utils/telemetry/model_diagnostics/model_diagnostics_settings.py new file mode 100644 index 000000000000..f7e509a21b26 --- /dev/null +++ b/python/semantic_kernel/utils/telemetry/model_diagnostics/model_diagnostics_settings.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import ClassVar + +from semantic_kernel.kernel_pydantic import KernelBaseSettings +from semantic_kernel.utils.experimental_decorator import experimental_class + + +@experimental_class +class ModelDiagnosticSettings(KernelBaseSettings): + """Settings for model diagnostics. + + The settings are first loaded from environment variables with + the prefix 'AZURE_AI_INFERENCE_'. + If the environment variables are not found, the settings can + be loaded from a .env file with the encoding 'utf-8'. + If the settings are not found in the .env file, the settings + are ignored; however, validation will fail alerting that the + settings are missing. + + Required settings for prefix 'SEMANTICKERNEL_EXPERIMENTAL_GENAI_' are: + - enable_otel_diagnostics: bool - Enable OpenTelemetry diagnostics. Default is False. + (Env var SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS) + - enable_otel_diagnostics_sensitive: bool - Enable OpenTelemetry sensitive events. Default is False. + (Env var SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE) + """ + + env_prefix: ClassVar[str] = "SEMANTICKERNEL_EXPERIMENTAL_GENAI_" + + enable_otel_diagnostics: bool = False + enable_otel_diagnostics_sensitive: bool = False diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 692c9c759ab1..3d8d263e7a45 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -161,6 +161,11 @@ def chat_history() -> "ChatHistory": return ChatHistory() +@fixture(scope="function") +def prompt() -> str: + return "test prompt" + + # @fixture(autouse=True) # def enable_debug_mode(): # """Set `autouse=True` to enable easy debugging for tests. @@ -306,10 +311,7 @@ def anthropic_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): if override_env_param_dict is None: override_env_param_dict = {} - env_vars = { - "ANTHROPIC_CHAT_MODEL_ID": "test_chat_model_id", - "ANTHROPIC_API_KEY": "test_api_key" - } + env_vars = {"ANTHROPIC_CHAT_MODEL_ID": "test_chat_model_id", "ANTHROPIC_API_KEY": "test_api_key"} env_vars.update(override_env_param_dict) diff --git a/python/tests/unit/connectors/mistral_ai/services/test_mistralai_text_embeddings.py b/python/tests/unit/connectors/mistral_ai/services/test_mistralai_text_embeddings.py index 98550ca6f1ad..61c960b4810f 100644 --- a/python/tests/unit/connectors/mistral_ai/services/test_mistralai_text_embeddings.py +++ b/python/tests/unit/connectors/mistral_ai/services/test_mistralai_text_embeddings.py @@ -13,7 +13,7 @@ def test_embedding_with_env_variables(mistralai_unit_test_env): text_embedding = MistralAITextEmbedding() assert text_embedding.ai_model_id == "test_embedding_model_id" - assert text_embedding.client._api_key == "test_api_key" + assert text_embedding.async_client._api_key == "test_api_key" @pytest.mark.parametrize("exclude_list", [["MISTRALAI_API_KEY", "MISTRALAI_EMBEDDING_MODEL_ID"]], indirect=True) @@ -23,33 +23,33 @@ def test_embedding_with_constructor(mistralai_unit_test_env): ai_model_id="overwrite-model", ) assert text_embedding.ai_model_id == "overwrite-model" - assert text_embedding.client._api_key == "overwrite-api-key" + assert text_embedding.async_client._api_key == "overwrite-api-key" def test_embedding_with_client(mistralai_unit_test_env): client = MagicMock(spec=MistralAsyncClient) - text_embedding = MistralAITextEmbedding(client=client) - assert text_embedding.client == client + text_embedding = MistralAITextEmbedding(async_client=client) + assert text_embedding.async_client == client assert text_embedding.ai_model_id == "test_embedding_model_id" def test_embedding_with_api_key(mistralai_unit_test_env): text_embedding = MistralAITextEmbedding(api_key="overwrite-api-key") - assert text_embedding.client._api_key == "overwrite-api-key" + assert text_embedding.async_client._api_key == "overwrite-api-key" assert text_embedding.ai_model_id == "test_embedding_model_id" def test_embedding_with_model(mistralai_unit_test_env): text_embedding = MistralAITextEmbedding(ai_model_id="overwrite-model") assert text_embedding.ai_model_id == "overwrite-model" - assert text_embedding.client._api_key == "test_api_key" + assert text_embedding.async_client._api_key == "test_api_key" -@pytest.mark.parametrize("exclude_list", [["MISTRALAI_EMBEDDING_MODEL_ID"]], indirect=True) +@pytest.mark.parametrize("exclude_list", [["MISTRALAI_EMBEDDING_MODEL_ID"]], indirect=True) def test_embedding_with_model_without_env(mistralai_unit_test_env): text_embedding = MistralAITextEmbedding(ai_model_id="overwrite-model") assert text_embedding.ai_model_id == "overwrite-model" - assert text_embedding.client._api_key == "test_api_key" + assert text_embedding.async_client._api_key == "test_api_key" @pytest.mark.parametrize("exclude_list", [["MISTRALAI_EMBEDDING_MODEL_ID"]], indirect=True) @@ -90,7 +90,7 @@ async def test_embedding_generate_raw_embedding(mistralai_unit_test_env): mock_client = AsyncMock(spec=MistralAsyncClient) mock_embedding_response = MagicMock(spec=EmbeddingResponse, data=[MagicMock(embedding=[1, 2, 3, 4, 5])]) mock_client.embeddings.return_value = mock_embedding_response - text_embedding = MistralAITextEmbedding(client=mock_client) + text_embedding = MistralAITextEmbedding(async_client=mock_client) embedding = await text_embedding.generate_raw_embeddings(["test"]) assert embedding == [[1, 2, 3, 4, 5]] @@ -100,7 +100,7 @@ async def test_embedding_generate_embedding(mistralai_unit_test_env): mock_client = AsyncMock(spec=MistralAsyncClient) mock_embedding_response = MagicMock(spec=EmbeddingResponse, data=[MagicMock(embedding=[1, 2, 3, 4, 5])]) mock_client.embeddings.return_value = mock_embedding_response - text_embedding = MistralAITextEmbedding(client=mock_client) + text_embedding = MistralAITextEmbedding(async_client=mock_client) embedding = await text_embedding.generate_embeddings(["test"]) assert embedding.tolist() == [[1, 2, 3, 4, 5]] @@ -109,6 +109,6 @@ async def test_embedding_generate_embedding(mistralai_unit_test_env): async def test_embedding_generate_embedding_exception(mistralai_unit_test_env): mock_client = AsyncMock(spec=MistralAsyncClient) mock_client.embeddings.side_effect = Exception("Test Exception") - text_embedding = MistralAITextEmbedding(client=mock_client) + text_embedding = MistralAITextEmbedding(async_client=mock_client) with pytest.raises(ServiceResponseException): await text_embedding.generate_embeddings(["test"]) diff --git a/python/tests/unit/utils/model_diagnostics/conftest.py b/python/tests/unit/utils/model_diagnostics/conftest.py new file mode 100644 index 000000000000..ab7528af2bad --- /dev/null +++ b/python/tests/unit/utils/model_diagnostics/conftest.py @@ -0,0 +1,91 @@ +# Copyright (c) Microsoft. All rights reserved. + + +import sys +from collections.abc import AsyncGenerator +from typing import Any, ClassVar + +import pytest + +if sys.version_info >= (3, 12): + from typing import override # pragma: no cover +else: + from typing_extensions import override # pragma: no cover + +import semantic_kernel +from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase +from semantic_kernel.contents.chat_history import ChatHistory +from semantic_kernel.contents.chat_message_content import ChatMessageContent +from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent +from semantic_kernel.contents.streaming_text_content import StreamingTextContent +from semantic_kernel.contents.text_content import TextContent +from semantic_kernel.utils.telemetry.model_diagnostics.model_diagnostics_settings import ModelDiagnosticSettings + + +@pytest.fixture() +def model_diagnostics_unit_test_env(monkeypatch): + """Fixture to set environment variables for Model Diagnostics Unit Tests.""" + env_vars = { + "SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS": "true", + "SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE": "true", + } + + for key, value in env_vars.items(): + monkeypatch.setenv(key, value) + + # Need to reload the settings to pick up the new environment variables since the + # settings are loaded at import time and this fixture is called after the import + semantic_kernel.utils.telemetry.model_diagnostics.decorators.MODEL_DIAGNOSTICS_SETTINGS = ( + ModelDiagnosticSettings.create() + ) + + +@pytest.fixture() +def service_env_vars(monkeypatch, request): + """Fixture to set environment variables for AI Service Unit Tests.""" + for key, value in request.param.items(): + monkeypatch.setenv(key, value) + + +class MockChatCompletion(ChatCompletionClientBase): + MODEL_PROVIDER_NAME: ClassVar[str] = "mock" + + @override + async def get_chat_message_contents( + self, + chat_history: "ChatHistory", + settings: "PromptExecutionSettings", + **kwargs: Any, + ) -> list["ChatMessageContent"]: + return [] + + @override + async def get_streaming_chat_message_contents( + self, + chat_history: "ChatHistory", + settings: "PromptExecutionSettings", + **kwargs: Any, + ) -> AsyncGenerator[list["StreamingChatMessageContent"], Any]: + yield [] + + +class MockTextCompletion(TextCompletionClientBase): + MODEL_PROVIDER_NAME: ClassVar[str] = "mock" + + @override + async def get_text_contents( + self, + prompt: str, + settings: "PromptExecutionSettings", + ) -> list["TextContent"]: + return [] + + @override + async def get_streaming_text_contents( + self, + prompt: str, + settings: "PromptExecutionSettings", + ) -> AsyncGenerator[list["StreamingTextContent"], Any]: + yield [] diff --git a/python/tests/unit/utils/model_diagnostics/test_trace_chat_completion.py b/python/tests/unit/utils/model_diagnostics/test_trace_chat_completion.py new file mode 100644 index 000000000000..95de327818e7 --- /dev/null +++ b/python/tests/unit/utils/model_diagnostics/test_trace_chat_completion.py @@ -0,0 +1,172 @@ +# Copyright (c) Microsoft. All rights reserved. + +from unittest.mock import patch + +import pytest +from opentelemetry.trace import StatusCode + +from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.contents.chat_message_content import ChatMessageContent +from semantic_kernel.contents.utils.author_role import AuthorRole +from semantic_kernel.contents.utils.finish_reason import FinishReason +from semantic_kernel.exceptions.service_exceptions import ServiceResponseException +from semantic_kernel.utils.telemetry.model_diagnostics import gen_ai_attributes +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import ( + CHAT_COMPLETION_OPERATION, + _messages_to_openai_format, + trace_chat_completion, +) +from tests.unit.utils.model_diagnostics.conftest import MockChatCompletion + +pytestmark = pytest.mark.parametrize( + "execution_settings, mock_response", + [ + pytest.param( + PromptExecutionSettings( + extension_data={ + "max_tokens": 1000, + "temperature": 0.5, + "top_p": 0.9, + } + ), + [ + ChatMessageContent( + role=AuthorRole.ASSISTANT, + ai_model_id="ai_model_id", + content="Test content", + metadata={"id": "test_id"}, + finish_reason=FinishReason.STOP, + ) + ], + id="test_execution_settings_with_extension_data", + ), + pytest.param( + PromptExecutionSettings(), + [ + ChatMessageContent( + role=AuthorRole.ASSISTANT, + ai_model_id="ai_model_id", + metadata={"id": "test_id"}, + finish_reason=FinishReason.STOP, + ) + ], + id="test_execution_settings_no_extension_data", + ), + pytest.param( + PromptExecutionSettings(), + [ + ChatMessageContent( + role=AuthorRole.ASSISTANT, + ai_model_id="ai_model_id", + metadata={}, + finish_reason=FinishReason.STOP, + ) + ], + id="test_chat_message_content_no_metadata", + ), + pytest.param( + PromptExecutionSettings(), + [ + ChatMessageContent( + role=AuthorRole.ASSISTANT, + ai_model_id="ai_model_id", + metadata={"id": "test_id"}, + ) + ], + id="test_chat_message_content_no_finish_reason", + ), + ], +) + + +@pytest.mark.asyncio +@patch("opentelemetry.trace.INVALID_SPAN") # When no tracer provider is available, the span will be an INVALID_SPAN +async def test_trace_chat_completion( + mock_span, + execution_settings, + mock_response, + chat_history, + model_diagnostics_unit_test_env, +): + # Setup + chat_completion: ChatCompletionClientBase = MockChatCompletion(ai_model_id="ai_model_id") + + with patch.object(MockChatCompletion, "get_chat_message_contents", return_value=mock_response): + # We need to reapply the decorator to the method since the mock will not have the decorator applied + MockChatCompletion.get_chat_message_contents = trace_chat_completion(MockChatCompletion.MODEL_PROVIDER_NAME)( + chat_completion.get_chat_message_contents + ) + + results: list[ChatMessageContent] = await chat_completion.get_chat_message_contents( + chat_history=chat_history, settings=execution_settings + ) + + assert results == mock_response + + # Before the call to the model + mock_span.set_attributes.assert_called_with({ + gen_ai_attributes.OPERATION: CHAT_COMPLETION_OPERATION, + gen_ai_attributes.SYSTEM: MockChatCompletion.MODEL_PROVIDER_NAME, + gen_ai_attributes.MODEL: chat_completion.ai_model_id, + }) + + # No all connectors take the same parameters + if execution_settings.extension_data.get("max_tokens") is not None: + mock_span.set_attribute.assert_any_call( + gen_ai_attributes.MAX_TOKENS, execution_settings.extension_data["max_tokens"] + ) + if execution_settings.extension_data.get("temperature") is not None: + mock_span.set_attribute.assert_any_call( + gen_ai_attributes.TEMPERATURE, execution_settings.extension_data["temperature"] + ) + if execution_settings.extension_data.get("top_p") is not None: + mock_span.set_attribute.assert_any_call(gen_ai_attributes.TOP_P, execution_settings.extension_data["top_p"]) + + mock_span.add_event.assert_any_call( + gen_ai_attributes.PROMPT_EVENT, + {gen_ai_attributes.PROMPT_EVENT_PROMPT: _messages_to_openai_format(chat_history)}, + ) + + # After the call to the model + # Not all connectors return the same metadata + if mock_response[0].metadata.get("id") is not None: + mock_span.set_attribute.assert_any_call(gen_ai_attributes.RESPONSE_ID, mock_response[0].metadata["id"]) + if any(completion.finish_reason is not None for completion in mock_response): + mock_span.set_attribute.assert_any_call( + gen_ai_attributes.FINISH_REASON, + ",".join([str(completion.finish_reason) for completion in mock_response]), + ) + + mock_span.add_event.assert_any_call( + gen_ai_attributes.COMPLETION_EVENT, + {gen_ai_attributes.COMPLETION_EVENT_COMPLETION: _messages_to_openai_format(mock_response)}, + ) + + +@pytest.mark.asyncio +@patch("opentelemetry.trace.INVALID_SPAN") # When no tracer provider is available, the span will be an INVALID_SPAN +async def test_trace_chat_completion_exception( + mock_span, + execution_settings, + mock_response, + chat_history, + model_diagnostics_unit_test_env, +): + # Setup + chat_completion: ChatCompletionClientBase = MockChatCompletion(ai_model_id="ai_model_id") + + with patch.object(MockChatCompletion, "get_chat_message_contents", side_effect=ServiceResponseException()): + # We need to reapply the decorator to the method since the mock will not have the decorator applied + MockChatCompletion.get_chat_message_contents = trace_chat_completion(MockChatCompletion.MODEL_PROVIDER_NAME)( + chat_completion.get_chat_message_contents + ) + + with pytest.raises(ServiceResponseException): + await chat_completion.get_chat_message_contents(chat_history=chat_history, settings=execution_settings) + + exception = ServiceResponseException() + mock_span.set_attribute.assert_any_call(gen_ai_attributes.ERROR_TYPE, str(type(exception))) + mock_span.set_status.assert_any_call(StatusCode.ERROR, repr(exception)) + + mock_span.end.assert_any_call() diff --git a/python/tests/unit/utils/model_diagnostics/test_trace_text_completion.py b/python/tests/unit/utils/model_diagnostics/test_trace_text_completion.py new file mode 100644 index 000000000000..f6b4d47e1b97 --- /dev/null +++ b/python/tests/unit/utils/model_diagnostics/test_trace_text_completion.py @@ -0,0 +1,150 @@ +# Copyright (c) Microsoft. All rights reserved. + +from unittest.mock import patch + +import pytest +from opentelemetry.trace import StatusCode + +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase +from semantic_kernel.contents.chat_message_content import ChatMessageContent +from semantic_kernel.contents.text_content import TextContent +from semantic_kernel.exceptions.service_exceptions import ServiceResponseException +from semantic_kernel.utils.telemetry.model_diagnostics import gen_ai_attributes +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import ( + TEXT_COMPLETION_OPERATION, + _messages_to_openai_format, + trace_text_completion, +) +from tests.unit.utils.model_diagnostics.conftest import MockTextCompletion + +pytestmark = pytest.mark.parametrize( + "execution_settings, mock_response", + [ + pytest.param( + PromptExecutionSettings( + extension_data={ + "max_tokens": 1000, + "temperature": 0.5, + "top_p": 0.9, + } + ), + [ + TextContent( + ai_model_id="ai_model_id", + text="Test content", + metadata={"id": "test_id"}, + ) + ], + id="test_execution_settings_with_extension_data", + ), + pytest.param( + PromptExecutionSettings(), + [ + TextContent( + ai_model_id="ai_model_id", + text="Test content", + metadata={"id": "test_id"}, + ) + ], + id="test_execution_settings_no_extension_data", + ), + pytest.param( + PromptExecutionSettings(), + [ + TextContent( + ai_model_id="ai_model_id", + text="Test content", + metadata={}, + ) + ], + id="test_text_content_no_metadata", + ), + ], +) + + +@pytest.mark.asyncio +@patch("opentelemetry.trace.INVALID_SPAN") # When no tracer provider is available, the span will be an INVALID_SPAN +async def test_trace_text_completion( + mock_span, + execution_settings, + mock_response, + prompt, + model_diagnostics_unit_test_env, +): + # Setup + text_completion: TextCompletionClientBase = MockTextCompletion(ai_model_id="ai_model_id") + + with patch.object(MockTextCompletion, "get_text_contents", return_value=mock_response): + # We need to reapply the decorator to the method since the mock will not have the decorator applied + MockTextCompletion.get_text_contents = trace_text_completion(MockTextCompletion.MODEL_PROVIDER_NAME)( + text_completion.get_text_contents + ) + + results: list[ChatMessageContent] = await text_completion.get_text_contents( + prompt=prompt, settings=execution_settings + ) + + assert results == mock_response + + # Before the call to the model + mock_span.set_attributes.assert_called_with({ + gen_ai_attributes.OPERATION: TEXT_COMPLETION_OPERATION, + gen_ai_attributes.SYSTEM: MockTextCompletion.MODEL_PROVIDER_NAME, + gen_ai_attributes.MODEL: text_completion.ai_model_id, + }) + + # No all connectors take the same parameters + if execution_settings.extension_data.get("max_tokens") is not None: + mock_span.set_attribute.assert_any_call( + gen_ai_attributes.MAX_TOKENS, execution_settings.extension_data["max_tokens"] + ) + if execution_settings.extension_data.get("temperature") is not None: + mock_span.set_attribute.assert_any_call( + gen_ai_attributes.TEMPERATURE, execution_settings.extension_data["temperature"] + ) + if execution_settings.extension_data.get("top_p") is not None: + mock_span.set_attribute.assert_any_call(gen_ai_attributes.TOP_P, execution_settings.extension_data["top_p"]) + + mock_span.add_event.assert_any_call( + gen_ai_attributes.PROMPT_EVENT, {gen_ai_attributes.PROMPT_EVENT_PROMPT: prompt} + ) + + # After the call to the model + # Not all connectors return the same metadata + if mock_response[0].metadata.get("id") is not None: + mock_span.set_attribute.assert_any_call(gen_ai_attributes.RESPONSE_ID, mock_response[0].metadata["id"]) + + mock_span.add_event.assert_any_call( + gen_ai_attributes.COMPLETION_EVENT, + {gen_ai_attributes.COMPLETION_EVENT_COMPLETION: _messages_to_openai_format(mock_response)}, + ) + + +@pytest.mark.asyncio +@patch("opentelemetry.trace.INVALID_SPAN") # When no tracer provider is available, the span will be an INVALID_SPAN +async def test_trace_text_completion_exception( + mock_span, + execution_settings, + mock_response, + prompt, + model_diagnostics_unit_test_env, +): + # Setup + text_completion: TextCompletionClientBase = MockTextCompletion(ai_model_id="ai_model_id") + + with patch.object(MockTextCompletion, "get_text_contents", side_effect=ServiceResponseException()): + # We need to reapply the decorator to the method since the mock will not have the decorator applied + MockTextCompletion.get_text_contents = trace_text_completion(MockTextCompletion.MODEL_PROVIDER_NAME)( + text_completion.get_text_contents + ) + + with pytest.raises(ServiceResponseException): + await text_completion.get_text_contents(prompt=prompt, settings=execution_settings) + + exception = ServiceResponseException() + mock_span.set_attribute.assert_any_call(gen_ai_attributes.ERROR_TYPE, str(type(exception))) + mock_span.set_status.assert_any_call(StatusCode.ERROR, repr(exception)) + + mock_span.end.assert_any_call() diff --git a/python/tests/unit/utils/test_tracing.py b/python/tests/unit/utils/test_tracing.py deleted file mode 100644 index 5d2c2f9e4bf6..000000000000 --- a/python/tests/unit/utils/test_tracing.py +++ /dev/null @@ -1,241 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from unittest.mock import patch - -import pytest -from openai.types import Completion as TextCompletion -from openai.types import CompletionChoice -from opentelemetry.trace import StatusCode - -from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion -from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base import OpenAIChatCompletionBase -from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion import OpenAITextCompletion -from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings -from semantic_kernel.contents.chat_history import ChatHistory -from semantic_kernel.contents.chat_message_content import ChatMessageContent -from semantic_kernel.contents.text_content import TextContent -from semantic_kernel.contents.utils.author_role import AuthorRole -from semantic_kernel.contents.utils.finish_reason import FinishReason -from semantic_kernel.exceptions.service_exceptions import ServiceResponseException -from semantic_kernel.utils.telemetry.const import ( - CHAT_COMPLETION_OPERATION, - COMPLETION_EVENT, - COMPLETION_EVENT_COMPLETION, - ERROR_TYPE, - FINISH_REASON, - MAX_TOKENS, - MODEL, - OPERATION, - PROMPT_EVENT, - PROMPT_EVENT_PROMPT, - RESPONSE_ID, - SYSTEM, - TEMPERATURE, - TEXT_COMPLETION_OPERATION, - TOP_P, -) - -TEST_CONTENT = "Test content" -TEST_RESPONSE_ID = "dummy_id" -TEST_MAX_TOKENS = "1000" -TEST_MODEL = "dummy_model" -TEST_TEMPERATURE = "0.5" -TEST_TOP_P = "0.9" -TEST_CREATED_AT = 1 -TEST_TEXT_PROMPT = "Test prompt" -EXPECTED_CHAT_COMPLETION_EVENT_PAYLOAD = f'[{{"role": "assistant", "content": "{TEST_CONTENT}"}}]' -EXPECTED_TEXT_COMPLETION_EVENT_PAYLOAD = f'["{TEST_CONTENT}"]' - -TEST_CHAT_RESPONSE = [ - ChatMessageContent( - role=AuthorRole.ASSISTANT, - ai_model_id=TEST_MODEL, - content=TEST_CONTENT, - metadata={"id": TEST_RESPONSE_ID}, - finish_reason=FinishReason.STOP, - ) -] - -TEST_TEXT_RESPONSE = TextCompletion( - model=TEST_MODEL, - text=TEST_CONTENT, - id=TEST_RESPONSE_ID, - choices=[CompletionChoice(index=0, text=TEST_CONTENT, finish_reason="stop")], - created=TEST_CREATED_AT, - object="text_completion", -) - -TEST_TEXT_RESPONSE_METADATA = { - "id": TEST_RESPONSE_ID, - "created": TEST_CREATED_AT, - "system_fingerprint": None, - "logprobs": None, - "usage": None, -} - -EXPECTED_TEXT_CONTENT = [ - TextContent( - ai_model_id=TEST_MODEL, - text=TEST_CONTENT, - encoding=None, - metadata=TEST_TEXT_RESPONSE_METADATA, - inner_content=TEST_TEXT_RESPONSE, - ) -] - - -@pytest.mark.asyncio -@patch("semantic_kernel.utils.telemetry.decorators.are_model_diagnostics_enabled", return_value=True) -@patch("semantic_kernel.utils.telemetry.decorators.are_sensitive_events_enabled", return_value=True) -@patch( - "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base.OpenAIChatCompletionBase._send_chat_request", - return_value=TEST_CHAT_RESPONSE, -) -@patch("opentelemetry.trace.INVALID_SPAN") -async def test_trace_chat_completion( - mock_span, - mock_send_chat_request, - mock_sensitive_events_enabled, - mock_model_diagnostics_enabled, - openai_unit_test_env, -): - chat_completion = OpenAIChatCompletion(ai_model_id=TEST_MODEL, env_file_path="test.env") - extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} - - results: list[ChatMessageContent] = await chat_completion.get_chat_message_contents( - chat_history=ChatHistory(), settings=PromptExecutionSettings(extension_data=extension_data) - ) - - assert results == TEST_CHAT_RESPONSE - - mock_span.set_attributes.assert_called_with( - { - OPERATION: CHAT_COMPLETION_OPERATION, - SYSTEM: OpenAIChatCompletionBase.MODEL_PROVIDER_NAME, - MODEL: TEST_MODEL, - } - ) - mock_span.set_attribute.assert_any_call(MAX_TOKENS, TEST_MAX_TOKENS) - mock_span.set_attribute.assert_any_call(TEMPERATURE, TEST_TEMPERATURE) - mock_span.set_attribute.assert_any_call(TOP_P, TEST_TOP_P) - mock_span.add_event.assert_any_call(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: "[]"}) - - mock_span.set_attribute.assert_any_call(RESPONSE_ID, TEST_RESPONSE_ID) - mock_span.set_attribute.assert_any_call(FINISH_REASON, str(FinishReason.STOP)) - mock_span.add_event.assert_any_call( - COMPLETION_EVENT, {COMPLETION_EVENT_COMPLETION: EXPECTED_CHAT_COMPLETION_EVENT_PAYLOAD} - ) - - -@pytest.mark.asyncio -@patch("semantic_kernel.utils.telemetry.decorators.are_model_diagnostics_enabled", return_value=True) -@patch("semantic_kernel.utils.telemetry.decorators.are_sensitive_events_enabled", return_value=True) -@patch( - "semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base.OpenAITextCompletionBase._send_request", - return_value=TEST_TEXT_RESPONSE, -) -@patch("opentelemetry.trace.INVALID_SPAN") -async def test_trace_text_completion( - mock_span, mock_send_request, mock_sensitive_events_enabled, mock_model_diagnostics_enabled, openai_unit_test_env -): - chat_completion = OpenAITextCompletion(ai_model_id=TEST_MODEL, env_file_path="test.env") - extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} - - results: list[TextContent] = await chat_completion.get_text_contents( - prompt=TEST_TEXT_PROMPT, settings=PromptExecutionSettings(extension_data=extension_data) - ) - - assert results == EXPECTED_TEXT_CONTENT - - mock_span.set_attributes.assert_called_with( - { - OPERATION: TEXT_COMPLETION_OPERATION, - SYSTEM: OpenAIChatCompletionBase.MODEL_PROVIDER_NAME, - MODEL: TEST_MODEL, - } - ) - mock_span.set_attribute.assert_any_call(MAX_TOKENS, TEST_MAX_TOKENS) - mock_span.set_attribute.assert_any_call(TEMPERATURE, TEST_TEMPERATURE) - mock_span.set_attribute.assert_any_call(TOP_P, TEST_TOP_P) - mock_span.add_event.assert_any_call(PROMPT_EVENT, {PROMPT_EVENT_PROMPT: TEST_TEXT_PROMPT}) - - mock_span.set_attribute.assert_any_call(RESPONSE_ID, TEST_RESPONSE_ID) - mock_span.add_event.assert_any_call( - COMPLETION_EVENT, {COMPLETION_EVENT_COMPLETION: EXPECTED_TEXT_COMPLETION_EVENT_PAYLOAD} - ) - - -@pytest.mark.asyncio -@patch("semantic_kernel.utils.telemetry.decorators.are_model_diagnostics_enabled", return_value=True) -@patch("semantic_kernel.utils.telemetry.decorators.are_sensitive_events_enabled", return_value=True) -@patch( - "semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base.OpenAIChatCompletionBase._send_chat_request", - side_effect=ServiceResponseException, -) -@patch("opentelemetry.trace.INVALID_SPAN") -async def test_trace_chat_completion_exception( - mock_span, - mock_send_chat_request, - mock_sensitive_events_enabled, - mock_model_diagnostics_enabled, - openai_unit_test_env, -): - chat_completion = OpenAIChatCompletion(ai_model_id=TEST_MODEL, env_file_path="test.env") - extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} - - with pytest.raises(ServiceResponseException): - await chat_completion.get_chat_message_contents( - chat_history=ChatHistory(), settings=PromptExecutionSettings(extension_data=extension_data) - ) - - mock_span.set_attributes.assert_called_with( - { - OPERATION: CHAT_COMPLETION_OPERATION, - SYSTEM: OpenAIChatCompletionBase.MODEL_PROVIDER_NAME, - MODEL: TEST_MODEL, - } - ) - - exception = ServiceResponseException() - mock_span.set_attribute.assert_any_call(ERROR_TYPE, str(type(exception))) - mock_span.set_status.assert_any_call(StatusCode.ERROR, repr(exception)) - - mock_span.end.assert_any_call() - - -@pytest.mark.asyncio -@patch("semantic_kernel.utils.telemetry.decorators.are_model_diagnostics_enabled", return_value=True) -@patch("semantic_kernel.utils.telemetry.decorators.are_sensitive_events_enabled", return_value=True) -@patch( - "semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base.OpenAITextCompletionBase._send_request", - side_effect=ServiceResponseException, -) -@patch("opentelemetry.trace.INVALID_SPAN") -async def test_trace_text_completion_exception( - mock_span, - mock_send_chat_request, - mock_sensitive_events_enabled, - mock_model_diagnostics_enabled, - openai_unit_test_env, -): - chat_completion = OpenAITextCompletion(ai_model_id=TEST_MODEL, env_file_path="test.env") - extension_data = {"max_tokens": TEST_MAX_TOKENS, "temperature": TEST_TEMPERATURE, "top_p": TEST_TOP_P} - - with pytest.raises(ServiceResponseException): - await chat_completion.get_text_contents( - prompt=TEST_TEXT_PROMPT, settings=PromptExecutionSettings(extension_data=extension_data) - ) - - mock_span.set_attributes.assert_called_with( - { - OPERATION: TEXT_COMPLETION_OPERATION, - SYSTEM: OpenAIChatCompletionBase.MODEL_PROVIDER_NAME, - MODEL: TEST_MODEL, - } - ) - - exception = ServiceResponseException() - mock_span.set_attribute.assert_any_call(ERROR_TYPE, str(type(exception))) - mock_span.set_status.assert_any_call(StatusCode.ERROR, repr(exception)) - - mock_span.end.assert_any_call() From 0e7bf893a4f268ec4fd61f38ad1e933ca58e0fdd Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:46:20 +0100 Subject: [PATCH 78/87] .Net: OpenAI V2 Bugfix for Streaming Asynchronous Filter Results (#8422) ### Motivation and Context Resolve #8407 ### Description Add temporary bugfix for error described in below issues: - #8407 - https://github.com/openai/openai-dotnet/issues/198 - Added UnitTest to ensure expected behavior. ### 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: --- .../AzureOpenAIChatCompletionServiceTests.cs | 60 +++++++++++++++++++ ...letion_streaming_async_filter_response.txt | 13 ++++ .../Core/ClientCore.ChatCompletion.cs | 30 +++++----- .../Core/OpenAIStreamingChatMessageContent.cs | 30 ++++++++-- 4 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_async_filter_response.txt diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs index 435caa3c425a..9302b75c39bf 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs @@ -415,6 +415,66 @@ public async Task GetStreamingTextContentsWorksCorrectlyAsync() Assert.Equal("Stop", enumerator.Current.Metadata?["FinishReason"]); } + [Fact] + public async Task GetStreamingChatContentsWithAsynchronousFilterWorksCorrectlyAsync() + { + // 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_async_filter_response.txt"))); + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }); + + // Act & Assert + var enumerator = service.GetStreamingChatMessageContentsAsync("Prompt").GetAsyncEnumerator(); + +#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. + await enumerator.MoveNextAsync(); + var message = enumerator.Current; + + Assert.IsType(message.InnerContent); + var update = (StreamingChatCompletionUpdate)message.InnerContent; + var promptResults = update.GetContentFilterResultForPrompt(); + Assert.Equal(ContentFilterSeverity.Safe, promptResults.Hate.Severity); + Assert.Equal(ContentFilterSeverity.Safe, promptResults.Sexual.Severity); + Assert.Equal(ContentFilterSeverity.Safe, promptResults.Violence.Severity); + Assert.Equal(ContentFilterSeverity.Safe, promptResults.SelfHarm.Severity); + Assert.False(promptResults.Jailbreak.Detected); + + await enumerator.MoveNextAsync(); + message = enumerator.Current; + + await enumerator.MoveNextAsync(); + message = enumerator.Current; + + await enumerator.MoveNextAsync(); + message = enumerator.Current; + + await enumerator.MoveNextAsync(); + message = enumerator.Current; + + Assert.IsType(message.InnerContent); + update = (StreamingChatCompletionUpdate)message.InnerContent; + + var filterResults = update.GetContentFilterResultForResponse(); + Assert.Equal(ContentFilterSeverity.Safe, filterResults.Hate.Severity); + Assert.Equal(ContentFilterSeverity.Safe, filterResults.Sexual.Severity); + Assert.Equal(ContentFilterSeverity.Safe, filterResults.SelfHarm.Severity); + Assert.Equal(ContentFilterSeverity.Safe, filterResults.Violence.Severity); + + await enumerator.MoveNextAsync(); + message = enumerator.Current; + + Assert.IsType(message.InnerContent); + update = (StreamingChatCompletionUpdate)message.InnerContent; + filterResults = update.GetContentFilterResultForResponse(); + Assert.False(filterResults.ProtectedMaterialCode.Detected); + Assert.False(filterResults.ProtectedMaterialText.Detected); +#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. + } + [Fact] public async Task GetStreamingChatMessageContentsWorksCorrectlyAsync() { diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_async_filter_response.txt b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_async_filter_response.txt new file mode 100644 index 000000000000..078ad45af412 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_async_filter_response.txt @@ -0,0 +1,13 @@ +data: {"choices":[],"created":0,"id":"","model":"","object":"","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"jailbreak":{"filtered":false,"detected":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}]} + +data: {"choices":[{"delta":{"content":"","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1724860848,"id":"chatcmpl-123","model":"gpt-4o-2024-05-13","object":"chat.completion.chunk","system_fingerprint":"fp_abc28019ad"} + +data: {"choices":[{"delta":{"content":"Kindness"},"finish_reason":null,"index":0,"logprobs":null}],"created":1724860848,"id":"chatcmpl-123","model":"gpt-4o-2024-05-13","object":"chat.completion.chunk","system_fingerprint":"fp_abc28019ad"} + +data: {"choices":[{"delta":{},"finish_reason":"stop","index":0,"logprobs":null}],"created":1724860848,"id":"chatcmpl-123","model":"gpt-4o-2024-05-13","object":"chat.completion.chunk","system_fingerprint":"fp_abc28019ad"} + +data: {"choices":[{"content_filter_offsets":{"check_offset":1576,"start_offset":1576,"end_offset":2318},"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"finish_reason":null,"index":0}],"created":0,"id":"","model":"","object":""} + +data: {"choices":[{"content_filter_offsets":{"check_offset":1576,"start_offset":1576,"end_offset":2318},"content_filter_results":{"protected_material_code":{"filtered":false,"detected":false},"protected_material_text":{"filtered":false,"detected":false}},"finish_reason":null,"index":0}],"created":0,"id":"","model":"","object":""} + +data: [DONE] \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs index 6546bd291235..bcad35358b0d 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs @@ -416,24 +416,26 @@ internal async IAsyncEnumerable GetStreamingC var openAIStreamingChatMessageContent = new OpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, targetModel, metadata); - foreach (var functionCallUpdate in chatCompletionUpdate.ToolCallUpdates) + if (openAIStreamingChatMessageContent.ToolCallUpdates is not null) { - // 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)) + foreach (var functionCallUpdate in openAIStreamingChatMessageContent.ToolCallUpdates!) { - continue; - } + // 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)); + openAIStreamingChatMessageContent.Items.Add(new StreamingFunctionCallUpdateContent( + callId: functionCallUpdate.Id, + name: functionCallUpdate.FunctionName, + arguments: functionCallUpdate.FunctionArgumentsUpdate, + functionCallIndex: functionCallUpdate.Index)); + } } - streamedContents?.Add(openAIStreamingChatMessageContent); yield return openAIStreamingChatMessageContent; } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIStreamingChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIStreamingChatMessageContent.cs index bd9ae55ce888..e83c16cdc31e 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIStreamingChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIStreamingChatMessageContent.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Text; using Microsoft.SemanticKernel.ChatCompletion; @@ -33,7 +34,7 @@ internal OpenAIStreamingChatMessageContent( string modelId, IReadOnlyDictionary? metadata = null) : base( - chatUpdate.Role.HasValue ? new AuthorRole(chatUpdate.Role.Value.ToString()) : null, + null, null, chatUpdate, choiceIndex, @@ -41,9 +42,30 @@ internal OpenAIStreamingChatMessageContent( Encoding.UTF8, metadata) { - this.ToolCallUpdates = chatUpdate.ToolCallUpdates; - this.FinishReason = chatUpdate.FinishReason; - this.Items = CreateContentItems(chatUpdate.ContentUpdate); + try + { + this.FinishReason = chatUpdate.FinishReason; + + if (chatUpdate.Role.HasValue) + { + this.Role = new AuthorRole(chatUpdate.Role.ToString()!); + } + + if (chatUpdate.ToolCallUpdates is not null) + { + this.ToolCallUpdates = chatUpdate.ToolCallUpdates; + } + + if (chatUpdate.ContentUpdate is not null) + { + this.Items = CreateContentItems(chatUpdate.ContentUpdate); + } + } + catch (NullReferenceException) + { + // Temporary bugfix for: https://github.com/openai/openai-dotnet/issues/198 + // TODO: Remove this try-catch block once the bug is fixed. + } } /// From 355729a488ced1be66955ea53606ba41789222f6 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 2 Sep 2024 10:33:09 +0100 Subject: [PATCH 79/87] .Net: OpenAI V2 - Azure SDK beta.4 update + Integration Tests Fix (#8427) ### Motivation and Context - Updated Integration Tests to not expect metadata - Update Azure SDK Version to `beta.4` after discovering a failure running `beta.3` with AudioCLient - https://github.com/Azure/azure-sdk-for-net/issues/45706 --- dotnet/Directory.Packages.props | 2 +- .../IntegrationTests/Agents/ChatCompletionAgentTests.cs | 1 - .../AzureOpenAI/AzureOpenAIChatCompletionTests.cs | 2 -- .../AzureOpenAIChatCompletion_NonStreamingTests.cs | 8 -------- 4 files changed, 1 insertion(+), 12 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 07663ec833e5..50ef0a16d455 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -8,7 +8,7 @@ - + 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; diff --git a/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs index 5728632e2886..7d47ee0f45e0 100644 --- a/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs @@ -136,8 +136,6 @@ public async Task AzureOpenAIShouldReturnMetadataAsync() Assert.True(jsonObject.TryGetProperty("OutputTokens", out JsonElement completionTokensJson)); Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); Assert.NotEqual(0, completionTokens); - - Assert.True(result.Metadata.ContainsKey("ContentFilterResultForResponse")); } [Theory(Skip = "This test is for manual verification.")] diff --git a/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs index b16a77bf882a..a463410765f5 100644 --- a/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs @@ -60,8 +60,6 @@ public async Task ChatCompletionShouldUseChatHistoryAndReturnMetadataAsync() Assert.True(result.Metadata.TryGetValue("CreatedAt", out object? createdAt)); Assert.NotNull(createdAt); - Assert.True(result.Metadata.ContainsKey("ContentFilterResultForPrompt")); - Assert.True(result.Metadata.ContainsKey("SystemFingerprint")); Assert.True(result.Metadata.TryGetValue("Usage", out object? usageObject)); @@ -76,8 +74,6 @@ public async Task ChatCompletionShouldUseChatHistoryAndReturnMetadataAsync() Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); Assert.NotEqual(0, completionTokens); - Assert.True(result.Metadata.ContainsKey("ContentFilterResultForResponse")); - Assert.True(result.Metadata.TryGetValue("FinishReason", out object? finishReason)); Assert.Equal("Stop", finishReason); @@ -123,8 +119,6 @@ public async Task TextGenerationShouldReturnMetadataAsync() Assert.True(result.Metadata.TryGetValue("CreatedAt", out object? createdAt)); Assert.NotNull(createdAt); - Assert.True(result.Metadata.ContainsKey("ContentFilterResultForPrompt")); - Assert.True(result.Metadata.ContainsKey("SystemFingerprint")); Assert.True(result.Metadata.TryGetValue("Usage", out object? usageObject)); @@ -139,8 +133,6 @@ public async Task TextGenerationShouldReturnMetadataAsync() Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); Assert.NotEqual(0, completionTokens); - Assert.True(result.Metadata.ContainsKey("ContentFilterResultForResponse")); - Assert.True(result.Metadata.TryGetValue("FinishReason", out object? finishReason)); Assert.Equal("Stop", finishReason); From ef2b8f3310ca4b589528e4078fae8f837f2cd375 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:02:54 +0100 Subject: [PATCH 80/87] .Net: Openai V2 - IT Passing, Client Request Body Optionality Improvements. (#8476) ### Motivation and Context - Reduce unnecessary request body information when not explicitly provided by the caller. - Ensure all Integration Tests passes - Ensure all Unit Tests passes - --- .../Core/AzureClientCore.ChatCompletion.cs | 18 ++++++++++------ .../OpenAIChatCompletionServiceTests.cs | 4 ++-- .../Core/ClientCore.ChatCompletion.cs | 16 ++++++++------ ...zureOpenAIChatCompletion_StreamingTests.cs | 3 +-- .../OpenAIChatCompletion_StreamingTests.cs | 3 +-- .../PromptWithChatRolesStreamingTest.json | 21 +++++++++++++++++++ .../Data/PromptWithChatRolesTest.json | 5 ----- ...PromptWithComplexObjectsStreamingTest.json | 13 ++++++++++++ .../Data/PromptWithComplexObjectsTest.json | 5 ----- ...romptWithHelperFunctionsStreamingTest.json | 17 +++++++++++++++ .../Data/PromptWithHelperFunctionsTest.json | 5 ----- ...PromptWithSimpleVariableStreamingTest.json | 13 ++++++++++++ .../Data/PromptWithSimpleVariableTest.json | 5 ----- .../Data/SimplePromptStreamingTest.json | 13 ++++++++++++ .../CrossLanguage/Data/SimplePromptTest.json | 5 ----- .../CrossLanguage/KernelRequestTracer.cs | 8 ++++--- .../CrossLanguage/PromptWithChatRolesTest.cs | 10 ++++----- .../PromptWithComplexObjectsTest.cs | 10 ++++----- .../PromptWithHelperFunctionsTest.cs | 5 ++++- .../PromptWithSimpleVariableTest.cs | 5 ++++- .../CrossLanguage/SimplePromptTest.cs | 5 ++++- .../CrossLanguage/YamlPromptTest.cs | 6 +++--- .../IntegrationTests/IntegrationTests.csproj | 20 ++++++++++++++++++ 23 files changed, 151 insertions(+), 64 deletions(-) create mode 100644 dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithChatRolesStreamingTest.json create mode 100644 dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithComplexObjectsStreamingTest.json create mode 100644 dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithHelperFunctionsStreamingTest.json create mode 100644 dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithSimpleVariableStreamingTest.json create mode 100644 dotnet/src/IntegrationTests/CrossLanguage/Data/SimplePromptStreamingTest.json diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs index f0c7cdf4250d..5deddb7e5646 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs @@ -47,22 +47,28 @@ protected override ChatCompletionOptions CreateChatCompletionOptions( EndUserId = executionSettings.User, TopLogProbabilityCount = executionSettings.TopLogprobs, IncludeLogProbabilities = executionSettings.Logprobs, - ResponseFormat = GetResponseFormat(azureSettings) ?? ChatResponseFormat.Text, - ToolChoice = toolCallingConfig.Choice }; - if (azureSettings.AzureChatDataSource is not null) + var responseFormat = GetResponseFormat(executionSettings); + if (responseFormat 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. + options.ResponseFormat = responseFormat; } + options.ToolChoice = toolCallingConfig.Choice; + if (toolCallingConfig.Tools is { Count: > 0 } tools) { options.Tools.AddRange(tools); } + 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 (executionSettings.TokenSelectionBiases is not null) { foreach (var keyValue in executionSettings.TokenSelectionBiases) diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs index 326b14bc7368..f560c9924977 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs @@ -551,8 +551,8 @@ public async Task GetChatMessageContentsWithChatMessageContentItemCollectionAndS 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) }; + using var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(ChatCompletionResponse) }; + this._messageHandlerStub.ResponseToReturn = response; var chatHistory = new ChatHistory(); chatHistory.AddUserMessage(Prompt); diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs index bcad35358b0d..3bc984478a5d 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs @@ -27,7 +27,7 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; internal partial class ClientCore { protected const string ModelProvider = "openai"; - protected record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); + 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 @@ -186,8 +186,6 @@ internal async Task> GetChatMessageContentsAsy 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 @@ -649,10 +647,16 @@ protected virtual ChatCompletionOptions CreateChatCompletionOptions( EndUserId = executionSettings.User, TopLogProbabilityCount = executionSettings.TopLogprobs, IncludeLogProbabilities = executionSettings.Logprobs, - ResponseFormat = GetResponseFormat(executionSettings) ?? ChatResponseFormat.Text, - ToolChoice = toolCallingConfig.Choice, }; + var responseFormat = GetResponseFormat(executionSettings); + if (responseFormat is not null) + { + options.ResponseFormat = responseFormat; + } + + options.ToolChoice = toolCallingConfig.Choice; + if (toolCallingConfig.Tools is { Count: > 0 } tools) { options.Tools.AddRange(tools); @@ -1138,7 +1142,7 @@ private ToolCallingConfig GetToolCallingConfiguration(Kernel? kernel, OpenAIProm { if (executionSettings.ToolCallBehavior is null) { - return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); + return new ToolCallingConfig(Tools: null, Choice: null, AutoInvoke: false); } if (requestIndex >= executionSettings.ToolCallBehavior.MaximumUseAttempts) diff --git a/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs index 0707f835ad7b..5fc0e7e0cad7 100644 --- a/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs @@ -116,7 +116,7 @@ public async Task TextGenerationShouldReturnMetadataAsync() 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)) + await foreach (var update in textGeneration.GetStreamingTextContentsAsync("What is the capital of France?", null, kernel)) { stringBuilder.Append(update); @@ -127,7 +127,6 @@ public async Task TextGenerationShouldReturnMetadataAsync() } // Assert - Assert.Contains("I don't know", stringBuilder.ToString()); Assert.NotNull(metadata); Assert.True(metadata.TryGetValue("Id", out object? id)); diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs index 342c6ed6f93f..1c09606cf932 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs @@ -119,7 +119,7 @@ public async Task TextGenerationShouldReturnMetadataAsync() 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)) + await foreach (var update in textGeneration.GetStreamingTextContentsAsync("What is the capital of France?", null, kernel)) { stringBuilder.Append(update); @@ -133,7 +133,6 @@ public async Task TextGenerationShouldReturnMetadataAsync() } // Assert - Assert.Contains("I don't know", stringBuilder.ToString()); Assert.NotNull(metadata); Assert.True(metadata.TryGetValue("Id", out object? id)); diff --git a/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithChatRolesStreamingTest.json b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithChatRolesStreamingTest.json new file mode 100644 index 000000000000..1a85a5330b24 --- /dev/null +++ b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithChatRolesStreamingTest.json @@ -0,0 +1,21 @@ +{ + "messages": [ + { + "content": "Can you help me tell the time in Seattle right now?", + "role": "user" + }, + { + "content": "Sure! The time in Seattle is currently 3:00 PM.", + "role": "assistant" + }, + { + "content": "What about New York?", + "role": "user" + } + ], + "model": "Dummy", + "stream": true, + "stream_options": { + "include_usage": true + } +} \ No newline at end of file diff --git a/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithChatRolesTest.json b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithChatRolesTest.json index 397d351c0f50..959c4f62fe15 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithChatRolesTest.json +++ b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithChatRolesTest.json @@ -13,10 +13,5 @@ "role": "user" } ], - "temperature": 1, - "top_p": 1, - "n": 1, - "presence_penalty": 0, - "frequency_penalty": 0, "model": "Dummy" } \ No newline at end of file diff --git a/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithComplexObjectsStreamingTest.json b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithComplexObjectsStreamingTest.json new file mode 100644 index 000000000000..02f714872433 --- /dev/null +++ b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithComplexObjectsStreamingTest.json @@ -0,0 +1,13 @@ +{ + "messages": [ + { + "content": "Can you help me tell the time in Seattle right now?", + "role": "user" + } + ], + "model": "Dummy", + "stream": true, + "stream_options": { + "include_usage": true + } +} \ No newline at end of file diff --git a/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithComplexObjectsTest.json b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithComplexObjectsTest.json index 8445e850bbb4..8d23881d66ff 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithComplexObjectsTest.json +++ b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithComplexObjectsTest.json @@ -5,10 +5,5 @@ "role": "user" } ], - "temperature": 1, - "top_p": 1, - "n": 1, - "presence_penalty": 0, - "frequency_penalty": 0, "model": "Dummy" } \ No newline at end of file diff --git a/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithHelperFunctionsStreamingTest.json b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithHelperFunctionsStreamingTest.json new file mode 100644 index 000000000000..f9472d3f2da0 --- /dev/null +++ b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithHelperFunctionsStreamingTest.json @@ -0,0 +1,17 @@ +{ + "messages": [ + { + "content": "The current time is Sun, 04 Jun 1989 12:11:13 GMT", + "role": "system" + }, + { + "content": "Can you help me tell the time in Seattle right now?", + "role": "user" + } + ], + "model": "Dummy", + "stream": true, + "stream_options": { + "include_usage": true + } +} \ No newline at end of file diff --git a/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithHelperFunctionsTest.json b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithHelperFunctionsTest.json index 571ddbcd55c6..cc0b8acb9f2e 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithHelperFunctionsTest.json +++ b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithHelperFunctionsTest.json @@ -9,10 +9,5 @@ "role": "user" } ], - "temperature": 1, - "top_p": 1, - "n": 1, - "presence_penalty": 0, - "frequency_penalty": 0, "model": "Dummy" } \ No newline at end of file diff --git a/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithSimpleVariableStreamingTest.json b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithSimpleVariableStreamingTest.json new file mode 100644 index 000000000000..02f714872433 --- /dev/null +++ b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithSimpleVariableStreamingTest.json @@ -0,0 +1,13 @@ +{ + "messages": [ + { + "content": "Can you help me tell the time in Seattle right now?", + "role": "user" + } + ], + "model": "Dummy", + "stream": true, + "stream_options": { + "include_usage": true + } +} \ No newline at end of file diff --git a/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithSimpleVariableTest.json b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithSimpleVariableTest.json index 8445e850bbb4..8d23881d66ff 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithSimpleVariableTest.json +++ b/dotnet/src/IntegrationTests/CrossLanguage/Data/PromptWithSimpleVariableTest.json @@ -5,10 +5,5 @@ "role": "user" } ], - "temperature": 1, - "top_p": 1, - "n": 1, - "presence_penalty": 0, - "frequency_penalty": 0, "model": "Dummy" } \ No newline at end of file diff --git a/dotnet/src/IntegrationTests/CrossLanguage/Data/SimplePromptStreamingTest.json b/dotnet/src/IntegrationTests/CrossLanguage/Data/SimplePromptStreamingTest.json new file mode 100644 index 000000000000..02f714872433 --- /dev/null +++ b/dotnet/src/IntegrationTests/CrossLanguage/Data/SimplePromptStreamingTest.json @@ -0,0 +1,13 @@ +{ + "messages": [ + { + "content": "Can you help me tell the time in Seattle right now?", + "role": "user" + } + ], + "model": "Dummy", + "stream": true, + "stream_options": { + "include_usage": true + } +} \ No newline at end of file diff --git a/dotnet/src/IntegrationTests/CrossLanguage/Data/SimplePromptTest.json b/dotnet/src/IntegrationTests/CrossLanguage/Data/SimplePromptTest.json index 8445e850bbb4..8d23881d66ff 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/Data/SimplePromptTest.json +++ b/dotnet/src/IntegrationTests/CrossLanguage/Data/SimplePromptTest.json @@ -5,10 +5,5 @@ "role": "user" } ], - "temperature": 1, - "top_p": 1, - "n": 1, - "presence_penalty": 0, - "frequency_penalty": 0, "model": "Dummy" } \ No newline at end of file diff --git a/dotnet/src/IntegrationTests/CrossLanguage/KernelRequestTracer.cs b/dotnet/src/IntegrationTests/CrossLanguage/KernelRequestTracer.cs index bbc55dfabfda..1621ffdfbfa8 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/KernelRequestTracer.cs +++ b/dotnet/src/IntegrationTests/CrossLanguage/KernelRequestTracer.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.IO; using System.Net; using System.Net.Http; using System.Text; @@ -39,6 +40,7 @@ internal sealed class KernelRequestTracer : IDisposable ] }"; + private MemoryStream? _memoryDummyResponse; private HttpClient? _httpClient; private HttpMessageHandlerStub? _httpMessageHandlerStub; @@ -134,17 +136,17 @@ private void DisposeHttpResources() { this._httpClient?.Dispose(); this._httpMessageHandlerStub?.Dispose(); + this._memoryDummyResponse?.Dispose(); } private void ResetHttpComponents() { this.DisposeHttpResources(); - + this._memoryDummyResponse = new MemoryStream(Encoding.UTF8.GetBytes(DummyResponse)); this._httpMessageHandlerStub = new HttpMessageHandlerStub(); this._httpMessageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent(DummyResponse, - Encoding.UTF8, "application/json") + Content = new StreamContent(this._memoryDummyResponse) }; this._httpClient = new HttpClient(this._httpMessageHandlerStub); } diff --git a/dotnet/src/IntegrationTests/CrossLanguage/PromptWithChatRolesTest.cs b/dotnet/src/IntegrationTests/CrossLanguage/PromptWithChatRolesTest.cs index 1e43ec9a4f93..fe12882d2dca 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/PromptWithChatRolesTest.cs +++ b/dotnet/src/IntegrationTests/CrossLanguage/PromptWithChatRolesTest.cs @@ -30,15 +30,13 @@ public async Task PromptWithChatRolesAsync(bool isInline, bool isStreaming, stri JsonNode? obtainedObject = JsonNode.Parse(requestContent); Assert.NotNull(obtainedObject); - string expected = await File.ReadAllTextAsync("./CrossLanguage/Data/PromptWithChatRolesTest.json"); + string expected = await File.ReadAllTextAsync(isStreaming + ? "./CrossLanguage/Data/PromptWithChatRolesStreamingTest.json" + : "./CrossLanguage/Data/PromptWithChatRolesTest.json"); + JsonNode? expectedObject = JsonNode.Parse(expected); Assert.NotNull(expectedObject); - if (isStreaming) - { - expectedObject["stream"] = true; - } - Assert.True(JsonNode.DeepEquals(obtainedObject, expectedObject)); } } diff --git a/dotnet/src/IntegrationTests/CrossLanguage/PromptWithComplexObjectsTest.cs b/dotnet/src/IntegrationTests/CrossLanguage/PromptWithComplexObjectsTest.cs index 87fb3e1c888d..b8a9a9b275ea 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/PromptWithComplexObjectsTest.cs +++ b/dotnet/src/IntegrationTests/CrossLanguage/PromptWithComplexObjectsTest.cs @@ -39,15 +39,13 @@ public async Task PromptWithComplexObjectsAsync(bool isInline, bool isStreaming, JsonNode? obtainedObject = JsonNode.Parse(requestContent); Assert.NotNull(obtainedObject); - string expected = await File.ReadAllTextAsync("./CrossLanguage/Data/PromptWithComplexObjectsTest.json"); + string expected = await File.ReadAllTextAsync(isStreaming + ? "./CrossLanguage/Data/PromptWithComplexObjectsStreamingTest.json" + : "./CrossLanguage/Data/PromptWithComplexObjectsTest.json"); + JsonNode? expectedObject = JsonNode.Parse(expected); Assert.NotNull(expectedObject); - if (isStreaming) - { - expectedObject["stream"] = true; - } - Assert.True(JsonNode.DeepEquals(obtainedObject, expectedObject)); } } diff --git a/dotnet/src/IntegrationTests/CrossLanguage/PromptWithHelperFunctionsTest.cs b/dotnet/src/IntegrationTests/CrossLanguage/PromptWithHelperFunctionsTest.cs index 12d7166e0bb5..ab192c2429cc 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/PromptWithHelperFunctionsTest.cs +++ b/dotnet/src/IntegrationTests/CrossLanguage/PromptWithHelperFunctionsTest.cs @@ -37,7 +37,10 @@ public async Task PromptWithHelperFunctionsAsync(bool isInline, bool isStreaming JsonNode? obtainedObject = JsonNode.Parse(requestContent); Assert.NotNull(obtainedObject); - string expected = await File.ReadAllTextAsync("./CrossLanguage/Data/PromptWithHelperFunctionsTest.json"); + string expected = await File.ReadAllTextAsync(isStreaming + ? "./CrossLanguage/Data/PromptWithHelperFunctionsStreamingTest.json" + : "./CrossLanguage/Data/PromptWithHelperFunctionsTest.json"); + JsonNode? expectedObject = JsonNode.Parse(expected); Assert.NotNull(expectedObject); diff --git a/dotnet/src/IntegrationTests/CrossLanguage/PromptWithSimpleVariableTest.cs b/dotnet/src/IntegrationTests/CrossLanguage/PromptWithSimpleVariableTest.cs index 80fa3bd5ae3e..af23d6b462ea 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/PromptWithSimpleVariableTest.cs +++ b/dotnet/src/IntegrationTests/CrossLanguage/PromptWithSimpleVariableTest.cs @@ -34,7 +34,10 @@ public async Task PromptWithSimpleVariableAsync(bool isInline, bool isStreaming, JsonNode? obtainedObject = JsonNode.Parse(requestContent); Assert.NotNull(obtainedObject); - string expected = await File.ReadAllTextAsync("./CrossLanguage/Data/PromptWithSimpleVariableTest.json"); + string expected = await File.ReadAllTextAsync(isStreaming + ? "./CrossLanguage/Data/PromptWithSimpleVariableStreamingTest.json" + : "./CrossLanguage/Data/PromptWithSimpleVariableTest.json"); + JsonNode? expectedObject = JsonNode.Parse(expected); Assert.NotNull(expectedObject); diff --git a/dotnet/src/IntegrationTests/CrossLanguage/SimplePromptTest.cs b/dotnet/src/IntegrationTests/CrossLanguage/SimplePromptTest.cs index d9cfa268ca49..46580dce8135 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/SimplePromptTest.cs +++ b/dotnet/src/IntegrationTests/CrossLanguage/SimplePromptTest.cs @@ -30,7 +30,10 @@ public async Task SimplePromptAsync(bool isInline, bool isStreaming, string temp JsonNode? obtainedObject = JsonNode.Parse(requestContent); Assert.NotNull(obtainedObject); - string expected = await File.ReadAllTextAsync("./CrossLanguage/Data/SimplePromptTest.json"); + string expected = await File.ReadAllTextAsync(isStreaming + ? "./CrossLanguage/Data/SimplePromptStreamingTest.json" + : "./CrossLanguage/Data/SimplePromptTest.json"); + JsonNode? expectedObject = JsonNode.Parse(expected); Assert.NotNull(expectedObject); diff --git a/dotnet/src/IntegrationTests/CrossLanguage/YamlPromptTest.cs b/dotnet/src/IntegrationTests/CrossLanguage/YamlPromptTest.cs index 084bcefbfd5f..8b0805165437 100644 --- a/dotnet/src/IntegrationTests/CrossLanguage/YamlPromptTest.cs +++ b/dotnet/src/IntegrationTests/CrossLanguage/YamlPromptTest.cs @@ -13,11 +13,11 @@ public class YamlPromptTest { [Theory] [InlineData(false, "./CrossLanguage/Data/SimplePromptTest.yaml", "./CrossLanguage/Data/SimplePromptTest.json")] - [InlineData(true, "./CrossLanguage/Data/SimplePromptTest.yaml", "./CrossLanguage/Data/SimplePromptTest.json")] + [InlineData(true, "./CrossLanguage/Data/SimplePromptTest.yaml", "./CrossLanguage/Data/SimplePromptStreamingTest.json")] [InlineData(false, "./CrossLanguage/Data/PromptWithChatRolesTest-HB.yaml", "./CrossLanguage/Data/PromptWithChatRolesTest.json")] - [InlineData(true, "./CrossLanguage/Data/PromptWithChatRolesTest-HB.yaml", "./CrossLanguage/Data/PromptWithChatRolesTest.json")] + [InlineData(true, "./CrossLanguage/Data/PromptWithChatRolesTest-HB.yaml", "./CrossLanguage/Data/PromptWithChatRolesStreamingTest.json")] [InlineData(false, "./CrossLanguage/Data/PromptWithSimpleVariableTest.yaml", "./CrossLanguage/Data/PromptWithSimpleVariableTest.json")] - [InlineData(true, "./CrossLanguage/Data/PromptWithSimpleVariableTest.yaml", "./CrossLanguage/Data/PromptWithSimpleVariableTest.json")] + [InlineData(true, "./CrossLanguage/Data/PromptWithSimpleVariableTest.yaml", "./CrossLanguage/Data/PromptWithSimpleVariableStreamingTest.json")] public async Task YamlPromptAsync(bool isStreaming, string promptPath, string expectedResultPath) { using var kernelProvider = new KernelRequestTracer(); diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj index 5a818e75e824..5686e8e3e96e 100644 --- a/dotnet/src/IntegrationTests/IntegrationTests.csproj +++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj @@ -23,12 +23,17 @@ + + + + + @@ -105,24 +110,39 @@ PreserveNewest + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + PreserveNewest + + PreserveNewest + PreserveNewest + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + PreserveNewest From dd86e713ab28fa464e863eef83adf58d25ec9cf7 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:07:28 +0100 Subject: [PATCH 81/87] .Net: Prevent assigning a nullable variable to a non-nullable property (#8492) --- .../Core/AzureClientCore.ChatCompletion.cs | 5 ++++- .../Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs index 5deddb7e5646..1f68ada62532 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs @@ -55,7 +55,10 @@ protected override ChatCompletionOptions CreateChatCompletionOptions( options.ResponseFormat = responseFormat; } - options.ToolChoice = toolCallingConfig.Choice; + if (toolCallingConfig.Choice is not null) + { + options.ToolChoice = toolCallingConfig.Choice; + } if (toolCallingConfig.Tools is { Count: > 0 } tools) { diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs index 3bc984478a5d..1c2269a9f966 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs @@ -655,7 +655,10 @@ protected virtual ChatCompletionOptions CreateChatCompletionOptions( options.ResponseFormat = responseFormat; } - options.ToolChoice = toolCallingConfig.Choice; + if (toolCallingConfig.Choice is not null) + { + options.ToolChoice = toolCallingConfig.Choice; + } if (toolCallingConfig.Tools is { Count: > 0 } tools) { From ec49b500dbdb54b509963d01b8c19d098cf29745 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:29:56 +0100 Subject: [PATCH 82/87] Python: .Net OpenAI V2 MergeFix for GA (#8486) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Motivation and Context - Merge conflicts fix --------- Signed-off-by: dependabot[bot] Co-authored-by: Dr. Artificial曾小健 <875100501@qq.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Co-authored-by: Tao Chen Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Co-authored-by: Maurycy Markowski Co-authored-by: gparmigiani Co-authored-by: Atiqur Rahman Foyshal <113086917+atiq-bs23@users.noreply.github.com> Co-authored-by: Eduard van Valkenburg Co-authored-by: Andrew Desousa <33275002+andrewldesousa@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Marcelo Garcia <129542431+MarceloAGG@users.noreply.github.com> Co-authored-by: Marcelo Garcia 🛸 Co-authored-by: Eirik Tsarpalis --- .github/workflows/python-build.yml | 30 + dotnet/Directory.Packages.props | 2 +- .../HandlebarsVisionPrompts.cs | 51 + dotnet/samples/Concepts/README.md | 3 +- .../src/Schema/JsonSchemaGenerationContext.cs | 98 ++ .../JsonSchemaMapper.ReflectionHelpers.cs | 407 -------- .../JsonSchemaMapper.STJv8.JsonSchema.cs | 559 ++++++++++ .../src/Schema/JsonSchemaMapper.STJv8.cs | 952 ++++++++++++++++++ .../src/Schema/JsonSchemaMapper.STJv9.cs | 186 ++++ .../src/Schema/JsonSchemaMapper.cs | 876 ++++------------ .../Schema/JsonSchemaMapperConfiguration.cs | 57 +- .../src/Schema/KernelJsonSchemaBuilder.cs | 25 +- .../InternalUtilities/src/Schema/README.md | 2 +- .../src/Schema/ReferenceTypeNullability.cs | 30 - .../AI/ChatCompletion/ChatPromptParser.cs | 9 +- .../Prompt/ChatPromptParserTests.cs | 49 + python/.pre-commit-config.yaml | 2 +- python/.vscode/tasks.json | 2 +- python/pyproject.toml | 1 + .../getting_started/00-getting-started.ipynb | 2 +- .../05-using-the-planner.ipynb | 2 +- python/semantic_kernel/__init__.py | 2 +- .../contents/chat_message_content.py | 17 +- .../functions/kernel_arguments.py | 6 + python/semantic_kernel/kernel.py | 8 +- .../test_anthropic_chat_completion.py | 135 ++- .../contents/test_chat_message_content.py | 92 +- .../unit/functions/test_kernel_arguments.py | 11 + 28 files changed, 2404 insertions(+), 1212 deletions(-) create mode 100644 .github/workflows/python-build.yml create mode 100644 dotnet/samples/Concepts/PromptTemplates/HandlebarsVisionPrompts.cs create mode 100644 dotnet/src/InternalUtilities/src/Schema/JsonSchemaGenerationContext.cs delete mode 100644 dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.ReflectionHelpers.cs create mode 100644 dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.JsonSchema.cs create mode 100644 dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.cs create mode 100644 dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv9.cs delete mode 100644 dotnet/src/InternalUtilities/src/Schema/ReferenceTypeNullability.cs diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml new file mode 100644 index 000000000000..6fbb810bae2f --- /dev/null +++ b/.github/workflows/python-build.yml @@ -0,0 +1,30 @@ +name: Python Build Assets + +on: + release: + types: [published] + +jobs: + python-build-assets: + if: github.event_name == 'release' && startsWith(github.event.release.tag_name, 'python-') + name: Python Build Assets and add to Release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Set up uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Check version + run: | + echo "Building and uploading Python package version: ${{ github.event.release.tag_name }}" + - name: Build the package + run: cd python && make build + - name: Release + uses: softprops/action-gh-release@v2 + with: + files: | + python/dist/* diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 50ef0a16d455..75d19fe11d0b 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -19,7 +19,7 @@ - + diff --git a/dotnet/samples/Concepts/PromptTemplates/HandlebarsVisionPrompts.cs b/dotnet/samples/Concepts/PromptTemplates/HandlebarsVisionPrompts.cs new file mode 100644 index 000000000000..195d281da570 --- /dev/null +++ b/dotnet/samples/Concepts/PromptTemplates/HandlebarsVisionPrompts.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.PromptTemplates.Handlebars; + +namespace PromptTemplates; + +// This example shows how to use chat completion handlebars template prompts with base64 encoded images as a parameter. +public class HandlebarsVisionPrompts(ITestOutputHelper output) : BaseTest(output) +{ + [Fact] + public async Task RunAsync() + { + const string HandlebarsTemplate = """ + You are an AI assistant designed to help with image recognition tasks. + + {{request}} + {{imageData}} + + """; + + var kernel = Kernel.CreateBuilder() + .AddOpenAIChatCompletion( + modelId: TestConfiguration.OpenAI.ChatModelId, + apiKey: TestConfiguration.OpenAI.ApiKey) + .Build(); + + var templateFactory = new HandlebarsPromptTemplateFactory(); + var promptTemplateConfig = new PromptTemplateConfig() + { + Template = HandlebarsTemplate, + TemplateFormat = "handlebars", + Name = "Vision_Chat_Prompt", + }; + var function = kernel.CreateFunctionFromPrompt(promptTemplateConfig, templateFactory); + + var arguments = new KernelArguments(new Dictionary + { + {"request","Describe this image:"}, + {"imageData", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAACVJREFUKFNj/KTO/J+BCMA4iBUyQX1A0I10VAizCj1oMdyISyEAFoQbHwTcuS8AAAAASUVORK5CYII="} + }); + + var response = await kernel.InvokeAsync(function, arguments); + Console.WriteLine(response); + + /* + Output: + The image is a solid block of bright red color. There are no additional features, shapes, or textures present. + */ + } +} diff --git a/dotnet/samples/Concepts/README.md b/dotnet/samples/Concepts/README.md index 26eef28982a7..937d832dfcba 100644 --- a/dotnet/samples/Concepts/README.md +++ b/dotnet/samples/Concepts/README.md @@ -142,7 +142,8 @@ Down below you can find the code snippets that demonstrate the usage of many Sem - [MultiplePromptTemplates](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/PromptTemplates/MultiplePromptTemplates.cs) - [PromptFunctionsWithChatGPT](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/PromptTemplates/PromptFunctionsWithChatGPT.cs) - [TemplateLanguage](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/PromptTemplates/TemplateLanguage.cs) -- [PromptyFunction](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/PromptYemplates/PromptyFunction.cs) +- [PromptyFunction](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/PromptTemplates/PromptyFunction.cs) +- [HandlebarsVisionPrompts](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/PromptTemplates/HandlebarsVisionPrompts.cs) ## RAG - Retrieval-Augmented Generation diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaGenerationContext.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaGenerationContext.cs new file mode 100644 index 000000000000..870013bb2bbe --- /dev/null +++ b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaGenerationContext.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization.Metadata; + +namespace JsonSchemaMapper; + +/// +/// Defines the context in which a JSON schema within a type graph is being generated. +/// +#if EXPOSE_JSON_SCHEMA_MAPPER +public +#else +internal +#endif + readonly struct JsonSchemaGenerationContext +{ + internal JsonSchemaGenerationContext( + JsonTypeInfo typeInfo, + Type? declaringType, + JsonPropertyInfo? propertyInfo, + ParameterInfo? parameterInfo, + ICustomAttributeProvider? propertyAttributeProvider) + { + TypeInfo = typeInfo; + DeclaringType = declaringType; + PropertyInfo = propertyInfo; + ParameterInfo = parameterInfo; + PropertyAttributeProvider = propertyAttributeProvider; + } + + /// + /// The for the type being processed. + /// + public JsonTypeInfo TypeInfo { get; } + + /// + /// The declaring type of the property or parameter being processed. + /// + public Type? DeclaringType { get; } + + /// + /// The if the schema is being generated for a property. + /// + public JsonPropertyInfo? PropertyInfo { get; } + + /// + /// The if a constructor parameter + /// has been associated with the accompanying . + /// + public ParameterInfo? ParameterInfo { get; } + + /// + /// The corresponding to the property or field being processed. + /// + public ICustomAttributeProvider? PropertyAttributeProvider { get; } + + /// + /// Checks if the type, property, or parameter has the specified attribute applied. + /// + /// The type of the attribute to resolve. + /// Whether to look up the hierarchy chain for the inherited custom attribute. + /// True if the attribute is defined by the current context. + public bool IsDefined(bool inherit = false) + where TAttribute : Attribute => + GetCustomAttributes(typeof(TAttribute), inherit).Any(); + + /// + /// Checks if the type, property, or parameter has the specified attribute applied. + /// + /// The type of the attribute to resolve. + /// Whether to look up the hierarchy chain for the inherited custom attribute. + /// The first attribute resolved from the current context, or null. + public TAttribute? GetAttribute(bool inherit = false) + where TAttribute : Attribute => + (TAttribute?)GetCustomAttributes(typeof(TAttribute), inherit).FirstOrDefault(); + + /// + /// Resolves any custom attributes that might have been applied to the type, property, or parameter. + /// + /// The attribute type to resolve. + /// Whether to look up the hierarchy chain for the inherited custom attribute. + /// An enumerable of all custom attributes defined by the context. + public IEnumerable GetCustomAttributes(Type type, bool inherit = false) + { + // Resolves attributes starting from the property, then the parameter, and finally the type itself. + return GetAttrs(PropertyAttributeProvider) + .Concat(GetAttrs(ParameterInfo)) + .Concat(GetAttrs(TypeInfo.Type)) + .Cast(); + + object[] GetAttrs(ICustomAttributeProvider? provider) => + provider?.GetCustomAttributes(type, inherit) ?? Array.Empty(); + } +} diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.ReflectionHelpers.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.ReflectionHelpers.cs deleted file mode 100644 index 11dc0c6d85b7..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.ReflectionHelpers.cs +++ /dev/null @@ -1,407 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; - -namespace JsonSchemaMapper; - -#if EXPOSE_JSON_SCHEMA_MAPPER - public -#else -internal -#endif -static partial class JsonSchemaMapper -{ - // Uses reflection to determine the element type of an enumerable or dictionary type - // Workaround for https://github.com/dotnet/runtime/issues/77306#issuecomment-2007887560 - private static Type GetElementType(JsonTypeInfo typeInfo) - { - Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary); - return (Type)typeof(JsonTypeInfo).GetProperty("ElementType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(typeInfo)!; - } - - // The source generator currently doesn't populate attribute providers for properties - // cf. https://github.com/dotnet/runtime/issues/100095 - // Work around the issue by running a query for the relevant MemberInfo using the internal MemberName property - // https://github.com/dotnet/runtime/blob/de774ff9ee1a2c06663ab35be34b755cd8d29731/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs#L206 -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", - Justification = "We're reading the internal JsonPropertyInfo.MemberName which cannot have been trimmed away.")] -#endif - private static ICustomAttributeProvider? ResolveAttributeProvider(JsonTypeInfo typeInfo, JsonPropertyInfo propertyInfo) - { - if (propertyInfo.AttributeProvider is { } provider) - { - return provider; - } - - PropertyInfo memberNameProperty = typeof(JsonPropertyInfo).GetProperty("MemberName", BindingFlags.Instance | BindingFlags.NonPublic)!; - var memberName = (string?)memberNameProperty.GetValue(propertyInfo); - if (memberName is not null) - { - return typeInfo.Type.GetMember(memberName, MemberTypes.Property | MemberTypes.Field, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(); - } - - return null; - } - - // Uses reflection to determine any custom converters specified for the element of a nullable type. -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2026", - Justification = "We're resolving private fields of the built-in Nullable converter which cannot have been trimmed away.")] -#endif - private static JsonConverter? ExtractCustomNullableConverter(JsonConverter? converter) - { - Debug.Assert(converter is null || IsBuiltInConverter(converter)); - - // There is unfortunately no way in which we can obtain the element converter from a nullable converter without resorting to private reflection - // https://github.com/dotnet/runtime/blob/5fda47434cecc590095e9aef3c4e560b7b7ebb47/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs#L15-L17 - Type? converterType = converter?.GetType(); - if (converterType?.Name == "NullableConverter`1") - { - FieldInfo elementConverterField = converterType.GetPrivateFieldWithPotentiallyTrimmedMetadata("_elementConverter"); - return (JsonConverter)elementConverterField!.GetValue(converter)!; - } - - return null; - } - - // Uses reflection to determine serialization configuration for enum types - // cf. https://github.com/dotnet/runtime/blob/5fda47434cecc590095e9aef3c4e560b7b7ebb47/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs#L23-L25 -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2026", - Justification = "We're resolving private fields of the built-in enum converter which cannot have been trimmed away.")] -#endif - private static bool TryGetStringEnumConverterValues(JsonTypeInfo typeInfo, JsonConverter converter, out JsonArray? values) - { - Debug.Assert(typeInfo.Type.IsEnum && IsBuiltInConverter(converter)); - - if (converter is JsonConverterFactory factory) - { - converter = factory.CreateConverter(typeInfo.Type, typeInfo.Options)!; - } - - Type converterType = converter.GetType(); - FieldInfo converterOptionsField = converterType.GetPrivateFieldWithPotentiallyTrimmedMetadata("_converterOptions"); - FieldInfo namingPolicyField = converterType.GetPrivateFieldWithPotentiallyTrimmedMetadata("_namingPolicy"); - - const int EnumConverterOptionsAllowStrings = 1; - var converterOptions = (int)converterOptionsField!.GetValue(converter)!; - if ((converterOptions & EnumConverterOptionsAllowStrings) != 0) - { - if (typeInfo.Type.GetCustomAttribute() is not null) - { - // For enums implemented as flags do not surface values in the JSON schema. - values = null; - } - else - { - var namingPolicy = (JsonNamingPolicy?)namingPolicyField!.GetValue(converter)!; - string[] names = Enum.GetNames(typeInfo.Type); - values = []; - foreach (string name in names) - { - string effectiveName = namingPolicy?.ConvertName(name) ?? name; - values.Add((JsonNode)effectiveName); - } - } - - return true; - } - - values = null; - return false; - } - -#if NETCOREAPP - [RequiresUnreferencedCode("Resolves unreferenced member metadata.")] -#endif - private static FieldInfo GetPrivateFieldWithPotentiallyTrimmedMetadata(this Type type, string fieldName) => - type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic) ?? - throw new InvalidOperationException( - $"Could not resolve metadata for field '{fieldName}' in type '{type}'. " + - "If running Native AOT ensure that the 'IlcTrimMetadata' property has been disabled."); - - // Resolves the parameters of the deserialization constructor for a type, if they exist. -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", - Justification = "The deserialization constructor should have already been referenced by the source generator and therefore will not have been trimmed.")] -#endif - private static Func ResolveJsonConstructorParameterMapper(JsonTypeInfo typeInfo) - { - Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object); - - if (typeInfo.Properties.Count > 0 && - typeInfo.CreateObject is null && // Ensure that a default constructor isn't being used - typeInfo.Type.TryGetDeserializationConstructor(useDefaultCtorInAnnotatedStructs: true, out ConstructorInfo? ctor)) - { - ParameterInfo[]? parameters = ctor?.GetParameters(); - if (parameters?.Length > 0) - { - Dictionary dict = new(parameters.Length); - foreach (ParameterInfo parameter in parameters) - { - if (parameter.Name is not null) - { - // We don't care about null parameter names or conflicts since they - // would have already been rejected by JsonTypeInfo configuration. - dict[new(parameter.Name, parameter.ParameterType)] = parameter; - } - } - - return prop => dict.TryGetValue(new(prop.Name, prop.PropertyType), out ParameterInfo? parameter) ? parameter : null; - } - } - - return static _ => null; - } - - // Parameter to property matching semantics as declared in - // https://github.com/dotnet/runtime/blob/12d96ccfaed98e23c345188ee08f8cfe211c03e7/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs#L1007-L1030 - private readonly struct ParameterLookupKey : IEquatable - { - public ParameterLookupKey(string name, Type type) - { - Name = name; - Type = type; - } - - public string Name { get; } - public Type Type { get; } - - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Name); - public bool Equals(ParameterLookupKey other) => Type == other.Type && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase); - public override bool Equals(object? obj) => obj is ParameterLookupKey key && Equals(key); - } - - // Resolves the deserialization constructor for a type using logic copied from - // https://github.com/dotnet/runtime/blob/e12e2fa6cbdd1f4b0c8ad1b1e2d960a480c21703/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs#L227-L286 - private static bool TryGetDeserializationConstructor( -#if NETCOREAPP - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] -#endif - this Type type, - bool useDefaultCtorInAnnotatedStructs, - out ConstructorInfo? deserializationCtor) - { - ConstructorInfo? ctorWithAttribute = null; - ConstructorInfo? publicParameterlessCtor = null; - ConstructorInfo? lonePublicCtor = null; - - ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); - - if (constructors.Length == 1) - { - lonePublicCtor = constructors[0]; - } - - foreach (ConstructorInfo constructor in constructors) - { - if (HasJsonConstructorAttribute(constructor)) - { - if (ctorWithAttribute is not null) - { - deserializationCtor = null; - return false; - } - - ctorWithAttribute = constructor; - } - else if (constructor.GetParameters().Length == 0) - { - publicParameterlessCtor = constructor; - } - } - - // Search for non-public ctors with [JsonConstructor]. - foreach (ConstructorInfo constructor in type.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)) - { - if (HasJsonConstructorAttribute(constructor)) - { - if (ctorWithAttribute is not null) - { - deserializationCtor = null; - return false; - } - - ctorWithAttribute = constructor; - } - } - - // Structs will use default constructor if attribute isn't used. - if (useDefaultCtorInAnnotatedStructs && type.IsValueType && ctorWithAttribute is null) - { - deserializationCtor = null; - return true; - } - - deserializationCtor = ctorWithAttribute ?? publicParameterlessCtor ?? lonePublicCtor; - return true; - - static bool HasJsonConstructorAttribute(ConstructorInfo constructorInfo) => - constructorInfo.GetCustomAttribute() is not null; - } - - private static bool IsBuiltInConverter(JsonConverter converter) => - converter.GetType().Assembly == typeof(JsonConverter).Assembly; - - // Resolves the nullable reference type annotations for a property or field, - // additionally addressing a few known bugs of the NullabilityInfo pre .NET 9. - private static NullabilityInfo GetMemberNullability(this NullabilityInfoContext context, MemberInfo memberInfo) - { - Debug.Assert(memberInfo is PropertyInfo or FieldInfo); - return memberInfo is PropertyInfo prop - ? context.Create(prop) - : context.Create((FieldInfo)memberInfo); - } - - private static NullabilityState GetParameterNullability(this NullabilityInfoContext context, ParameterInfo parameterInfo) - { - // Workaround for https://github.com/dotnet/runtime/issues/92487 - if (parameterInfo.GetGenericParameterDefinition() is { ParameterType: { IsGenericParameter: true } typeParam }) - { - // Step 1. Look for nullable annotations on the type parameter. - if (GetNullableFlags(typeParam) is byte[] flags) - { - return TranslateByte(flags[0]); - } - - // Step 2. Look for nullable annotations on the generic method declaration. - if (typeParam.DeclaringMethod is not null && GetNullableContextFlag(typeParam.DeclaringMethod) is byte flag) - { - return TranslateByte(flag); - } - - // Step 3. Look for nullable annotations on the generic method declaration. - if (GetNullableContextFlag(typeParam.DeclaringType!) is byte flag2) - { - return TranslateByte(flag2); - } - - // Default to nullable. - return NullabilityState.Nullable; - -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", - Justification = "We're resolving private fields of the built-in enum converter which cannot have been trimmed away.")] -#endif - static byte[]? GetNullableFlags(MemberInfo member) - { - Attribute? attr = member.GetCustomAttributes().FirstOrDefault(attr => - { - Type attrType = attr.GetType(); - return attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableAttribute"; - }); - - return (byte[])attr?.GetType().GetField("NullableFlags")?.GetValue(attr)!; - } - -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", - Justification = "We're resolving private fields of the built-in enum converter which cannot have been trimmed away.")] -#endif - static byte? GetNullableContextFlag(MemberInfo member) - { - Attribute? attr = member.GetCustomAttributes().FirstOrDefault(attr => - { - Type attrType = attr.GetType(); - return attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableContextAttribute"; - }); - - return (byte?)attr?.GetType().GetField("Flag")?.GetValue(attr)!; - } - - static NullabilityState TranslateByte(byte b) => - b switch - { - 1 => NullabilityState.NotNull, - 2 => NullabilityState.Nullable, - _ => NullabilityState.Unknown - }; - } - - return context.Create(parameterInfo).WriteState; - } - - private static ParameterInfo GetGenericParameterDefinition(this ParameterInfo parameter) - { - if (parameter.Member is { DeclaringType.IsConstructedGenericType: true } - or MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false }) - { - var genericMethod = (MethodBase)parameter.Member.GetGenericMemberDefinition()!; - return genericMethod.GetParameters()[parameter.Position]; - } - - return parameter; - } - -#if NETCOREAPP - [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", - Justification = "Looking up the generic member definition of the provided member.")] -#endif - private static MemberInfo GetGenericMemberDefinition(this MemberInfo member) - { - if (member is Type type) - { - return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type; - } - - if (member.DeclaringType!.IsConstructedGenericType) - { - const BindingFlags AllMemberFlags = - BindingFlags.Static | BindingFlags.Instance | - BindingFlags.Public | BindingFlags.NonPublic; - - return member.DeclaringType.GetGenericTypeDefinition() - .GetMember(member.Name, AllMemberFlags) - .First(m => m.MetadataToken == member.MetadataToken); - } - - if (member is MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false } method) - { - return method.GetGenericMethodDefinition(); - } - - return member; - } - - // Taken from https://github.com/dotnet/runtime/blob/903bc019427ca07080530751151ea636168ad334/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs#L288-L317 - private static object? GetNormalizedDefaultValue(this ParameterInfo parameterInfo) - { - Type parameterType = parameterInfo.ParameterType; - object? defaultValue = parameterInfo.DefaultValue; - - if (defaultValue is null) - { - return null; - } - - // DBNull.Value is sometimes used as the default value (returned by reflection) of nullable params in place of null. - if (defaultValue == DBNull.Value && parameterType != typeof(DBNull)) - { - return null; - } - - // Default values of enums or nullable enums are represented using the underlying type and need to be cast explicitly - // cf. https://github.com/dotnet/runtime/issues/68647 - if (parameterType.IsEnum) - { - return Enum.ToObject(parameterType, defaultValue); - } - - if (Nullable.GetUnderlyingType(parameterType) is Type underlyingType && underlyingType.IsEnum) - { - return Enum.ToObject(underlyingType, defaultValue); - } - - return defaultValue; - } -} diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.JsonSchema.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.JsonSchema.cs new file mode 100644 index 000000000000..0119d2a816fd --- /dev/null +++ b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.JsonSchema.cs @@ -0,0 +1,559 @@ +// Copyright (c) Microsoft. All rights reserved. + +#if !NET9_0_OR_GREATER && !SYSTEM_TEXT_JSON_V9 +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Text.Json.Nodes; + +namespace JsonSchemaMapper; + +#if EXPOSE_JSON_SCHEMA_MAPPER +public +#else +internal +#endif + static partial class JsonSchemaMapper +{ + // Simple JSON schema representation taken from System.Text.Json + // https://github.com/dotnet/runtime/blob/50d6cad649aad2bfa4069268eddd16fd51ec5cf3/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs + private sealed class JsonSchema + { + public static JsonSchema False { get; } = new(false); + public static JsonSchema True { get; } = new(true); + + public JsonSchema() + { + } + + private JsonSchema(bool trueOrFalse) + { + _trueOrFalse = trueOrFalse; + } + + public bool IsTrue => _trueOrFalse is true; + public bool IsFalse => _trueOrFalse is false; + private readonly bool? _trueOrFalse; + + public string? Schema + { + get => _schema; + set + { + VerifyMutable(); + _schema = value; + } + } + + private string? _schema; + + public string? Title + { + get => _title; + set + { + VerifyMutable(); + _title = value; + } + } + + private string? _title; + + public string? Description + { + get => _description; + set + { + VerifyMutable(); + _description = value; + } + } + + private string? _description; + + public string? Ref + { + get => _ref; + set + { + VerifyMutable(); + _ref = value; + } + } + + private string? _ref; + + public string? Comment + { + get => _comment; + set + { + VerifyMutable(); + _comment = value; + } + } + + private string? _comment; + + public JsonSchemaType Type + { + get => _type; + set + { + VerifyMutable(); + _type = value; + } + } + + private JsonSchemaType _type = JsonSchemaType.Any; + + public string? Format + { + get => _format; + set + { + VerifyMutable(); + _format = value; + } + } + + private string? _format; + + public string? Pattern + { + get => _pattern; + set + { + VerifyMutable(); + _pattern = value; + } + } + + private string? _pattern; + + public JsonNode? Constant + { + get => _constant; + set + { + VerifyMutable(); + _constant = value; + } + } + + private JsonNode? _constant; + + public List>? Properties + { + get => _properties; + set + { + VerifyMutable(); + _properties = value; + } + } + + private List>? _properties; + + public List? Required + { + get => _required; + set + { + VerifyMutable(); + _required = value; + } + } + + private List? _required; + + public JsonSchema? Items + { + get => _items; + set + { + VerifyMutable(); + _items = value; + } + } + + private JsonSchema? _items; + + public JsonSchema? AdditionalProperties + { + get => _additionalProperties; + set + { + VerifyMutable(); + _additionalProperties = value; + } + } + + private JsonSchema? _additionalProperties; + + public JsonArray? Enum + { + get => _enum; + set + { + VerifyMutable(); + _enum = value; + } + } + + private JsonArray? _enum; + + public JsonSchema? Not + { + get => _not; + set + { + VerifyMutable(); + _not = value; + } + } + + private JsonSchema? _not; + + public List? AnyOf + { + get => _anyOf; + set + { + VerifyMutable(); + _anyOf = value; + } + } + + private List? _anyOf; + + public bool HasDefaultValue + { + get => _hasDefaultValue; + set + { + VerifyMutable(); + _hasDefaultValue = value; + } + } + + private bool _hasDefaultValue; + + public JsonNode? DefaultValue + { + get => _defaultValue; + set + { + VerifyMutable(); + _defaultValue = value; + } + } + + private JsonNode? _defaultValue; + + public int? MinLength + { + get => _minLength; + set + { + VerifyMutable(); + _minLength = value; + } + } + + private int? _minLength; + + public int? MaxLength + { + get => _maxLength; + set + { + VerifyMutable(); + _maxLength = value; + } + } + + private int? _maxLength; + + public JsonSchemaGenerationContext? GenerationContext { get; set; } + + public int KeywordCount + { + get + { + if (_trueOrFalse != null) + { + return 0; + } + + int count = 0; + Count(Schema != null); + Count(Ref != null); + Count(Comment != null); + Count(Title != null); + Count(Description != null); + Count(Type != JsonSchemaType.Any); + Count(Format != null); + Count(Pattern != null); + Count(Constant != null); + Count(Properties != null); + Count(Required != null); + Count(Items != null); + Count(AdditionalProperties != null); + Count(Enum != null); + Count(Not != null); + Count(AnyOf != null); + Count(HasDefaultValue); + Count(MinLength != null); + Count(MaxLength != null); + + return count; + + void Count(bool isKeywordSpecified) + { + count += isKeywordSpecified ? 1 : 0; + } + } + } + + public void MakeNullable() + { + if (_trueOrFalse != null) + { + return; + } + + if (Type != JsonSchemaType.Any) + { + Type |= JsonSchemaType.Null; + } + } + + public JsonNode ToJsonNode(JsonSchemaMapperConfiguration options) + { + if (_trueOrFalse is { } boolSchema) + { + return CompleteSchema((JsonNode)boolSchema); + } + + var objSchema = new JsonObject(); + + if (Schema != null) + { + objSchema.Add(JsonSchemaConstants.SchemaPropertyName, Schema); + } + + if (Title != null) + { + objSchema.Add(JsonSchemaConstants.TitlePropertyName, Title); + } + + if (Description != null) + { + objSchema.Add(JsonSchemaConstants.DescriptionPropertyName, Description); + } + + if (Ref != null) + { + objSchema.Add(JsonSchemaConstants.RefPropertyName, Ref); + } + + if (Comment != null) + { + objSchema.Add(JsonSchemaConstants.CommentPropertyName, Comment); + } + + if (MapSchemaType(Type) is JsonNode type) + { + objSchema.Add(JsonSchemaConstants.TypePropertyName, type); + } + + if (Format != null) + { + objSchema.Add(JsonSchemaConstants.FormatPropertyName, Format); + } + + if (Pattern != null) + { + objSchema.Add(JsonSchemaConstants.PatternPropertyName, Pattern); + } + + if (Constant != null) + { + objSchema.Add(JsonSchemaConstants.ConstPropertyName, Constant); + } + + if (Properties != null) + { + var properties = new JsonObject(); + foreach (KeyValuePair property in Properties) + { + properties.Add(property.Key, property.Value.ToJsonNode(options)); + } + + objSchema.Add(JsonSchemaConstants.PropertiesPropertyName, properties); + } + + if (Required != null) + { + var requiredArray = new JsonArray(); + foreach (string requiredProperty in Required) + { + requiredArray.Add((JsonNode)requiredProperty); + } + + objSchema.Add(JsonSchemaConstants.RequiredPropertyName, requiredArray); + } + + if (Items != null) + { + objSchema.Add(JsonSchemaConstants.ItemsPropertyName, Items.ToJsonNode(options)); + } + + if (AdditionalProperties != null) + { + objSchema.Add(JsonSchemaConstants.AdditionalPropertiesPropertyName, AdditionalProperties.ToJsonNode(options)); + } + + if (Enum != null) + { + objSchema.Add(JsonSchemaConstants.EnumPropertyName, Enum); + } + + if (Not != null) + { + objSchema.Add(JsonSchemaConstants.NotPropertyName, Not.ToJsonNode(options)); + } + + if (AnyOf != null) + { + JsonArray anyOfArray = new(); + foreach (JsonSchema schema in AnyOf) + { + anyOfArray.Add(schema.ToJsonNode(options)); + } + + objSchema.Add(JsonSchemaConstants.AnyOfPropertyName, anyOfArray); + } + + if (HasDefaultValue) + { + objSchema.Add(JsonSchemaConstants.DefaultPropertyName, DefaultValue); + } + + if (MinLength is int minLength) + { + objSchema.Add(JsonSchemaConstants.MinLengthPropertyName, (JsonNode)minLength); + } + + if (MaxLength is int maxLength) + { + objSchema.Add(JsonSchemaConstants.MaxLengthPropertyName, (JsonNode)maxLength); + } + + return CompleteSchema(objSchema); + + JsonNode CompleteSchema(JsonNode schema) + { + if (GenerationContext is { } context) + { + Debug.Assert(options.TransformSchemaNode != null, "context should only be populated if a callback is present."); + + // Apply any user-defined transformations to the schema. + return options.TransformSchemaNode!(context, schema); + } + + return schema; + } + } + + public static void EnsureMutable(ref JsonSchema schema) + { + switch (schema._trueOrFalse) + { + case false: + schema = new JsonSchema { Not = JsonSchema.True }; + break; + case true: + schema = new JsonSchema(); + break; + } + } + + private static readonly JsonSchemaType[] s_schemaValues = new JsonSchemaType[] + { + // NB the order of these values influences order of types in the rendered schema + JsonSchemaType.String, + JsonSchemaType.Integer, + JsonSchemaType.Number, + JsonSchemaType.Boolean, + JsonSchemaType.Array, + JsonSchemaType.Object, + JsonSchemaType.Null, + }; + + private void VerifyMutable() + { + Debug.Assert(_trueOrFalse is null, "Schema is not mutable"); + if (_trueOrFalse is not null) + { + Throw(); + static void Throw() => throw new InvalidOperationException(); + } + } + + private static JsonNode? MapSchemaType(JsonSchemaType schemaType) + { + if (schemaType is JsonSchemaType.Any) + { + return null; + } + + if (ToIdentifier(schemaType) is string identifier) + { + return identifier; + } + + var array = new JsonArray(); + foreach (JsonSchemaType type in s_schemaValues) + { + if ((schemaType & type) != 0) + { + array.Add((JsonNode)ToIdentifier(type)!); + } + } + + return array; + + static string? ToIdentifier(JsonSchemaType schemaType) + { + return schemaType switch + { + JsonSchemaType.Null => "null", + JsonSchemaType.Boolean => "boolean", + JsonSchemaType.Integer => "integer", + JsonSchemaType.Number => "number", + JsonSchemaType.String => "string", + JsonSchemaType.Array => "array", + JsonSchemaType.Object => "object", + _ => null, + }; + } + } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + private enum JsonSchemaType + { + Any = 0, // No type declared on the schema + Null = 1, + Boolean = 2, + Integer = 4, + Number = 8, + String = 16, + Array = 32, + Object = 64, + } +} +#endif diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.cs new file mode 100644 index 000000000000..afce5e8e85d7 --- /dev/null +++ b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv8.cs @@ -0,0 +1,952 @@ +// Copyright (c) Microsoft. All rights reserved. + +#if !NET9_0_OR_GREATER && !SYSTEM_TEXT_JSON_V9 +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; + +namespace JsonSchemaMapper; + +#if EXPOSE_JSON_SCHEMA_MAPPER +public +#else +internal +#endif + static partial class JsonSchemaMapper +{ + // For System.Text.Json versions prior to v9, JsonSchemaMapper is implemented as a standalone component. + // The implementation uses private reflection to access metadata not available with the older APIs of STJ. + // While the implementation is forward compatible with .NET 9, it is not guaranteed that it will work with + // later versions of .NET and users are encouraged to switch to the built-in JsonSchemaExporter eventually. + + private static partial JsonNode MapRootTypeJsonSchema(JsonTypeInfo typeInfo, JsonSchemaMapperConfiguration configuration) + { + GenerationState state = new(configuration, typeInfo.Options); + JsonSchema schema = MapJsonSchemaCore(ref state, typeInfo); + return schema.ToJsonNode(configuration); + } + + private static partial JsonNode MapMethodParameterJsonSchema( + ParameterInfo parameterInfo, + JsonTypeInfo parameterTypeInfo, + JsonSchemaMapperConfiguration configuration, + NullabilityInfoContext nullabilityContext, + out bool isRequired) + { + Debug.Assert(parameterInfo.Name != null); + + GenerationState state = new(configuration, parameterTypeInfo.Options, nullabilityContext); + + string? parameterDescription = null; + isRequired = false; + + ResolveParameterInfo( + parameterInfo, + parameterTypeInfo, + state.NullabilityInfoContext, + state.Configuration, + out bool hasDefaultValue, + out JsonNode? defaultValue, + out bool isNonNullableType, + ref parameterDescription, + ref isRequired); + + state.PushSchemaNode(JsonSchemaConstants.PropertiesPropertyName); + state.PushSchemaNode(parameterInfo.Name!); + + JsonSchema paramSchema = MapJsonSchemaCore( + ref state, + parameterTypeInfo, + parameterInfo: parameterInfo, + description: parameterDescription, + isNonNullableType: isNonNullableType); + + if (hasDefaultValue) + { + paramSchema.DefaultValue = defaultValue; + paramSchema.HasDefaultValue = true; + } + + state.PopSchemaNode(); + state.PopSchemaNode(); + + return paramSchema.ToJsonNode(configuration); + } + + private static JsonSchema MapJsonSchemaCore( + ref GenerationState state, + JsonTypeInfo typeInfo, + Type? parentType = null, + JsonPropertyInfo? propertyInfo = null, + ICustomAttributeProvider? propertyAttributeProvider = null, + ParameterInfo? parameterInfo = null, + bool isNonNullableType = false, + JsonConverter? customConverter = null, + JsonNumberHandling? customNumberHandling = null, + JsonTypeInfo? parentPolymorphicTypeInfo = null, + bool parentPolymorphicTypeContainsTypesWithoutDiscriminator = false, + bool parentPolymorphicTypeIsNonNullable = false, + KeyValuePair? typeDiscriminator = null, + string? description = null, + bool cacheResult = true) + { + Debug.Assert(typeInfo.IsReadOnly); + + if (cacheResult && state.TryPushType(typeInfo, propertyInfo, out string? existingJsonPointer)) + { + // We're generating the schema of a recursive type, return a reference pointing to the outermost schema. + return CompleteSchema(ref state, new JsonSchema { Ref = existingJsonPointer }); + } + + JsonSchema schema; + JsonConverter effectiveConverter = customConverter ?? typeInfo.Converter; + JsonNumberHandling effectiveNumberHandling = customNumberHandling ?? typeInfo.NumberHandling ?? typeInfo.Options.NumberHandling; + + if (!IsBuiltInConverter(effectiveConverter)) + { + // Return a `true` schema for types with user-defined converters. + return CompleteSchema(ref state, JsonSchema.True); + } + + if (state.Configuration.ResolveDescriptionAttributes) + { + description ??= typeInfo.Type.GetCustomAttribute()?.Description; + } + + if (parentPolymorphicTypeInfo is null && typeInfo.PolymorphismOptions is { DerivedTypes.Count: > 0 } polyOptions) + { + // This is the base type of a polymorphic type hierarchy. The schema for this type + // will include an "anyOf" property with the schemas for all derived types. + + string typeDiscriminatorKey = polyOptions.TypeDiscriminatorPropertyName; + List derivedTypes = polyOptions.DerivedTypes.ToList(); + + if (!typeInfo.Type.IsAbstract && !derivedTypes.Any(derived => derived.DerivedType == typeInfo.Type)) + { + // For non-abstract base types that haven't been explicitly configured, + // add a trivial schema to the derived types since we should support it. + derivedTypes.Add(new JsonDerivedType(typeInfo.Type)); + } + + bool containsTypesWithoutDiscriminator = derivedTypes.Exists(static derivedTypes => derivedTypes.TypeDiscriminator is null); + JsonSchemaType schemaType = JsonSchemaType.Any; + List? anyOf = new(derivedTypes.Count); + + state.PushSchemaNode(JsonSchemaConstants.AnyOfPropertyName); + + foreach (JsonDerivedType derivedType in derivedTypes) + { + Debug.Assert(derivedType.TypeDiscriminator is null or int or string); + + KeyValuePair? derivedTypeDiscriminator = null; + if (derivedType.TypeDiscriminator is { } discriminatorValue) + { + JsonNode discriminatorNode = discriminatorValue switch + { + string stringId => (JsonNode)stringId, + _ => (JsonNode)(int)discriminatorValue, + }; + + JsonSchema discriminatorSchema = new() { Constant = discriminatorNode }; + derivedTypeDiscriminator = new(typeDiscriminatorKey, discriminatorSchema); + } + + JsonTypeInfo derivedTypeInfo = typeInfo.Options.GetTypeInfo(derivedType.DerivedType); + + state.PushSchemaNode(anyOf.Count.ToString(CultureInfo.InvariantCulture)); + JsonSchema derivedSchema = MapJsonSchemaCore( + ref state, + derivedTypeInfo, + parentPolymorphicTypeInfo: typeInfo, + typeDiscriminator: derivedTypeDiscriminator, + parentPolymorphicTypeContainsTypesWithoutDiscriminator: containsTypesWithoutDiscriminator, + parentPolymorphicTypeIsNonNullable: isNonNullableType, + cacheResult: false); + + state.PopSchemaNode(); + + // Determine if all derived schemas have the same type. + if (anyOf.Count == 0) + { + schemaType = derivedSchema.Type; + } + else if (schemaType != derivedSchema.Type) + { + schemaType = JsonSchemaType.Any; + } + + anyOf.Add(derivedSchema); + } + + state.PopSchemaNode(); + + if (schemaType is not JsonSchemaType.Any) + { + // If all derived types have the same schema type, we can simplify the schema + // by moving the type keyword to the base schema and removing it from the derived schemas. + foreach (JsonSchema derivedSchema in anyOf) + { + derivedSchema.Type = JsonSchemaType.Any; + + if (derivedSchema.KeywordCount == 0) + { + // if removing the type results in an empty schema, + // remove the anyOf array entirely since it's always true. + anyOf = null; + break; + } + } + } + + schema = new() + { + Type = schemaType, + AnyOf = anyOf, + + // If all derived types have a discriminator, we can require it in the base schema. + Required = containsTypesWithoutDiscriminator ? null : new() { typeDiscriminatorKey }, + }; + + return CompleteSchema(ref state, schema); + } + + if (Nullable.GetUnderlyingType(typeInfo.Type) is Type nullableElementType) + { + JsonTypeInfo elementTypeInfo = typeInfo.Options.GetTypeInfo(nullableElementType); + customConverter = ExtractCustomNullableConverter(customConverter); + schema = MapJsonSchemaCore(ref state, elementTypeInfo, customConverter: customConverter, cacheResult: false); + + if (schema.Enum != null) + { + Debug.Assert(elementTypeInfo.Type.IsEnum, "The enum keyword should only be populated by schemas for enum types."); + schema.Enum.Add(null); // Append null to the enum array. + } + + return CompleteSchema(ref state, schema); + } + + switch (typeInfo.Kind) + { + case JsonTypeInfoKind.Object: + List>? properties = null; + List? required = null; + JsonSchema? additionalProperties = null; + + if (typeInfo.UnmappedMemberHandling is JsonUnmappedMemberHandling.Disallow) + { + // Disallow unspecified properties. + additionalProperties = JsonSchema.False; + } + + if (typeDiscriminator is { } typeDiscriminatorPair) + { + (properties ??= new()).Add(typeDiscriminatorPair); + if (parentPolymorphicTypeContainsTypesWithoutDiscriminator) + { + // Require the discriminator here since it's not common to all derived types. + (required ??= new()).Add(typeDiscriminatorPair.Key); + } + } + + Func? parameterInfoMapper = ResolveJsonConstructorParameterMapper(typeInfo); + + state.PushSchemaNode(JsonSchemaConstants.PropertiesPropertyName); + foreach (JsonPropertyInfo property in typeInfo.Properties) + { + if (property is { Get: null, Set: null } or { IsExtensionData: true }) + { + continue; // Skip JsonIgnored properties and extension data + } + + JsonNumberHandling? propertyNumberHandling = property.NumberHandling ?? effectiveNumberHandling; + JsonTypeInfo propertyTypeInfo = typeInfo.Options.GetTypeInfo(property.PropertyType); + + // Resolve the attribute provider for the property. + ICustomAttributeProvider? attributeProvider = ResolveAttributeProvider(typeInfo.Type, property); + + // Resolve property-level description attributes. + string? propertyDescription = state.Configuration.ResolveDescriptionAttributes + ? attributeProvider?.GetCustomAttributes(inherit: true).OfType().FirstOrDefault()?.Description + : null; + + // Declare the property as nullable if either getter or setter are nullable. + bool isNonNullableProperty = false; + if (attributeProvider is MemberInfo memberInfo) + { + NullabilityInfo nullabilityInfo = state.NullabilityInfoContext.GetMemberNullability(memberInfo); + isNonNullableProperty = + (property.Get is null || nullabilityInfo.ReadState is NullabilityState.NotNull) && + (property.Set is null || nullabilityInfo.WriteState is NullabilityState.NotNull); + } + + bool isRequired = property.IsRequired; + bool hasDefaultValue = false; + JsonNode? defaultValue = null; + + ParameterInfo? associatedParameter = parameterInfoMapper?.Invoke(property); + if (associatedParameter != null) + { + ResolveParameterInfo( + associatedParameter, + propertyTypeInfo, + state.NullabilityInfoContext, + state.Configuration, + out hasDefaultValue, + out defaultValue, + out bool isNonNullableParameter, + ref propertyDescription, + ref isRequired); + + isNonNullableProperty &= isNonNullableParameter; + } + + state.PushSchemaNode(property.Name); + JsonSchema propertySchema = MapJsonSchemaCore( + ref state, + propertyTypeInfo, + parentType: typeInfo.Type, + propertyInfo: property, + parameterInfo: associatedParameter, + propertyAttributeProvider: attributeProvider, + isNonNullableType: isNonNullableProperty, + description: propertyDescription, + customConverter: property.CustomConverter, + customNumberHandling: propertyNumberHandling); + + state.PopSchemaNode(); + + if (hasDefaultValue) + { + propertySchema.DefaultValue = defaultValue; + propertySchema.HasDefaultValue = true; + } + + (properties ??= new()).Add(new(property.Name, propertySchema)); + + if (isRequired) + { + (required ??= new()).Add(property.Name); + } + } + + state.PopSchemaNode(); + return CompleteSchema(ref state, new() + { + Type = JsonSchemaType.Object, + Properties = properties, + Required = required, + AdditionalProperties = additionalProperties, + }); + + case JsonTypeInfoKind.Enumerable: + Type elementType = GetElementType(typeInfo); + JsonTypeInfo elementTypeInfo = typeInfo.Options.GetTypeInfo(elementType); + + if (typeDiscriminator is null) + { + state.PushSchemaNode(JsonSchemaConstants.ItemsPropertyName); + JsonSchema items = MapJsonSchemaCore(ref state, elementTypeInfo, customNumberHandling: effectiveNumberHandling); + state.PopSchemaNode(); + + return CompleteSchema(ref state, new() + { + Type = JsonSchemaType.Array, + Items = items.IsTrue ? null : items, + }); + } + else + { + // Polymorphic enumerable types are represented using a wrapping object: + // { "$type" : "discriminator", "$values" : [element1, element2, ...] } + // Which corresponds to the schema + // { "properties" : { "$type" : { "const" : "discriminator" }, "$values" : { "type" : "array", "items" : { ... } } } } + const string ValuesKeyword = "$values"; + + state.PushSchemaNode(JsonSchemaConstants.PropertiesPropertyName); + state.PushSchemaNode(ValuesKeyword); + state.PushSchemaNode(JsonSchemaConstants.ItemsPropertyName); + + JsonSchema items = MapJsonSchemaCore(ref state, elementTypeInfo, customNumberHandling: effectiveNumberHandling); + + state.PopSchemaNode(); + state.PopSchemaNode(); + state.PopSchemaNode(); + + return CompleteSchema(ref state, new() + { + Type = JsonSchemaType.Object, + Properties = new() + { + typeDiscriminator.Value, + new(ValuesKeyword, + new JsonSchema() + { + Type = JsonSchemaType.Array, + Items = items.IsTrue ? null : items, + }), + }, + Required = parentPolymorphicTypeContainsTypesWithoutDiscriminator ? new() { typeDiscriminator.Value.Key } : null, + }); + } + + case JsonTypeInfoKind.Dictionary: + Type valueType = GetElementType(typeInfo); + JsonTypeInfo valueTypeInfo = typeInfo.Options.GetTypeInfo(valueType); + + List>? dictProps = null; + List? dictRequired = null; + + if (typeDiscriminator is { } dictDiscriminator) + { + dictProps = new() { dictDiscriminator }; + if (parentPolymorphicTypeContainsTypesWithoutDiscriminator) + { + // Require the discriminator here since it's not common to all derived types. + dictRequired = new() { dictDiscriminator.Key }; + } + } + + state.PushSchemaNode(JsonSchemaConstants.AdditionalPropertiesPropertyName); + JsonSchema valueSchema = MapJsonSchemaCore(ref state, valueTypeInfo, customNumberHandling: effectiveNumberHandling); + state.PopSchemaNode(); + + return CompleteSchema(ref state, new() + { + Type = JsonSchemaType.Object, + Properties = dictProps, + Required = dictRequired, + AdditionalProperties = valueSchema.IsTrue ? null : valueSchema, + }); + + default: + Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.None); + + if (s_simpleTypeSchemaFactories.TryGetValue(typeInfo.Type, out Func? simpleTypeSchemaFactory)) + { + schema = simpleTypeSchemaFactory(effectiveNumberHandling); + } + else if (typeInfo.Type.IsEnum) + { + schema = GetEnumConverterSchema(typeInfo, effectiveConverter, state.Configuration); + } + else + { + schema = JsonSchema.True; + } + + return CompleteSchema(ref state, schema); + } + + JsonSchema CompleteSchema(ref GenerationState state, JsonSchema schema) + { + if (schema.Ref is null) + { + if (state.Configuration.IncludeSchemaVersion && state.CurrentDepth == 0) + { + JsonSchema.EnsureMutable(ref schema); + schema.Schema = SchemaVersion; + } + + if (description is not null) + { + JsonSchema.EnsureMutable(ref schema); + schema.Description = description; + } + + // A schema is marked as nullable if either + // 1. We have a schema for a property where either the getter or setter are marked as nullable. + // 2. We have a schema for a reference type, unless we're explicitly treating null-oblivious types as non-nullable. + bool isNullableSchema = (propertyInfo != null || parameterInfo != null) + ? !isNonNullableType + : CanBeNull(typeInfo.Type) && !parentPolymorphicTypeIsNonNullable && !state.Configuration.TreatNullObliviousAsNonNullable; + + if (isNullableSchema) + { + schema.MakeNullable(); + } + + if (cacheResult) + { + state.PopGeneratedType(); + } + } + + if (state.Configuration.TransformSchemaNode != null) + { + // Prime the schema for invocation by the JsonNode transformer. + schema.GenerationContext = new(typeInfo, parentType, propertyInfo, parameterInfo, propertyAttributeProvider); + } + + return schema; + } + } + + private readonly ref struct GenerationState + { + private readonly List _currentPath; + private readonly List<(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo, int depth)> _generationStack; + private readonly int _maxDepth; + + public GenerationState(JsonSchemaMapperConfiguration configuration, JsonSerializerOptions options, NullabilityInfoContext? nullabilityInfoContext = null) + { + Configuration = configuration; + NullabilityInfoContext = nullabilityInfoContext ?? new(); + _maxDepth = options.MaxDepth is 0 ? 64 : options.MaxDepth; + _generationStack = new(); + _currentPath = new(); + } + + public JsonSchemaMapperConfiguration Configuration { get; } + public NullabilityInfoContext NullabilityInfoContext { get; } + public int CurrentDepth => _currentPath.Count; + + public void PushSchemaNode(string nodeId) + { + if (CurrentDepth == _maxDepth) + { + ThrowHelpers.ThrowInvalidOperationException_MaxDepthReached(); + } + + _currentPath.Add(nodeId); + } + + public void PopSchemaNode() + { + _currentPath.RemoveAt(_currentPath.Count - 1); + } + + /// + /// Pushes the current type/property to the generation stack or returns a JSON pointer if the type is recursive. + /// + public bool TryPushType(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo, [NotNullWhen(true)] out string? existingJsonPointer) + { + foreach ((JsonTypeInfo otherTypeInfo, JsonPropertyInfo? otherPropertyInfo, int depth) in _generationStack) + { + if (typeInfo == otherTypeInfo && propertyInfo == otherPropertyInfo) + { + existingJsonPointer = FormatJsonPointer(_currentPath, depth); + return true; + } + } + + _generationStack.Add((typeInfo, propertyInfo, CurrentDepth)); + existingJsonPointer = null; + return false; + } + + public void PopGeneratedType() + { + Debug.Assert(_generationStack.Count > 0); + _generationStack.RemoveAt(_generationStack.Count - 1); + } + + private static string FormatJsonPointer(List currentPathList, int depth) + { + Debug.Assert(0 <= depth && depth < currentPathList.Count); + + if (depth == 0) + { + return "#"; + } + + StringBuilder sb = new(); + sb.Append('#'); + + for (int i = 0; i < depth; i++) + { + string segment = currentPathList[i]; + if (segment.AsSpan().IndexOfAny('~', '/') != -1) + { + segment = segment.Replace("~", "~0").Replace("/", "~1"); + } + + sb.Append('/'); + sb.Append(segment); + } + + return sb.ToString(); + } + } + + private static readonly Dictionary> s_simpleTypeSchemaFactories = new() + { + [typeof(object)] = _ => JsonSchema.True, + [typeof(bool)] = _ => new JsonSchema { Type = JsonSchemaType.Boolean }, + [typeof(byte)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), + [typeof(ushort)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), + [typeof(uint)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), + [typeof(ulong)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), + [typeof(sbyte)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), + [typeof(short)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), + [typeof(int)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), + [typeof(long)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), + [typeof(float)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Number, numberHandling, isIeeeFloatingPoint: true), + [typeof(double)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Number, numberHandling, isIeeeFloatingPoint: true), + [typeof(decimal)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Number, numberHandling), +#if NET6_0_OR_GREATER + [typeof(Half)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Number, numberHandling, isIeeeFloatingPoint: true), +#endif +#if NET7_0_OR_GREATER + [typeof(UInt128)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), + [typeof(Int128)] = numberHandling => GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling), +#endif + [typeof(char)] = _ => new JsonSchema { Type = JsonSchemaType.String, MinLength = 1, MaxLength = 1 }, + [typeof(string)] = _ => new JsonSchema { Type = JsonSchemaType.String }, + [typeof(byte[])] = _ => new JsonSchema { Type = JsonSchemaType.String }, + [typeof(Memory)] = _ => new JsonSchema { Type = JsonSchemaType.String }, + [typeof(ReadOnlyMemory)] = _ => new JsonSchema { Type = JsonSchemaType.String }, + [typeof(DateTime)] = _ => new JsonSchema { Type = JsonSchemaType.String, Format = "date-time" }, + [typeof(DateTimeOffset)] = _ => new JsonSchema { Type = JsonSchemaType.String, Format = "date-time" }, + [typeof(TimeSpan)] = _ => new JsonSchema + { + Comment = "Represents a System.TimeSpan value.", + Type = JsonSchemaType.String, + Pattern = @"^-?(\d+\.)?\d{2}:\d{2}:\d{2}(\.\d{1,7})?$", + }, + +#if NET6_0_OR_GREATER + [typeof(DateOnly)] = _ => new JsonSchema { Type = JsonSchemaType.String, Format = "date" }, + [typeof(TimeOnly)] = _ => new JsonSchema { Type = JsonSchemaType.String, Format = "time" }, +#endif + [typeof(Guid)] = _ => new JsonSchema { Type = JsonSchemaType.String, Format = "uuid" }, + [typeof(Uri)] = _ => new JsonSchema { Type = JsonSchemaType.String, Format = "uri" }, + [typeof(Version)] = _ => new JsonSchema + { + Comment = "Represents a version string.", + Type = JsonSchemaType.String, + Pattern = @"^\d+(\.\d+){1,3}$", + }, + + [typeof(JsonDocument)] = _ => new JsonSchema { Type = JsonSchemaType.Any }, + [typeof(JsonElement)] = _ => new JsonSchema { Type = JsonSchemaType.Any }, + [typeof(JsonNode)] = _ => new JsonSchema { Type = JsonSchemaType.Any }, + [typeof(JsonValue)] = _ => new JsonSchema { Type = JsonSchemaType.Any }, + [typeof(JsonObject)] = _ => new JsonSchema { Type = JsonSchemaType.Object }, + [typeof(JsonArray)] = _ => new JsonSchema { Type = JsonSchemaType.Array }, + }; + + // Adapted from https://github.com/dotnet/runtime/blob/d606c601510c1a1a28cb6ef3550f12db049c0776/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonPrimitiveConverter.cs#L36-L69 + private static JsonSchema GetSchemaForNumericType(JsonSchemaType schemaType, JsonNumberHandling numberHandling, bool isIeeeFloatingPoint = false) + { + Debug.Assert(schemaType is JsonSchemaType.Integer or JsonSchemaType.Number); + Debug.Assert(!isIeeeFloatingPoint || schemaType is JsonSchemaType.Number); + + string? pattern = null; + + if ((numberHandling & (JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)) != 0) + { + pattern = schemaType is JsonSchemaType.Integer + ? @"^-?(?:0|[1-9]\d*)$" + : isIeeeFloatingPoint + ? @"^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$" + : @"^-?(?:0|[1-9]\d*)(?:\.\d+)?$"; + + schemaType |= JsonSchemaType.String; + } + + if (isIeeeFloatingPoint && (numberHandling & JsonNumberHandling.AllowNamedFloatingPointLiterals) != 0) + { + return new JsonSchema + { + AnyOf = new() + { + new JsonSchema { Type = schemaType, Pattern = pattern }, + new JsonSchema { Enum = new() { (JsonNode)"NaN", (JsonNode)"Infinity", (JsonNode)"-Infinity" } }, + }, + }; + } + + return new JsonSchema { Type = schemaType, Pattern = pattern }; + } + + // Uses reflection to determine the element type of an enumerable or dictionary type + // Workaround for https://github.com/dotnet/runtime/issues/77306#issuecomment-2007887560 + private static Type GetElementType(JsonTypeInfo typeInfo) + { + Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary); + s_elementTypeProperty ??= typeof(JsonTypeInfo).GetProperty("ElementType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + return (Type)s_elementTypeProperty?.GetValue(typeInfo)!; + } + + private static PropertyInfo? s_elementTypeProperty; + + // The source generator currently doesn't populate attribute providers for properties + // cf. https://github.com/dotnet/runtime/issues/100095 + // Work around the issue by running a query for the relevant MemberInfo using the internal MemberName property + // https://github.com/dotnet/runtime/blob/de774ff9ee1a2c06663ab35be34b755cd8d29731/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs#L206 +#if NETCOREAPP + [EditorBrowsable(EditorBrowsableState.Never)] + [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", + Justification = "We're reading the internal JsonPropertyInfo.MemberName which cannot have been trimmed away.")] + [UnconditionalSuppressMessage("Trimming", "IL2070:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The parameter of method does not have matching annotations.", + Justification = "We're reading the member which is already accessed by the source generator.")] +#endif + private static ICustomAttributeProvider? ResolveAttributeProvider(Type? declaringType, JsonPropertyInfo? propertyInfo) + { + if (declaringType is null || propertyInfo is null) + { + return null; + } + + if (propertyInfo.AttributeProvider is { } provider) + { + return provider; + } + + s_memberNameProperty ??= typeof(JsonPropertyInfo).GetProperty("MemberName", BindingFlags.Instance | BindingFlags.NonPublic)!; + var memberName = (string?)s_memberNameProperty.GetValue(propertyInfo); + if (memberName is not null) + { + return declaringType.GetMember(memberName, MemberTypes.Property | MemberTypes.Field, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(); + } + + return null; + } + + private static PropertyInfo? s_memberNameProperty; + + // Uses reflection to determine any custom converters specified for the element of a nullable type. +#if NETCOREAPP + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "We're resolving private fields of the built-in Nullable converter which cannot have been trimmed away.")] +#endif + private static JsonConverter? ExtractCustomNullableConverter(JsonConverter? converter) + { + Debug.Assert(converter is null || IsBuiltInConverter(converter)); + + // There is unfortunately no way in which we can obtain the element converter from a nullable converter without resorting to private reflection + // https://github.com/dotnet/runtime/blob/5fda47434cecc590095e9aef3c4e560b7b7ebb47/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs#L15-L17 + Type? converterType = converter?.GetType(); + if (converterType?.Name == "NullableConverter`1") + { + FieldInfo elementConverterField = converterType.GetPrivateFieldWithPotentiallyTrimmedMetadata("_elementConverter"); + return (JsonConverter)elementConverterField!.GetValue(converter)!; + } + + return null; + } + + // Uses reflection to determine schema for enum types + // Adapted from https://github.com/dotnet/runtime/blob/d606c601510c1a1a28cb6ef3550f12db049c0776/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs#L498-L521 +#if NETCOREAPP + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "We're resolving private fields of the built-in enum converter which cannot have been trimmed away.")] +#endif + private static JsonSchema GetEnumConverterSchema(JsonTypeInfo typeInfo, JsonConverter converter, JsonSchemaMapperConfiguration configuration) + { + Debug.Assert(typeInfo.Type.IsEnum && IsBuiltInConverter(converter)); + + if (converter is JsonConverterFactory factory) + { + converter = factory.CreateConverter(typeInfo.Type, typeInfo.Options)!; + } + + Type converterType = converter.GetType(); + FieldInfo converterOptionsField = converterType.GetPrivateFieldWithPotentiallyTrimmedMetadata("_converterOptions"); + FieldInfo namingPolicyField = converterType.GetPrivateFieldWithPotentiallyTrimmedMetadata("_namingPolicy"); + + const int EnumConverterOptionsAllowStrings = 1; + var converterOptions = (int)converterOptionsField!.GetValue(converter)!; + if ((converterOptions & EnumConverterOptionsAllowStrings) != 0) + { + // This explicitly ignores the integer component in converters configured as AllowNumbers | AllowStrings + // which is the default for JsonStringEnumConverter. This sacrifices some precision in the schema for simplicity. + + if (typeInfo.Type.GetCustomAttribute() is not null) + { + // Do not report enum values in case of flags. + return new() { Type = JsonSchemaType.String }; + } + + var namingPolicy = (JsonNamingPolicy?)namingPolicyField!.GetValue(converter)!; + JsonArray enumValues = new(); + foreach (string name in Enum.GetNames(typeInfo.Type)) + { + // This does not account for custom names specified via the new + // JsonStringEnumMemberNameAttribute introduced in .NET 9. + string effectiveName = namingPolicy?.ConvertName(name) ?? name; + enumValues.Add((JsonNode)effectiveName); + } + + JsonSchema schema = new() { Enum = enumValues }; + if (configuration.IncludeTypeInEnums) + { + schema.Type = JsonSchemaType.String; + } + + return schema; + } + + return new() { Type = JsonSchemaType.Integer }; + } + +#if NETCOREAPP + [RequiresUnreferencedCode("Resolves unreferenced member metadata.")] +#endif + private static FieldInfo GetPrivateFieldWithPotentiallyTrimmedMetadata(this Type type, string fieldName) + { + FieldInfo? field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + if (field is null) + { + throw new InvalidOperationException( + $"Could not resolve metadata for field '{fieldName}' in type '{type}'. " + + "If running Native AOT ensure that the 'IlcTrimMetadata' property has been disabled."); + } + + return field; + } + + // Resolves the parameters of the deserialization constructor for a type, if they exist. +#if NETCOREAPP + [UnconditionalSuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", + Justification = "The deserialization constructor should have already been referenced by the source generator and therefore will not have been trimmed.")] +#endif + private static Func? ResolveJsonConstructorParameterMapper(JsonTypeInfo typeInfo) + { + Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object); + + if (typeInfo.Properties.Count > 0 && + typeInfo.CreateObject is null && // Ensure that a default constructor isn't being used + typeInfo.Type.TryGetDeserializationConstructor(useDefaultCtorInAnnotatedStructs: true, out ConstructorInfo? ctor)) + { + ParameterInfo[]? parameters = ctor?.GetParameters(); + if (parameters?.Length > 0) + { + Dictionary dict = new(parameters.Length); + foreach (ParameterInfo parameter in parameters) + { + if (parameter.Name is not null) + { + // We don't care about null parameter names or conflicts since they + // would have already been rejected by JsonTypeInfo configuration. + dict[new(parameter.Name, parameter.ParameterType)] = parameter; + } + } + + return prop => dict.TryGetValue(new(prop.Name, prop.PropertyType), out ParameterInfo? parameter) ? parameter : null; + } + } + + return null; + } + + // Parameter to property matching semantics as declared in + // https://github.com/dotnet/runtime/blob/12d96ccfaed98e23c345188ee08f8cfe211c03e7/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs#L1007-L1030 + private readonly struct ParameterLookupKey : IEquatable + { + public ParameterLookupKey(string name, Type type) + { + Name = name; + Type = type; + } + + public string Name { get; } + public Type Type { get; } + + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Name); + public bool Equals(ParameterLookupKey other) => Type == other.Type && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase); + public override bool Equals(object? obj) => obj is ParameterLookupKey key && Equals(key); + } + + // Resolves the deserialization constructor for a type using logic copied from + // https://github.com/dotnet/runtime/blob/e12e2fa6cbdd1f4b0c8ad1b1e2d960a480c21703/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs#L227-L286 + private static bool TryGetDeserializationConstructor( +#if NETCOREAPP + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] +#endif + this Type type, + bool useDefaultCtorInAnnotatedStructs, + out ConstructorInfo? deserializationCtor) + { + ConstructorInfo? ctorWithAttribute = null; + ConstructorInfo? publicParameterlessCtor = null; + ConstructorInfo? lonePublicCtor = null; + + ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + + if (constructors.Length == 1) + { + lonePublicCtor = constructors[0]; + } + + foreach (ConstructorInfo constructor in constructors) + { + if (HasJsonConstructorAttribute(constructor)) + { + if (ctorWithAttribute != null) + { + deserializationCtor = null; + return false; + } + + ctorWithAttribute = constructor; + } + else if (constructor.GetParameters().Length == 0) + { + publicParameterlessCtor = constructor; + } + } + + // Search for non-public ctors with [JsonConstructor]. + foreach (ConstructorInfo constructor in type.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)) + { + if (HasJsonConstructorAttribute(constructor)) + { + if (ctorWithAttribute != null) + { + deserializationCtor = null; + return false; + } + + ctorWithAttribute = constructor; + } + } + + // Structs will use default constructor if attribute isn't used. + if (useDefaultCtorInAnnotatedStructs && type.IsValueType && ctorWithAttribute == null) + { + deserializationCtor = null; + return true; + } + + deserializationCtor = ctorWithAttribute ?? publicParameterlessCtor ?? lonePublicCtor; + return true; + + static bool HasJsonConstructorAttribute(ConstructorInfo constructorInfo) => + constructorInfo.GetCustomAttribute() != null; + } + + private static bool IsBuiltInConverter(JsonConverter converter) => + converter.GetType().Assembly == typeof(JsonConverter).Assembly; + + // Resolves the nullable reference type annotations for a property or field, + // additionally addressing a few known bugs of the NullabilityInfo pre .NET 9. + private static NullabilityInfo GetMemberNullability(this NullabilityInfoContext context, MemberInfo memberInfo) + { + Debug.Assert(memberInfo is PropertyInfo or FieldInfo); + return memberInfo is PropertyInfo prop + ? context.Create(prop) + : context.Create((FieldInfo)memberInfo); + } + + private static bool CanBeNull(Type type) => !type.IsValueType || Nullable.GetUnderlyingType(type) is not null; + + private static partial class ThrowHelpers + { + [DoesNotReturn] + public static void ThrowInvalidOperationException_MaxDepthReached() => + throw new InvalidOperationException("The depth of the generated JSON schema exceeds the JsonSerializerOptions.MaxDepth setting."); + } +} +#endif diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv9.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv9.cs new file mode 100644 index 000000000000..20a77621173f --- /dev/null +++ b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.STJv9.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft. All rights reserved. + +#if NET9_0_OR_GREATER || SYSTEM_TEXT_JSON_V9 +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; + +namespace JsonSchemaMapper; + +#if EXPOSE_JSON_SCHEMA_MAPPER +public +#else +internal +#endif + static partial class JsonSchemaMapper +{ + // For System.Text.Json v9 or greater, JsonSchemaMapper is implemented as a shim over the + // built-in JsonSchemaExporter component. Added functionality is implemented by performing + // fix-ups over the generated schema. + + private static partial JsonNode MapRootTypeJsonSchema(JsonTypeInfo typeInfo, JsonSchemaMapperConfiguration configuration) + { + JsonSchemaExporterOptions exporterOptions = new() + { + TreatNullObliviousAsNonNullable = configuration.TreatNullObliviousAsNonNullable, + TransformSchemaNode = (JsonSchemaExporterContext ctx, JsonNode schema) => ApplySchemaTransformations(schema, ctx, configuration), + }; + + return JsonSchemaExporter.GetJsonSchemaAsNode(typeInfo, exporterOptions); + } + + private static partial JsonNode MapMethodParameterJsonSchema( + ParameterInfo parameterInfo, + JsonTypeInfo parameterTypeInfo, + JsonSchemaMapperConfiguration configuration, + NullabilityInfoContext nullabilityContext, + out bool isRequired) + { + Debug.Assert(parameterInfo.Name != null); + + JsonSchemaExporterOptions exporterOptions = new() + { + TreatNullObliviousAsNonNullable = configuration.TreatNullObliviousAsNonNullable, + TransformSchemaNode = (JsonSchemaExporterContext ctx, JsonNode schema) => ApplySchemaTransformations(schema, ctx, configuration, parameterInfo.Name), + }; + + string? parameterDescription = null; + isRequired = false; + + ResolveParameterInfo( + parameterInfo, + parameterTypeInfo, + nullabilityContext, + configuration, + out bool hasDefaultValue, + out JsonNode? defaultValue, + out bool isNonNullableType, + ref parameterDescription, + ref isRequired); + + JsonNode parameterSchema = JsonSchemaExporter.GetJsonSchemaAsNode(parameterTypeInfo, exporterOptions); + + if (parameterDescription is not null) + { + ConvertSchemaToObject(ref parameterSchema).Insert(0, JsonSchemaConstants.DescriptionPropertyName, (JsonNode)parameterDescription); + } + + if (hasDefaultValue) + { + ConvertSchemaToObject(ref parameterSchema).Add(JsonSchemaConstants.DefaultPropertyName, defaultValue); + } + + if (isNonNullableType && + parameterSchema is JsonObject parameterSchemaObj && + parameterSchemaObj.TryGetPropertyValue(JsonSchemaConstants.TypePropertyName, out JsonNode? typeSchema) && + typeSchema is JsonArray typeArray) + { + for (int i = 0; i < typeArray.Count; i++) + { + if (typeArray[i]!.GetValue() is "null") + { + typeArray.RemoveAt(i); + break; + } + } + + if (typeArray.Count == 1) + { + parameterSchemaObj[JsonSchemaConstants.TypePropertyName] = (JsonNode)(string)typeArray[0]!; + } + } + + return parameterSchema; + } + + private static JsonNode ApplySchemaTransformations( + JsonNode schema, + JsonSchemaExporterContext ctx, + JsonSchemaMapperConfiguration configuration, + string? parameterName = null) + { + JsonSchemaGenerationContext mapperCtx = new( + ctx.TypeInfo, + ctx.TypeInfo.Type, + ctx.PropertyInfo, + (ParameterInfo?)ctx.PropertyInfo?.AssociatedParameter?.AttributeProvider, + ctx.PropertyInfo?.AttributeProvider); + + if (configuration.IncludeTypeInEnums) + { + if (ctx.TypeInfo.Type.IsEnum && + schema is JsonObject enumSchema && + enumSchema.ContainsKey(JsonSchemaConstants.EnumPropertyName)) + { + enumSchema.Insert(0, JsonSchemaConstants.TypePropertyName, (JsonNode)"string"); + } + else if ( + Nullable.GetUnderlyingType(ctx.TypeInfo.Type) is Type { IsEnum: true } && + schema is JsonObject nullableEnumSchema && + nullableEnumSchema.ContainsKey(JsonSchemaConstants.EnumPropertyName)) + { + nullableEnumSchema.Insert(0, JsonSchemaConstants.TypePropertyName, new JsonArray() { (JsonNode)"string", (JsonNode)"null" }); + } + } + + if (configuration.ResolveDescriptionAttributes && mapperCtx.GetAttribute() is DescriptionAttribute attr) + { + ConvertSchemaToObject(ref schema).Insert(0, JsonSchemaConstants.DescriptionPropertyName, (JsonNode)attr.Description); + } + + if (parameterName is null && configuration.IncludeSchemaVersion && ctx.Path.IsEmpty) + { + ConvertSchemaToObject(ref schema).Insert(0, JsonSchemaConstants.SchemaPropertyName, (JsonNode)SchemaVersion); + } + + if (configuration.TransformSchemaNode is { } callback) + { + schema = callback(mapperCtx, schema); + } + + if (parameterName != null && schema is JsonObject refObj && + refObj.TryGetPropertyValue(JsonSchemaConstants.RefPropertyName, out JsonNode? paramName)) + { + // Fix up any $ref URIs to match the path from the root document. + string refUri = paramName!.GetValue(); + Debug.Assert(refUri is "#" || refUri.StartsWith("#/", StringComparison.Ordinal)); + refUri = refUri == "#" + ? $"#/{JsonSchemaConstants.PropertiesPropertyName}/{parameterName}" + : $"#/{JsonSchemaConstants.PropertiesPropertyName}/{parameterName}/{refUri[2..]}"; + + refObj[JsonSchemaConstants.RefPropertyName] = (JsonNode)refUri; + } + + return schema; + } + + private static JsonObject ConvertSchemaToObject(ref JsonNode schema) + { + JsonObject jObj; + + switch (schema.GetValueKind()) + { + case JsonValueKind.Object: + return (JsonObject)schema; + + case JsonValueKind.False: + schema = jObj = new() { [JsonSchemaConstants.NotPropertyName] = true }; + return jObj; + + default: + Debug.Assert(schema.GetValueKind() is JsonValueKind.True, "invalid schema type."); + schema = jObj = new JsonObject(); + return jObj; + } + } +} +#endif diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.cs index 55e7763b786f..a718d5965472 100644 --- a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.cs +++ b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapper.cs @@ -1,11 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Linq; using System.Reflection; using System.Text.Json; @@ -19,12 +17,12 @@ namespace JsonSchemaMapper; /// Maps .NET types to JSON schema objects using contract metadata from instances. /// #if EXPOSE_JSON_SCHEMA_MAPPER - public +public #else [ExcludeFromCodeCoverage] internal #endif -static partial class JsonSchemaMapper + static partial class JsonSchemaMapper { /// /// The JSON schema draft version used by the generated schemas. @@ -37,10 +35,10 @@ static partial class JsonSchemaMapper /// The options instance from which to resolve the contract metadata. /// The root type for which to generate the JSON schema. /// The configuration object controlling the schema generation. - /// A new instance defining the JSON schema for . + /// A new instance defining the JSON schema for . /// One of the specified parameters is . /// The parameter contains unsupported configuration. - public static JsonObject GetJsonSchema(this JsonSerializerOptions options, Type type, JsonSchemaMapperConfiguration? configuration = null) + public static JsonNode GetJsonSchema(this JsonSerializerOptions options, Type type, JsonSchemaMapperConfiguration? configuration = null) { if (options is null) { @@ -54,10 +52,8 @@ public static JsonObject GetJsonSchema(this JsonSerializerOptions options, Type ValidateOptions(options); configuration ??= JsonSchemaMapperConfiguration.Default; - JsonTypeInfo typeInfo = options.GetTypeInfo(type); - var state = new GenerationState(configuration); - return MapJsonSchemaCore(typeInfo, ref state); + return MapRootTypeJsonSchema(typeInfo, configuration); } /// @@ -66,10 +62,10 @@ public static JsonObject GetJsonSchema(this JsonSerializerOptions options, Type /// The options instance from which to resolve the contract metadata. /// The method from whose parameters to generate the JSON schema. /// The configuration object controlling the schema generation. - /// A new instance defining the JSON schema for . + /// A new instance defining the JSON schema for . /// One of the specified parameters is . /// The parameter contains unsupported configuration. - public static JsonObject GetJsonSchema(this JsonSerializerOptions options, MethodBase method, JsonSchemaMapperConfiguration? configuration = null) + public static JsonNode GetJsonSchema(this JsonSerializerOptions options, MethodBase method, JsonSchemaMapperConfiguration? configuration = null) { if (options is null) { @@ -84,52 +80,60 @@ public static JsonObject GetJsonSchema(this JsonSerializerOptions options, Metho ValidateOptions(options); configuration ??= JsonSchemaMapperConfiguration.Default; - var state = new GenerationState(configuration); - string title = method.Name; - string? description = configuration.ResolveDescriptionAttributes - ? method.GetCustomAttribute()?.Description - : null; + JsonObject schema = new(); + + if (configuration.IncludeSchemaVersion) + { + schema.Add(JsonSchemaConstants.SchemaPropertyName, SchemaVersion); + } + + schema.Add(JsonSchemaConstants.TitlePropertyName, method.Name); - JsonSchemaType type = JsonSchemaType.Object; + if (configuration.ResolveDescriptionAttributes && + method.GetCustomAttribute() is DescriptionAttribute attr) + { + schema.Add(JsonSchemaConstants.DescriptionPropertyName, attr.Description); + } + + schema.Add(JsonSchemaConstants.TypePropertyName, "object"); + + NullabilityInfoContext nullabilityInfoContext = new(); JsonObject? paramSchemas = null; JsonArray? requiredParams = null; - foreach (ParameterInfo parameter in method.GetParameters()) + foreach (ParameterInfo parameterInfo in method.GetParameters()) { - if (parameter.Name is null) + if (parameterInfo.Name is null) { ThrowHelpers.ThrowInvalidOperationException_TrimmedMethodParameters(method); } - JsonTypeInfo parameterInfo = options.GetTypeInfo(parameter.ParameterType); - bool isNullableReferenceType = false; - string? parameterDescription = null; - bool hasDefaultValue = false; - JsonNode? defaultValue = null; - bool isRequired = false; - - ResolveParameterInfo(parameter, parameterInfo, ref state, ref parameterDescription, ref hasDefaultValue, ref defaultValue, ref isNullableReferenceType, ref isRequired); - - state.Push(parameter.Name); - JsonObject paramSchema = MapJsonSchemaCore( + JsonTypeInfo parameterTypeInfo = options.GetTypeInfo(parameterInfo.ParameterType); + JsonNode parameterSchema = MapMethodParameterJsonSchema( parameterInfo, - ref state, - title: null, - parameterDescription, - isNullableReferenceType, - hasDefaultValue: hasDefaultValue, - defaultValue: defaultValue); + parameterTypeInfo, + configuration, + nullabilityInfoContext, + out bool isRequired); - state.Pop(); - - (paramSchemas ??= []).Add(parameter.Name, paramSchema); + (paramSchemas ??= new()).Add(parameterInfo.Name, parameterSchema); if (isRequired) { - (requiredParams ??= []).Add((JsonNode)parameter.Name); + (requiredParams ??= new()).Add((JsonNode)parameterInfo.Name); } } - return CreateSchemaDocument(ref state, title: title, description: description, schemaType: type, properties: paramSchemas, requiredProperties: requiredParams); + if (paramSchemas != null) + { + schema.Add(JsonSchemaConstants.PropertiesPropertyName, paramSchemas); + } + + if (requiredParams != null) + { + schema.Add(JsonSchemaConstants.RequiredPropertyName, requiredParams); + } + + return schema; } /// @@ -137,10 +141,10 @@ public static JsonObject GetJsonSchema(this JsonSerializerOptions options, Metho /// /// The contract metadata for which to generate the schema. /// The configuration object controlling the schema generation. - /// A new instance defining the JSON schema for . + /// A new instance defining the JSON schema for . /// One of the specified parameters is . /// The parameter contains unsupported configuration. - public static JsonObject GetJsonSchema(this JsonTypeInfo typeInfo, JsonSchemaMapperConfiguration? configuration = null) + public static JsonNode GetJsonSchema(this JsonTypeInfo typeInfo, JsonSchemaMapperConfiguration? configuration = null) { if (typeInfo is null) { @@ -149,9 +153,8 @@ public static JsonObject GetJsonSchema(this JsonTypeInfo typeInfo, JsonSchemaMap ValidateOptions(typeInfo.Options); typeInfo.MakeReadOnly(); - - var state = new GenerationState(configuration ?? JsonSchemaMapperConfiguration.Default); - return MapJsonSchemaCore(typeInfo, ref state); + configuration ??= JsonSchemaMapperConfiguration.Default; + return MapRootTypeJsonSchema(typeInfo, configuration); } /// @@ -162,397 +165,51 @@ public static JsonObject GetJsonSchema(this JsonTypeInfo typeInfo, JsonSchemaMap /// The JSON node rendered as a JSON string. public static string ToJsonString(this JsonNode? node, bool writeIndented = false) { - return node is null - ? "null" - : node.ToJsonString(writeIndented ? new JsonSerializerOptions { WriteIndented = true } : null); + return node is null ? "null" : node.ToJsonString(writeIndented ? s_writeIndentedOptions : null); } - private static JsonObject MapJsonSchemaCore( - JsonTypeInfo typeInfo, - ref GenerationState state, - string? title = null, - string? description = null, - bool isNullableReferenceType = false, - bool isNullableOfTElement = false, - JsonConverter? customConverter = null, - bool hasDefaultValue = false, - JsonNode? defaultValue = null, - JsonNumberHandling? customNumberHandling = null, - KeyValuePair? derivedTypeDiscriminator = null, - Type? parentNullableOfT = null) - { - Debug.Assert(typeInfo.IsReadOnly); - - Type type = typeInfo.Type; - JsonConverter effectiveConverter = customConverter ?? typeInfo.Converter; - JsonNumberHandling? effectiveNumberHandling = customNumberHandling ?? typeInfo.NumberHandling; - bool emitsTypeDiscriminator = derivedTypeDiscriminator?.Value is not null; - bool isCacheable = !emitsTypeDiscriminator && description is null && !hasDefaultValue && !isNullableOfTElement; - - if (!IsBuiltInConverter(effectiveConverter)) - { - return []; // We can't make any schema determinations if a custom converter is used - } - - if (isCacheable && state.TryGetGeneratedSchemaPath(type, parentNullableOfT, customConverter, isNullableReferenceType, customNumberHandling, out string? typePath)) - { - // Schema for type has already been generated, return a reference to it. - // For derived types using discriminators, the schema is generated inline. - return new JsonObject { [RefPropertyName] = typePath }; - } - - if (state.Configuration.ResolveDescriptionAttributes) - { - description ??= type.GetCustomAttribute()?.Description; - } - - if (Nullable.GetUnderlyingType(type) is Type nullableElementType) - { - // Nullable types must be handled separately - JsonTypeInfo nullableElementTypeInfo = typeInfo.Options.GetTypeInfo(nullableElementType); - customConverter = ExtractCustomNullableConverter(customConverter); - - return MapJsonSchemaCore( - nullableElementTypeInfo, - ref state, - title, - description, - hasDefaultValue: hasDefaultValue, - defaultValue: defaultValue, - customNumberHandling: customNumberHandling, - customConverter: customConverter, - parentNullableOfT: type, - isNullableOfTElement: true); - } - - if (isCacheable && typeInfo.Kind != JsonTypeInfoKind.None) - { - // For complex types such objects, arrays, and dictionaries register the current path - // so that it can be referenced by later occurrences in the type graph. Do not register - // types in a polymorphic hierarchy using discriminators as they need to be inlined. - state.RegisterTypePath(type, parentNullableOfT, customConverter, isNullableReferenceType, customNumberHandling); - } - - JsonSchemaType schemaType = JsonSchemaType.Any; - string? format = null; - string? pattern = null; - JsonObject? properties = null; - JsonArray? requiredProperties = null; - JsonObject? arrayItems = null; - JsonNode? additionalProperties = null; - JsonArray? enumValues = null; - JsonArray? anyOfTypes = null; - - if (derivedTypeDiscriminator is null && typeInfo.PolymorphismOptions is { DerivedTypes.Count: > 0 } polyOptions) - { - // This is the base type of a polymorphic type hierarchy. The schema for this type - // will include an "anyOf" property with the schemas for all derived types. - - string typeDiscriminatorKey = polyOptions.TypeDiscriminatorPropertyName; - List derivedTypes = polyOptions.DerivedTypes.ToList(); - - if (!type.IsAbstract && derivedTypes.Any(derived => derived.DerivedType == type)) - { - // For non-abstract base types that haven't been explicitly configured, - // add a trivial schema to the derived types since we should support it. - derivedTypes.Add(new JsonDerivedType(type)); - } - - state.Push(AnyOfPropertyName); - anyOfTypes = []; - - int i = 0; - foreach (JsonDerivedType derivedType in derivedTypes) - { - Debug.Assert(derivedType.TypeDiscriminator is null or int or string); - JsonNode? typeDiscriminatorPropertySchema = derivedType.TypeDiscriminator switch - { - string stringId => new JsonObject { [ConstPropertyName] = (JsonNode)stringId }, - int intId => new JsonObject { [ConstPropertyName] = (JsonNode)intId }, - _ => null, - }; - - JsonTypeInfo derivedTypeInfo = typeInfo.Options.GetTypeInfo(derivedType.DerivedType); - - state.Push(i++.ToString(CultureInfo.InvariantCulture)); - JsonObject derivedSchema = MapJsonSchemaCore( - derivedTypeInfo, - ref state, - derivedTypeDiscriminator: new(typeDiscriminatorKey, typeDiscriminatorPropertySchema)); - state.Pop(); - - anyOfTypes.Add((JsonNode)derivedSchema); - } - - state.Pop(); - goto ConstructSchemaDocument; - } - - switch (typeInfo.Kind) - { - case JsonTypeInfoKind.None: - if (s_simpleTypeInfo.TryGetValue(type, out SimpleTypeJsonSchema simpleTypeInfo)) - { - schemaType = simpleTypeInfo.SchemaType; - format = simpleTypeInfo.Format; - pattern = simpleTypeInfo.Pattern; - - if (effectiveNumberHandling is JsonNumberHandling numberHandling && - schemaType is JsonSchemaType.Integer or JsonSchemaType.Number) - { - if ((numberHandling & (JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)) != 0) - { - schemaType |= JsonSchemaType.String; - } - else if (numberHandling is JsonNumberHandling.AllowNamedFloatingPointLiterals) - { - anyOfTypes = - [ - (JsonNode)new JsonObject { [TypePropertyName] = MapSchemaType(schemaType) }, - (JsonNode)new JsonObject - { - [EnumPropertyName] = new JsonArray { (JsonNode)"NaN", (JsonNode)"Infinity", (JsonNode)"-Infinity" }, - }, - ]; - - schemaType = JsonSchemaType.Any; // reset the parent setting - } - } - } - else if (type.IsEnum) - { - if (TryGetStringEnumConverterValues(typeInfo, effectiveConverter, out enumValues)) - { - schemaType = JsonSchemaType.String; - - if (enumValues != null && isNullableOfTElement) - { - // We're generating the schema for a nullable - // enum type. Append null to the "enum" array. - enumValues.Add(null); - } - } - else - { - schemaType = JsonSchemaType.Integer; - } - } - - break; - - case JsonTypeInfoKind.Object: - schemaType = JsonSchemaType.Object; - - if (typeInfo.UnmappedMemberHandling is JsonUnmappedMemberHandling.Disallow) - { - // Disallow unspecified properties. - additionalProperties = false; - } - - if (emitsTypeDiscriminator) - { - Debug.Assert(derivedTypeDiscriminator?.Value is not null); - (properties ??= []).Add(derivedTypeDiscriminator!.Value); - (requiredProperties ??= []).Add((JsonNode)derivedTypeDiscriminator.Value.Key); - } - - Func parameterInfoMapper = ResolveJsonConstructorParameterMapper(typeInfo); + private static readonly JsonSerializerOptions s_writeIndentedOptions = new() { WriteIndented = true }; - state.Push(PropertiesPropertyName); - foreach (JsonPropertyInfo property in typeInfo.Properties) - { - if (property is { Get: null, Set: null }) - { - continue; // Skip [JsonIgnore] property - } - - if (property.IsExtensionData) - { - continue; // Extension data properties don't impact the schema. - } - - JsonNumberHandling? propertyNumberHandling = property.NumberHandling ?? effectiveNumberHandling; - JsonTypeInfo propertyTypeInfo = typeInfo.Options.GetTypeInfo(property.PropertyType); - - // Only resolve nullability metadata for reference types. - NullabilityInfoContext? nullabilityCtx = !property.PropertyType.IsValueType ? state.NullabilityInfoContext : null; - - // Only resolve the attribute provider if needed. - ICustomAttributeProvider? attributeProvider = state.Configuration.ResolveDescriptionAttributes || nullabilityCtx is not null - ? ResolveAttributeProvider(typeInfo, property) - : null; - - // Resolve property-level description attributes. - string? propertyDescription = state.Configuration.ResolveDescriptionAttributes - ? attributeProvider?.GetCustomAttributes(inherit: true).OfType().FirstOrDefault()?.Description - : null; - - // Declare the property as nullable if either getter or setter are nullable. - bool isPropertyNullableReferenceType = nullabilityCtx is not null && attributeProvider is MemberInfo memberInfo - ? nullabilityCtx.GetMemberNullability(memberInfo) is { WriteState: NullabilityState.Nullable } or { ReadState: NullabilityState.Nullable } - : false; - - bool isRequired = property.IsRequired; - bool propertyHasDefaultValue = false; - JsonNode? propertyDefaultValue = null; - - if (parameterInfoMapper(property) is ParameterInfo ctorParam) - { - ResolveParameterInfo( - ctorParam, - propertyTypeInfo, - ref state, - ref propertyDescription, - ref propertyHasDefaultValue, - ref propertyDefaultValue, - ref isPropertyNullableReferenceType, - ref isRequired); - } - - state.Push(property.Name); - JsonObject propertySchema = MapJsonSchemaCore( - typeInfo: propertyTypeInfo, - state: ref state, - title: null, - description: propertyDescription, - isNullableReferenceType: isPropertyNullableReferenceType, - customConverter: property.CustomConverter, - hasDefaultValue: propertyHasDefaultValue, - defaultValue: propertyDefaultValue, - customNumberHandling: propertyNumberHandling); - - state.Pop(); - - (properties ??= []).Add(property.Name, propertySchema); - - if (isRequired) - { - (requiredProperties ??= []).Add((JsonNode)property.Name); - } - } - - state.Pop(); - break; - - case JsonTypeInfoKind.Enumerable: - Type elementType = GetElementType(typeInfo); - JsonTypeInfo elementTypeInfo = typeInfo.Options.GetTypeInfo(elementType); - - if (emitsTypeDiscriminator) - { - Debug.Assert(derivedTypeDiscriminator is not null); - - // Polymorphic enumerable types are represented using a wrapping object: - // { "$type" : "discriminator", "$values" : [element1, element2, ...] } - // Which corresponds to the schema - // { "properties" : { "$type" : { "const" : "discriminator" }, "$values" : { "type" : "array", "items" : { ... } } } } - - schemaType = JsonSchemaType.Object; - (properties ??= []).Add(derivedTypeDiscriminator!.Value); - (requiredProperties ??= []).Add((JsonNode)derivedTypeDiscriminator.Value.Key); - - state.Push(PropertiesPropertyName); - state.Push(StjValuesMetadataProperty); - state.Push(ItemsPropertyName); - JsonObject elementSchema = MapJsonSchemaCore(elementTypeInfo, ref state); - state.Pop(); - state.Pop(); - state.Pop(); - - properties.Add( - StjValuesMetadataProperty, - new JsonObject - { - [TypePropertyName] = MapSchemaType(JsonSchemaType.Array), - [ItemsPropertyName] = elementSchema, - }); - } - else - { - schemaType = JsonSchemaType.Array; - - state.Push(ItemsPropertyName); - arrayItems = MapJsonSchemaCore(elementTypeInfo, ref state); - state.Pop(); - } - - break; - - case JsonTypeInfoKind.Dictionary: - schemaType = JsonSchemaType.Object; - Type valueType = GetElementType(typeInfo); - JsonTypeInfo valueTypeInfo = typeInfo.Options.GetTypeInfo(valueType); + private static partial JsonNode MapRootTypeJsonSchema(JsonTypeInfo typeInfo, JsonSchemaMapperConfiguration configuration); - if (emitsTypeDiscriminator) - { - Debug.Assert(derivedTypeDiscriminator?.Value is not null); - (properties ??= []).Add(derivedTypeDiscriminator!.Value); - (requiredProperties ??= []).Add((JsonNode)derivedTypeDiscriminator.Value.Key); - } - - state.Push(AdditionalPropertiesPropertyName); - additionalProperties = MapJsonSchemaCore(valueTypeInfo, ref state); - state.Pop(); - break; - - default: - Debug.Fail("Unreachable code"); - break; - } + private static partial JsonNode MapMethodParameterJsonSchema( + ParameterInfo parameterInfo, + JsonTypeInfo parameterTypeInfo, + JsonSchemaMapperConfiguration configuration, + NullabilityInfoContext nullabilityContext, + out bool isRequired); - if (schemaType != JsonSchemaType.Any && - (type.IsValueType - ? parentNullableOfT is not null - : (isNullableReferenceType || state.Configuration.ReferenceTypeNullability is ReferenceTypeNullability.AlwaysNullable))) + private static void ValidateOptions(JsonSerializerOptions options) + { + if (options.ReferenceHandler == ReferenceHandler.Preserve) { - // Append "null" to the type array in the following cases: - // 1. The type is a nullable value type or - // 2. The type has been inferred to be a nullable reference type annotation or - // 3. The schema generator has been configured to always emit null for reference types (default STJ semantics). - schemaType |= JsonSchemaType.Null; + ThrowHelpers.ThrowNotSupportedException_ReferenceHandlerPreserveNotSupported(); } -ConstructSchemaDocument: - return CreateSchemaDocument( - ref state, - title, - description, - schemaType, - format, - pattern, - properties, - requiredProperties, - arrayItems, - additionalProperties, - enumValues, - anyOfTypes, - hasDefaultValue, - defaultValue); + options.MakeReadOnly(); } private static void ResolveParameterInfo( ParameterInfo parameter, JsonTypeInfo parameterTypeInfo, - ref GenerationState state, + NullabilityInfoContext nullabilityInfoContext, + JsonSchemaMapperConfiguration configuration, + out bool hasDefaultValue, + out JsonNode? defaultValue, + out bool isNonNullable, ref string? description, - ref bool hasDefaultValue, - ref JsonNode? defaultValue, - ref bool isNullableReferenceType, ref bool isRequired) { Debug.Assert(parameterTypeInfo.Type == parameter.ParameterType); - if (state.Configuration.ResolveDescriptionAttributes) + if (configuration.ResolveDescriptionAttributes) { // Resolve parameter-level description attributes. description ??= parameter.GetCustomAttribute()?.Description; } - if (!isNullableReferenceType && state.NullabilityInfoContext is { } ctx) - { - // Consult the nullability annotation of the constructor parameter if available. - isNullableReferenceType = ctx.GetParameterNullability(parameter) is NullabilityState.Nullable; - } + // Incorporate the nullability information from the parameter. + isNonNullable = nullabilityInfoContext.GetParameterNullability(parameter) is NullabilityState.NotNull; if (parameter.HasDefaultValue) { @@ -561,337 +218,194 @@ private static void ResolveParameterInfo( defaultValue = JsonSerializer.SerializeToNode(defaultVal, parameterTypeInfo); hasDefaultValue = true; } - else if (state.Configuration.RequireConstructorParameters) + else { // Parameter is not optional, mark as required. isRequired = true; + defaultValue = null; + hasDefaultValue = false; } } - private ref struct GenerationState + private static NullabilityState GetParameterNullability(this NullabilityInfoContext context, ParameterInfo parameterInfo) { - private readonly JsonSchemaMapperConfiguration _configuration; - private readonly NullabilityInfoContext? _nullabilityInfoContext; - private readonly Dictionary<(Type, JsonConverter? CustomConverter, bool IsNullableReferenceType, JsonNumberHandling? CustomNumberHandling), string>? _generatedTypePaths; - private readonly List? _currentPath; - private int _currentDepth; - - public GenerationState(JsonSchemaMapperConfiguration configuration) - { - _configuration = configuration; - _nullabilityInfoContext = configuration.ReferenceTypeNullability is ReferenceTypeNullability.Annotated ? new() : null; - _generatedTypePaths = configuration.AllowSchemaReferences ? new() : null; - _currentPath = configuration.AllowSchemaReferences ? new() : null; - _currentDepth = 0; - } - - public readonly JsonSchemaMapperConfiguration Configuration => _configuration; - public readonly NullabilityInfoContext? NullabilityInfoContext => _nullabilityInfoContext; - public readonly int CurrentDepth => _currentDepth; - - public void Push(string nodeId) +#if !NET9_0_OR_GREATER + // Workaround for https://github.com/dotnet/runtime/issues/92487 + if (GetGenericParameterDefinition(parameterInfo) is { ParameterType: { IsGenericParameter: true } typeParam }) { - if (_currentDepth == Configuration.MaxDepth) + // Step 1. Look for nullable annotations on the type parameter. + if (GetNullableFlags(typeParam) is byte[] flags) { - ThrowHelpers.ThrowInvalidOperationException_MaxDepthReached(); + return TranslateByte(flags[0]); } - _currentDepth++; + // Step 2. Look for nullable annotations on the generic method declaration. + if (typeParam.DeclaringMethod != null && GetNullableContextFlag(typeParam.DeclaringMethod) is byte flag) + { + return TranslateByte(flag); + } - if (Configuration.AllowSchemaReferences) + // Step 3. Look for nullable annotations on the generic method declaration. + if (GetNullableContextFlag(typeParam.DeclaringType!) is byte flag2) { - Debug.Assert(_currentPath is not null); - _currentPath!.Add(nodeId); + return TranslateByte(flag2); } - } - public void Pop() - { - Debug.Assert(_currentDepth > 0); - _currentDepth--; + // Default to nullable. + return NullabilityState.Nullable; - if (Configuration.AllowSchemaReferences) +#if NETCOREAPP + [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", + Justification = "We're resolving private fields of the built-in enum converter which cannot have been trimmed away.")] +#endif + static byte[]? GetNullableFlags(MemberInfo member) { - Debug.Assert(_currentPath is not null); - _currentPath!.RemoveAt(_currentPath.Count - 1); + Attribute? attr = member.GetCustomAttributes().FirstOrDefault(attr => + { + Type attrType = attr.GetType(); + return attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableAttribute"; + }); + + return (byte[])attr?.GetType().GetField("NullableFlags")?.GetValue(attr)!; } - } - /// - /// Associates the specified type configuration with the current path in the schema. - /// - public readonly void RegisterTypePath(Type type, Type? parentNullableOfT, JsonConverter? customConverter, bool isNullableReferenceType, JsonNumberHandling? customNumberHandling) - { - if (Configuration.AllowSchemaReferences) +#if NETCOREAPP + [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", + Justification = "We're resolving private fields of the built-in enum converter which cannot have been trimmed away.")] +#endif + static byte? GetNullableContextFlag(MemberInfo member) { - Debug.Assert(_currentPath is not null); - Debug.Assert(_generatedTypePaths is not null); + Attribute? attr = member.GetCustomAttributes().FirstOrDefault(attr => + { + Type attrType = attr.GetType(); + return attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableContextAttribute"; + }); - string pointer = _currentDepth == 0 ? "#" : "#/" + string.Join("/", _currentPath); - _generatedTypePaths!.Add((parentNullableOfT ?? type, customConverter, isNullableReferenceType, customNumberHandling), pointer); + return (byte?)attr?.GetType().GetField("Flag")?.GetValue(attr)!; } - } - /// - /// Looks up the schema path for the specified type configuration. - /// - public readonly bool TryGetGeneratedSchemaPath(Type type, Type? parentNullableOfT, JsonConverter? customConverter, bool isNullableReferenceType, JsonNumberHandling? customNumberHandling, [NotNullWhen(true)] out string? value) - { - if (Configuration.AllowSchemaReferences) + static NullabilityState TranslateByte(byte b) { - Debug.Assert(_generatedTypePaths is not null); - return _generatedTypePaths!.TryGetValue((parentNullableOfT ?? type, customConverter, isNullableReferenceType, customNumberHandling), out value); + return b switch + { + 1 => NullabilityState.NotNull, + 2 => NullabilityState.Nullable, + _ => NullabilityState.Unknown + }; } - - value = null; - return false; - } - } - - private static JsonObject CreateSchemaDocument( - ref GenerationState state, - string? title = null, - string? description = null, - JsonSchemaType schemaType = JsonSchemaType.Any, - string? format = null, - string? pattern = null, - JsonObject? properties = null, - JsonArray? requiredProperties = null, - JsonObject? arrayItems = null, - JsonNode? additionalProperties = null, - JsonArray? enumValues = null, - JsonArray? anyOfSchema = null, - bool hasDefaultValue = false, - JsonNode? defaultValue = null) - { - var schema = new JsonObject(); - - if (state.CurrentDepth == 0 && state.Configuration.IncludeSchemaVersion) - { - schema.Add(SchemaPropertyName, SchemaVersion); - } - - if (title is not null) - { - schema.Add(TitlePropertyName, title); } - if (description is not null) + static ParameterInfo GetGenericParameterDefinition(ParameterInfo parameter) { - schema.Add(DescriptionPropertyName, description); - } - - if (MapSchemaType(schemaType) is JsonNode type) - { - schema.Add(TypePropertyName, type); - } + if (parameter.Member is { DeclaringType.IsConstructedGenericType: true } + or MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false }) + { + var genericMethod = (MethodBase)GetGenericMemberDefinition(parameter.Member); + return genericMethod.GetParameters()[parameter.Position]; + } - if (format is not null) - { - schema.Add(FormatPropertyName, format); + return parameter; } - if (pattern is not null) +#if NETCOREAPP + [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", + Justification = "Looking up the generic member definition of the provided member.")] +#endif + static MemberInfo GetGenericMemberDefinition(MemberInfo member) { - schema.Add(PatternPropertyName, pattern); - } + if (member is Type type) + { + return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type; + } - if (properties is not null) - { - schema.Add(PropertiesPropertyName, properties); - } + if (member.DeclaringType!.IsConstructedGenericType) + { + const BindingFlags AllMemberFlags = + BindingFlags.Static | BindingFlags.Instance | + BindingFlags.Public | BindingFlags.NonPublic; - if (requiredProperties is not null) - { - schema.Add(RequiredPropertyName, requiredProperties); - } + return member.DeclaringType.GetGenericTypeDefinition() + .GetMember(member.Name, AllMemberFlags) + .First(m => m.MetadataToken == member.MetadataToken); + } - if (arrayItems is not null) - { - schema.Add(ItemsPropertyName, arrayItems); - } + if (member is MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false } method) + { + return method.GetGenericMethodDefinition(); + } - if (additionalProperties is not null) - { - schema.Add(AdditionalPropertiesPropertyName, additionalProperties); + return member; } +#endif + return context.Create(parameterInfo).WriteState; + } - if (enumValues is not null) - { - schema.Add(EnumPropertyName, enumValues); - } + // Taken from https://github.com/dotnet/runtime/blob/903bc019427ca07080530751151ea636168ad334/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs#L288-L317 + private static object? GetNormalizedDefaultValue(this ParameterInfo parameterInfo) + { + Type parameterType = parameterInfo.ParameterType; + object? defaultValue = parameterInfo.DefaultValue; - if (anyOfSchema is not null) + if (defaultValue is null) { - schema.Add(AnyOfPropertyName, anyOfSchema); + return null; } - if (hasDefaultValue) + // DBNull.Value is sometimes used as the default value (returned by reflection) of nullable params in place of null. + if (defaultValue == DBNull.Value && parameterType != typeof(DBNull)) { - schema.Add(DefaultPropertyName, defaultValue); + return null; } - return schema; - } - - [Flags] - private enum JsonSchemaType - { - Any = 0, // No type declared on the schema - Null = 1, - Boolean = 2, - Integer = 4, - Number = 8, - String = 16, - Array = 32, - Object = 64, - } - - private static readonly JsonSchemaType[] s_schemaValues = - [ - // NB the order of these values influences order of types in the rendered schema - JsonSchemaType.String, - JsonSchemaType.Integer, - JsonSchemaType.Number, - JsonSchemaType.Boolean, - JsonSchemaType.Array, - JsonSchemaType.Object, - JsonSchemaType.Null, - ]; - - private static JsonNode? MapSchemaType(JsonSchemaType schemaType) - { - return schemaType switch - { - JsonSchemaType.Any => null, - JsonSchemaType.Null => "null", - JsonSchemaType.Boolean => "boolean", - JsonSchemaType.Integer => "integer", - JsonSchemaType.Number => "number", - JsonSchemaType.String => "string", - JsonSchemaType.Array => "array", - JsonSchemaType.Object => "object", - _ => MapCompositeSchemaType(schemaType), - }; - - static JsonArray MapCompositeSchemaType(JsonSchemaType schemaType) + // Default values of enums or nullable enums are represented using the underlying type and need to be cast explicitly + // cf. https://github.com/dotnet/runtime/issues/68647 + if (parameterType.IsEnum) { - var array = new JsonArray(); - foreach (JsonSchemaType type in s_schemaValues) - { - if ((schemaType & type) != 0) - { - array.Add(MapSchemaType(type)); - } - } - - return array; + return Enum.ToObject(parameterType, defaultValue); } - } - private const string SchemaPropertyName = "$schema"; - private const string RefPropertyName = "$ref"; - private const string TitlePropertyName = "title"; - private const string DescriptionPropertyName = "description"; - private const string TypePropertyName = "type"; - private const string FormatPropertyName = "format"; - private const string PatternPropertyName = "pattern"; - private const string PropertiesPropertyName = "properties"; - private const string RequiredPropertyName = "required"; - private const string ItemsPropertyName = "items"; - private const string AdditionalPropertiesPropertyName = "additionalProperties"; - private const string EnumPropertyName = "enum"; - private const string AnyOfPropertyName = "anyOf"; - private const string ConstPropertyName = "const"; - private const string DefaultPropertyName = "default"; - private const string StjValuesMetadataProperty = "$values"; - - private readonly struct SimpleTypeJsonSchema - { - public SimpleTypeJsonSchema(JsonSchemaType schemaType, string? format = null, string? pattern = null) + if (Nullable.GetUnderlyingType(parameterType) is Type underlyingType && underlyingType.IsEnum) { - SchemaType = schemaType; - Format = format; - Pattern = pattern; + return Enum.ToObject(underlyingType, defaultValue); } - public JsonSchemaType SchemaType { get; } - public string? Format { get; } - public string? Pattern { get; } + return defaultValue; } - private static readonly Dictionary s_simpleTypeInfo = new() + private static class JsonSchemaConstants { - [typeof(object)] = new(JsonSchemaType.Any), - [typeof(bool)] = new(JsonSchemaType.Boolean), - [typeof(byte)] = new(JsonSchemaType.Integer), - [typeof(ushort)] = new(JsonSchemaType.Integer), - [typeof(uint)] = new(JsonSchemaType.Integer), - [typeof(ulong)] = new(JsonSchemaType.Integer), - [typeof(sbyte)] = new(JsonSchemaType.Integer), - [typeof(short)] = new(JsonSchemaType.Integer), - [typeof(int)] = new(JsonSchemaType.Integer), - [typeof(long)] = new(JsonSchemaType.Integer), - [typeof(float)] = new(JsonSchemaType.Number), - [typeof(double)] = new(JsonSchemaType.Number), - [typeof(decimal)] = new(JsonSchemaType.Number), -#if NET6_0_OR_GREATER - [typeof(Half)] = new(JsonSchemaType.Number), -#endif -#if NET7_0_OR_GREATER - [typeof(UInt128)] = new(JsonSchemaType.Integer), - [typeof(Int128)] = new(JsonSchemaType.Integer), -#endif - [typeof(char)] = new(JsonSchemaType.String), - [typeof(string)] = new(JsonSchemaType.String), - [typeof(byte[])] = new(JsonSchemaType.String), - [typeof(Memory)] = new(JsonSchemaType.String), - [typeof(ReadOnlyMemory)] = new(JsonSchemaType.String), - [typeof(DateTime)] = new(JsonSchemaType.String, format: "date-time"), - [typeof(DateTimeOffset)] = new(JsonSchemaType.String, format: "date-time"), - - // TimeSpan is represented as a string in the format "[-][d.]hh:mm:ss[.fffffff]". - [typeof(TimeSpan)] = new(JsonSchemaType.String, pattern: @"^-?(\d+\.)?\d{2}:\d{2}:\d{2}(\.\d{1,7})?$"), -#if NET6_0_OR_GREATER - [typeof(DateOnly)] = new(JsonSchemaType.String, format: "date"), - [typeof(TimeOnly)] = new(JsonSchemaType.String, format: "time"), -#endif - [typeof(Guid)] = new(JsonSchemaType.String, format: "uuid"), - [typeof(Uri)] = new(JsonSchemaType.String, format: "uri"), - [typeof(Version)] = new(JsonSchemaType.String), - [typeof(JsonDocument)] = new(JsonSchemaType.Any), - [typeof(JsonElement)] = new(JsonSchemaType.Any), - [typeof(JsonNode)] = new(JsonSchemaType.Any), - [typeof(JsonValue)] = new(JsonSchemaType.Any), - [typeof(JsonObject)] = new(JsonSchemaType.Object), - [typeof(JsonArray)] = new(JsonSchemaType.Array), - }; - - private static void ValidateOptions(JsonSerializerOptions options) - { - if (options.ReferenceHandler == ReferenceHandler.Preserve) - { - ThrowHelpers.ThrowNotSupportedException_ReferenceHandlerPreserveNotSupported(); - } - - options.MakeReadOnly(); + public const string SchemaPropertyName = "$schema"; + public const string RefPropertyName = "$ref"; + public const string CommentPropertyName = "$comment"; + public const string TitlePropertyName = "title"; + public const string DescriptionPropertyName = "description"; + public const string TypePropertyName = "type"; + public const string FormatPropertyName = "format"; + public const string PatternPropertyName = "pattern"; + public const string PropertiesPropertyName = "properties"; + public const string RequiredPropertyName = "required"; + public const string ItemsPropertyName = "items"; + public const string AdditionalPropertiesPropertyName = "additionalProperties"; + public const string EnumPropertyName = "enum"; + public const string NotPropertyName = "not"; + public const string AnyOfPropertyName = "anyOf"; + public const string ConstPropertyName = "const"; + public const string DefaultPropertyName = "default"; + public const string MinLengthPropertyName = "minLength"; + public const string MaxLengthPropertyName = "maxLength"; } - private static class ThrowHelpers + private static partial class ThrowHelpers { [DoesNotReturn] public static void ThrowArgumentNullException(string name) => throw new ArgumentNullException(name); - [DoesNotReturn] - public static void ThrowNotSupportedException_ReferenceHandlerPreserveNotSupported() => - throw new NotSupportedException("Schema generation not supported with ReferenceHandler.Preserve enabled."); - [DoesNotReturn] public static void ThrowInvalidOperationException_TrimmedMethodParameters(MethodBase method) => throw new InvalidOperationException($"The parameters for method '{method}' have been trimmed away."); [DoesNotReturn] - public static void ThrowInvalidOperationException_MaxDepthReached() => - throw new InvalidOperationException("The maximum depth of the schema has been reached."); + public static void ThrowNotSupportedException_ReferenceHandlerPreserveNotSupported() => + throw new NotSupportedException("Schema generation not supported with ReferenceHandler.Preserve enabled."); } } diff --git a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapperConfiguration.cs b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapperConfiguration.cs index 2bffb91b0e0c..78b3303df16d 100644 --- a/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapperConfiguration.cs +++ b/dotnet/src/InternalUtilities/src/Schema/JsonSchemaMapperConfiguration.cs @@ -2,7 +2,7 @@ using System; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Nodes; namespace JsonSchemaMapper; @@ -10,28 +10,17 @@ namespace JsonSchemaMapper; /// Controls the behavior of the class. /// #if EXPOSE_JSON_SCHEMA_MAPPER - public +public #else -[ExcludeFromCodeCoverage] internal #endif -class JsonSchemaMapperConfiguration + class JsonSchemaMapperConfiguration { /// /// Gets the default configuration object used by . /// public static JsonSchemaMapperConfiguration Default { get; } = new(); - private readonly int _maxDepth = 64; - - /// - /// Determines whether schema references using JSON pointers should be generated for repeated complex types. - /// - /// - /// Defaults to . Should be left enabled if recursive types (e.g. trees, linked lists) are expected. - /// - public bool AllowSchemaReferences { get; init; } = true; - /// /// Determines whether the '$schema' property should be included in the root schema document. /// @@ -49,45 +38,25 @@ class JsonSchemaMapperConfiguration public bool ResolveDescriptionAttributes { get; init; } = true; /// - /// Determines the nullability behavior of reference types in the generated schema. + /// Specifies whether the type keyword should be included in enum type schemas. /// /// - /// Defaults to . Currently JsonSerializer - /// doesn't recognize non-nullable reference types (https://github.com/dotnet/runtime/issues/1256) - /// so the serializer will always treat them as nullable. Setting to - /// improves accuracy of the generated schema with respect to the actual serialization behavior but can result in more noise. + /// Defaults to false. /// - public ReferenceTypeNullability ReferenceTypeNullability { get; init; } = ReferenceTypeNullability.Annotated; + public bool IncludeTypeInEnums { get; init; } /// - /// Dtermines whether properties bound to non-optional constructor parameters should be flagged as required. + /// Determines whether non-nullable schemas should be generated for null oblivious reference types. /// /// - /// Defaults to true. Current STJ treats all constructor parameters as optional - /// (https://github.com/dotnet/runtime/issues/100075) so disabling this option - /// will generate schemas that are more compatible with the actual serialization behavior. + /// Defaults to . Due to restrictions in the run-time representation of nullable reference types + /// most occurrences are null oblivious and are treated as nullable by the serializer. A notable exception to that rule + /// are nullability annotations of field, property and constructor parameters which are represented in the contract metadata. /// - public bool RequireConstructorParameters { get; init; } = true; + public bool TreatNullObliviousAsNonNullable { get; init; } /// - /// Determines the maximum permitted depth when traversing the generated type graph. + /// Defines a callback that is invoked for every schema that is generated within the type graph. /// - /// Thrown when the value is less than 0. - /// - /// Defaults to 64. - /// - public int MaxDepth - { - get => _maxDepth; - init - { - if (value < 0) - { - Throw(); - static void Throw() => throw new ArgumentOutOfRangeException(nameof(value)); - } - - _maxDepth = value; - } - } + public Func? TransformSchemaNode { get; init; } } diff --git a/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs b/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs index 9fa11e616c5a..f5f9bc07bce2 100644 --- a/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs +++ b/dotnet/src/InternalUtilities/src/Schema/KernelJsonSchemaBuilder.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -16,23 +18,38 @@ namespace Microsoft.SemanticKernel; // 1) Use the JSO from the Kernel used to create the KernelFunction when constructing the schema // 2) Check when the schema is being used (e.g. function calling) whether the JSO being used is equivalent to // whichever was used to build the schema, and if it's not, generate a new schema for that JSO - +[ExcludeFromCodeCoverage] internal static class KernelJsonSchemaBuilder { private static readonly JsonSerializerOptions s_options = CreateDefaultOptions(); - private static readonly JsonSchemaMapperConfiguration s_config = new() { IncludeSchemaVersion = false }; + private static readonly JsonSchemaMapperConfiguration s_config = new() + { + IncludeSchemaVersion = false, + IncludeTypeInEnums = true, + TreatNullObliviousAsNonNullable = true, + }; public static KernelJsonSchema Build(JsonSerializerOptions? options, Type type, string? description = null) { options ??= s_options; - JsonObject jsonObj = options.GetJsonSchema(type, s_config); + JsonNode jsonSchema = options.GetJsonSchema(type, s_config); + Debug.Assert(jsonSchema.GetValueKind() is JsonValueKind.Object or JsonValueKind.False or JsonValueKind.True); + + if (jsonSchema is not JsonObject jsonObj) + { + // Transform boolean schemas into object equivalents. + jsonObj = jsonSchema.GetValue() + ? new JsonObject() + : new JsonObject { ["not"] = true }; + } + if (!string.IsNullOrWhiteSpace(description)) { jsonObj["description"] = description; } - return KernelJsonSchema.Parse(JsonSerializer.Serialize(jsonObj, options)); + return KernelJsonSchema.Parse(jsonObj.ToJsonString(options)); } private static JsonSerializerOptions CreateDefaultOptions() diff --git a/dotnet/src/InternalUtilities/src/Schema/README.md b/dotnet/src/InternalUtilities/src/Schema/README.md index 6a22bac7b896..0ddddcbd1ac1 100644 --- a/dotnet/src/InternalUtilities/src/Schema/README.md +++ b/dotnet/src/InternalUtilities/src/Schema/README.md @@ -1,5 +1,5 @@ The *.cs files in this folder, other than KernelJsonSchemaBuilder.cs, are a direct copy of the code at -https://github.com/eiriktsarpalis/stj-schema-mapper/tree/b7d7f5a3794e48c45e2b5b0ab050d89aabfc94d6/src/JsonSchemaMapper. +https://github.com/eiriktsarpalis/stj-schema-mapper/tree/94b6d9b979f1a80a1c305605dfc6de3b7a6fe78b/src/JsonSchemaMapper. They should be kept in sync with any changes made in that repo, and should be removed once the relevant replacements are available in System.Text.Json. EXPOSE_JSON_SCHEMA_MAPPER should _not_ be defined so as to keep all of the functionality internal. diff --git a/dotnet/src/InternalUtilities/src/Schema/ReferenceTypeNullability.cs b/dotnet/src/InternalUtilities/src/Schema/ReferenceTypeNullability.cs deleted file mode 100644 index d373e9eeba64..000000000000 --- a/dotnet/src/InternalUtilities/src/Schema/ReferenceTypeNullability.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace JsonSchemaMapper; - -/// -/// Controls the nullable behavior of reference types in the generated schema. -/// -#if EXPOSE_JSON_SCHEMA_MAPPER - public -#else -internal -#endif -enum ReferenceTypeNullability -{ - /// - /// Always treat reference types as nullable. Follows the built-in behavior - /// of the serializer (cf. https://github.com/dotnet/runtime/issues/1256). - /// - AlwaysNullable, - - /// - /// Treat reference types as nullable only if they are annotated with a nullable reference type modifier. - /// - Annotated, - - /// - /// Always treat reference types as non-nullable. - /// - NeverNullable, -} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatPromptParser.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatPromptParser.cs index c9cae7acb070..12d63de28d3c 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatPromptParser.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatPromptParser.cs @@ -75,7 +75,14 @@ private static ChatMessageContent ParseChatNode(PromptNode node) { if (childNode.TagName.Equals(ImageTagName, StringComparison.OrdinalIgnoreCase)) { - items.Add(new ImageContent(new Uri(childNode.Content!))); + if (childNode.Content!.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + { + items.Add(new ImageContent(childNode.Content)); + } + else + { + items.Add(new ImageContent(new Uri(childNode.Content!))); + } } else if (childNode.TagName.Equals(TextTagName, StringComparison.OrdinalIgnoreCase)) { diff --git a/dotnet/src/SemanticKernel.UnitTests/Prompt/ChatPromptParserTests.cs b/dotnet/src/SemanticKernel.UnitTests/Prompt/ChatPromptParserTests.cs index ecb051b7d7b1..e3ad0cd53a5c 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Prompt/ChatPromptParserTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Prompt/ChatPromptParserTests.cs @@ -114,6 +114,40 @@ public void ItReturnsChatHistoryWithValidContentItemsIncludeCData() """, c.Content)); } + [Fact] + public void ItReturnsChatHistoryWithValidDataImageContent() + { + // Arrange + string prompt = GetValidPromptWithDataUriImageContent(); + + // Act + bool result = ChatPromptParser.TryParse(prompt, out var chatHistory); + + // Assert + Assert.True(result); + Assert.NotNull(chatHistory); + + Assert.Collection(chatHistory, + c => Assert.Equal("What can I help with?", c.Content), + c => + { + Assert.Equal("Explain this image", c.Content); + Assert.Collection(c.Items, + o => + { + Assert.IsType(o); + Assert.Equal("Explain this image", ((TextContent)o).Text); + }, + o => + { + Assert.IsType(o); + Assert.Equal("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAACVJREFUKFNj/KTO/J+BCMA4iBUyQX1A0I10VAizCj1oMdyISyEAFoQbHwTcuS8AAAAASUVORK5CYII=", ((ImageContent)o).DataUri); + Assert.Equal("image/png", ((ImageContent)o).MimeType); + Assert.NotNull(((ImageContent)o).Data); + }); + }); + } + [Fact] public void ItReturnsChatHistoryWithValidContentItemsIncludeCode() { @@ -210,6 +244,21 @@ Second line. """; } + private static string GetValidPromptWithDataUriImageContent() + { + return + """ + + What can I help with? + + + Explain this image + data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAACVJREFUKFNj/KTO/J+BCMA4iBUyQX1A0I10VAizCj1oMdyISyEAFoQbHwTcuS8AAAAASUVORK5CYII= + + + """; + } + private static string GetValidPromptWithCDataSection() { return diff --git a/python/.pre-commit-config.yaml b/python/.pre-commit-config.yaml index 56ae96fa7e1e..5c3521dd142d 100644 --- a/python/.pre-commit-config.yaml +++ b/python/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: - id: mypy files: ^python/semantic_kernel/ name: mypy - entry: cd python && uv run mypy -p semantic_kernel --config-file mypy.ini + entry: uv run mypy -p semantic_kernel --config-file python/mypy.ini language: system types: [python] pass_filenames: false diff --git a/python/.vscode/tasks.json b/python/.vscode/tasks.json index 9a6a7ecbfd69..dbd972976939 100644 --- a/python/.vscode/tasks.json +++ b/python/.vscode/tasks.json @@ -122,7 +122,7 @@ "pytest", "tests/unit/", "--last-failed", - "-v" + "-vv" ], "group": "test", "presentation": { diff --git a/python/pyproject.toml b/python/pyproject.toml index f59e68580514..c972af15f722 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -12,6 +12,7 @@ urls.source = "https://github.com/microsoft/semantic-kernel/tree/main/python" urls.release_notes = "https://github.com/microsoft/semantic-kernel/releases?q=tag%3Apython-1&expanded=true" urls.issues = "https://github.com/microsoft/semantic-kernel/issues" classifiers = [ + "License :: OSI Approved :: MIT License", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", diff --git a/python/samples/getting_started/00-getting-started.ipynb b/python/samples/getting_started/00-getting-started.ipynb index d6bcc8a6adf8..c7997783b82e 100644 --- a/python/samples/getting_started/00-getting-started.ipynb +++ b/python/samples/getting_started/00-getting-started.ipynb @@ -232,4 +232,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/python/samples/getting_started/05-using-the-planner.ipynb b/python/samples/getting_started/05-using-the-planner.ipynb index 961a37591f01..c8e7f4d58994 100644 --- a/python/samples/getting_started/05-using-the-planner.ipynb +++ b/python/samples/getting_started/05-using-the-planner.ipynb @@ -586,4 +586,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/python/semantic_kernel/__init__.py b/python/semantic_kernel/__init__.py index 003edae59a41..b62be420e7b0 100644 --- a/python/semantic_kernel/__init__.py +++ b/python/semantic_kernel/__init__.py @@ -2,5 +2,5 @@ from semantic_kernel.kernel import Kernel -__version__ = "1.8.1" +__version__ = "1.8.2" __all__ = ["Kernel", "__version__"] diff --git a/python/semantic_kernel/contents/chat_message_content.py b/python/semantic_kernel/contents/chat_message_content.py index ced273de75a2..1b52a8c9ea65 100644 --- a/python/semantic_kernel/contents/chat_message_content.py +++ b/python/semantic_kernel/contents/chat_message_content.py @@ -3,7 +3,7 @@ import logging from enum import Enum from html import unescape -from typing import Any, ClassVar, Literal, Union, overload +from typing import Annotated, Any, ClassVar, Literal, overload from xml.etree.ElementTree import Element # nosec from defusedxml import ElementTree @@ -26,7 +26,6 @@ from semantic_kernel.contents.function_result_content import FunctionResultContent from semantic_kernel.contents.image_content import ImageContent from semantic_kernel.contents.kernel_content import KernelContent -from semantic_kernel.contents.streaming_text_content import StreamingTextContent from semantic_kernel.contents.text_content import TextContent from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.contents.utils.finish_reason import FinishReason @@ -41,15 +40,9 @@ IMAGE_CONTENT_TAG: ImageContent, } -ITEM_TYPES = Union[ - AnnotationContent, - ImageContent, - TextContent, - StreamingTextContent, - FunctionResultContent, - FunctionCallContent, - FileReferenceContent, -] +ITEM_TYPES = ( + AnnotationContent | ImageContent | TextContent | FunctionResultContent | FunctionCallContent | FileReferenceContent +) logger = logging.getLogger(__name__) @@ -78,7 +71,7 @@ class ChatMessageContent(KernelContent): tag: ClassVar[str] = CHAT_MESSAGE_CONTENT_TAG role: AuthorRole name: str | None = None - items: list[ITEM_TYPES] = Field(default_factory=list, discriminator=DISCRIMINATOR_FIELD) + items: list[Annotated[ITEM_TYPES, Field(..., discriminator=DISCRIMINATOR_FIELD)]] = Field(default_factory=list) encoding: str | None = None finish_reason: FinishReason | None = None diff --git a/python/semantic_kernel/functions/kernel_arguments.py b/python/semantic_kernel/functions/kernel_arguments.py index f6fa8060fe71..573b512d1f1d 100644 --- a/python/semantic_kernel/functions/kernel_arguments.py +++ b/python/semantic_kernel/functions/kernel_arguments.py @@ -43,3 +43,9 @@ def __init__( else: settings_dict = {settings.service_id or DEFAULT_SERVICE_NAME: settings} self.execution_settings: dict[str, "PromptExecutionSettings"] | None = settings_dict + + def __bool__(self) -> bool: + """Returns True if the arguments have any values.""" + has_arguments = self.__len__() > 0 + has_execution_settings = self.execution_settings is not None and len(self.execution_settings) > 0 + return has_arguments or has_execution_settings diff --git a/python/semantic_kernel/kernel.py b/python/semantic_kernel/kernel.py index 34540b0443e8..3fd5d33dcc1d 100644 --- a/python/semantic_kernel/kernel.py +++ b/python/semantic_kernel/kernel.py @@ -41,9 +41,7 @@ from semantic_kernel.utils.naming import generate_random_ascii_name if TYPE_CHECKING: - from semantic_kernel.connectors.ai.function_choice_behavior import ( - FunctionChoiceBehavior, - ) + from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings from semantic_kernel.functions.kernel_function import KernelFunction @@ -239,7 +237,7 @@ async def invoke_prompt( Returns: FunctionResult | list[FunctionResult] | None: The result of the function(s) """ - if not arguments: + if arguments is None: arguments = KernelArguments(**kwargs) if not prompt: raise TemplateSyntaxError("The prompt is either null or empty.") @@ -280,7 +278,7 @@ async def invoke_prompt_stream( Returns: AsyncIterable[StreamingContentMixin]: The content of the stream of the last function provided. """ - if not arguments: + if arguments is None: arguments = KernelArguments(**kwargs) if not prompt: raise TemplateSyntaxError("The prompt is either null or empty.") diff --git a/python/tests/unit/connectors/anthropic/services/test_anthropic_chat_completion.py b/python/tests/unit/connectors/anthropic/services/test_anthropic_chat_completion.py index 11c7882b49df..1f1168e970f6 100644 --- a/python/tests/unit/connectors/anthropic/services/test_anthropic_chat_completion.py +++ b/python/tests/unit/connectors/anthropic/services/test_anthropic_chat_completion.py @@ -3,6 +3,21 @@ import pytest from anthropic import AsyncAnthropic +from anthropic.lib.streaming import TextEvent +from anthropic.types import ( + ContentBlockStopEvent, + Message, + MessageDeltaUsage, + MessageStopEvent, + RawContentBlockDeltaEvent, + RawContentBlockStartEvent, + RawMessageDeltaEvent, + RawMessageStartEvent, + TextBlock, + TextDelta, + Usage, +) +from anthropic.types.raw_message_delta_event import Delta from semantic_kernel.connectors.ai.anthropic.prompt_execution_settings.anthropic_prompt_execution_settings import ( AnthropicChatPromptExecutionSettings, @@ -12,6 +27,7 @@ from semantic_kernel.connectors.ai.open_ai.prompt_execution_settings.open_ai_prompt_execution_settings import ( OpenAIChatPromptExecutionSettings, ) +from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.exceptions import ServiceInitializationError, ServiceResponseException from semantic_kernel.functions.kernel_arguments import KernelArguments @@ -26,43 +42,120 @@ def mock_settings() -> AnthropicChatPromptExecutionSettings: @pytest.fixture def mock_anthropic_client_completion() -> AsyncAnthropic: client = MagicMock(spec=AsyncAnthropic) + chat_completion_response = AsyncMock() - - content = [MagicMock(finish_reason="stop", message=MagicMock(role="assistant", content="Test"))] - chat_completion_response.content = content + chat_completion_response.content = [TextBlock(text="Hello! It's nice to meet you.", type="text")] + chat_completion_response.id = "test_id" + chat_completion_response.model = "claude-3-opus-20240229" + chat_completion_response.role = "assistant" + chat_completion_response.stop_reason = "end_turn" + chat_completion_response.stop_sequence = None + chat_completion_response.type = "message" + chat_completion_response.usage = Usage(input_tokens=114, output_tokens=75) # Create a MagicMock for the messages attribute messages_mock = MagicMock() - messages_mock.create = chat_completion_response + messages_mock.create = AsyncMock(return_value=chat_completion_response) # Assign the messages_mock to the client.messages attribute client.messages = messages_mock + return client @pytest.fixture def mock_anthropic_client_completion_stream() -> AsyncAnthropic: client = MagicMock(spec=AsyncAnthropic) - chat_completion_response = MagicMock() - content = [ - MagicMock(finish_reason="stop", delta=MagicMock(role="assistant", content="Test")), - MagicMock(finish_reason="stop", delta=MagicMock(role="assistant", content="Test", tool_calls=None)), + # Create MagicMock instances for each event with the spec set to the appropriate class + mock_raw_message_start_event = MagicMock(spec=RawMessageStartEvent) + mock_raw_message_start_event.message = MagicMock(spec=Message) + mock_raw_message_start_event.message.id = "test_message_id" + mock_raw_message_start_event.message.content = [] + mock_raw_message_start_event.message.model = "claude-3-opus-20240229" + mock_raw_message_start_event.message.role = "assistant" + mock_raw_message_start_event.message.stop_reason = None + mock_raw_message_start_event.message.stop_sequence = None + mock_raw_message_start_event.message.type = "message" + mock_raw_message_start_event.message.usage = MagicMock(spec=Usage) + mock_raw_message_start_event.message.usage.input_tokens = 41 + mock_raw_message_start_event.message.usage.output_tokens = 3 + mock_raw_message_start_event.type = "message_start" + + mock_raw_content_block_start_event = MagicMock(spec=RawContentBlockStartEvent) + mock_raw_content_block_start_event.content_block = MagicMock(spec=TextBlock) + mock_raw_content_block_start_event.content_block.text = "" + mock_raw_content_block_start_event.content_block.type = "text" + mock_raw_content_block_start_event.index = 0 + mock_raw_content_block_start_event.type = "content_block_start" + + mock_raw_content_block_delta_event = MagicMock(spec=RawContentBlockDeltaEvent) + mock_raw_content_block_delta_event.delta = MagicMock(spec=TextDelta) + mock_raw_content_block_delta_event.delta.text = "Hello! It" + mock_raw_content_block_delta_event.delta.type = "text_delta" + mock_raw_content_block_delta_event.index = 0 + mock_raw_content_block_delta_event.type = "content_block_delta" + + mock_text_event = MagicMock(spec=TextEvent) + mock_text_event.type = "text" + mock_text_event.text = "Hello! It" + mock_text_event.snapshot = "Hello! It" + + mock_content_block_stop_event = MagicMock(spec=ContentBlockStopEvent) + mock_content_block_stop_event.index = 0 + mock_content_block_stop_event.type = "content_block_stop" + mock_content_block_stop_event.content_block = MagicMock(spec=TextBlock) + mock_content_block_stop_event.content_block.text = "Hello! It's nice to meet you." + mock_content_block_stop_event.content_block.type = "text" + + mock_raw_message_delta_event = MagicMock(spec=RawMessageDeltaEvent) + mock_raw_message_delta_event.delta = MagicMock(spec=Delta) + mock_raw_message_delta_event.delta.stop_reason = "end_turn" + mock_raw_message_delta_event.delta.stop_sequence = None + mock_raw_message_delta_event.type = "message_delta" + mock_raw_message_delta_event.usage = MagicMock(spec=MessageDeltaUsage) + mock_raw_message_delta_event.usage.output_tokens = 84 + + mock_message_stop_event = MagicMock(spec=MessageStopEvent) + mock_message_stop_event.type = "message_stop" + mock_message_stop_event.message = MagicMock(spec=Message) + mock_message_stop_event.message.id = "test_message_stop_id" + mock_message_stop_event.message.content = [MagicMock(spec=TextBlock)] + mock_message_stop_event.message.content[0].text = "Hello! It's nice to meet you." + mock_message_stop_event.message.content[0].type = "text" + mock_message_stop_event.message.model = "claude-3-opus-20240229" + mock_message_stop_event.message.role = "assistant" + mock_message_stop_event.message.stop_reason = "end_turn" + mock_message_stop_event.message.stop_sequence = None + mock_message_stop_event.message.type = "message" + mock_message_stop_event.message.usage = MagicMock(spec=Usage) + mock_message_stop_event.message.usage.input_tokens = 41 + mock_message_stop_event.message.usage.output_tokens = 84 + + # Combine all mock events into a list + stream_events = [ + mock_raw_message_start_event, + mock_raw_content_block_start_event, + mock_raw_content_block_delta_event, + mock_text_event, + mock_content_block_stop_event, + mock_raw_message_delta_event, + mock_message_stop_event, ] - chat_completion_response.content = content - chat_completion_response_empty = MagicMock() - chat_completion_response_empty.content = [] + async def async_generator(): + for event in stream_events: + yield event + + # Create an AsyncMock for the stream + stream_mock = AsyncMock() + stream_mock.__aenter__.return_value = async_generator() - # Create a MagicMock for the messages attribute messages_mock = MagicMock() - messages_mock.stream = chat_completion_response - - generator_mock = MagicMock() - generator_mock.__aiter__.return_value = [chat_completion_response_empty, chat_completion_response] - + messages_mock.stream.return_value = stream_mock + client.messages = messages_mock - + return client @@ -72,7 +165,10 @@ async def test_complete_chat_contents( mock_settings: AnthropicChatPromptExecutionSettings, mock_anthropic_client_completion: AsyncAnthropic, ): - chat_history = MagicMock() + chat_history = ChatHistory() + chat_history.add_user_message("test_user_message") + chat_history.add_assistant_message("test_assistant_message") + arguments = KernelArguments() chat_completion_base = AnthropicChatCompletion( ai_model_id="test_model_id", service_id="test", api_key="", async_client=mock_anthropic_client_completion @@ -81,6 +177,7 @@ async def test_complete_chat_contents( content: list[ChatMessageContent] = await chat_completion_base.get_chat_message_contents( chat_history=chat_history, settings=mock_settings, kernel=kernel, arguments=arguments ) + assert content is not None diff --git a/python/tests/unit/contents/test_chat_message_content.py b/python/tests/unit/contents/test_chat_message_content.py index 10997b9a0d98..9e7dcaa07b8a 100644 --- a/python/tests/unit/contents/test_chat_message_content.py +++ b/python/tests/unit/contents/test_chat_message_content.py @@ -284,8 +284,98 @@ def test_cmc_to_dict_keys(): "content": [{"type": "text", "text": "Hello, "}, {"type": "text", "text": "world!"}], }, ), + ( + { + "role": "user", + "items": [ + {"content_type": "text", "text": "Hello, "}, + {"content_type": "text", "text": "world!"}, + ], + }, + { + "role": "user", + "content": [{"type": "text", "text": "Hello, "}, {"type": "text", "text": "world!"}], + }, + ), + ( + { + "role": "user", + "items": [ + {"content_type": "annotation", "file_id": "test"}, + ], + }, + { + "role": "user", + "content": [{"type": "text", "text": "test None (Start Index=None->End Index=None)"}], + }, + ), + ( + { + "role": "user", + "items": [ + {"content_type": "file_reference", "file_id": "test"}, + ], + }, + { + "role": "user", + "content": [{"file_id": "test"}], + }, + ), + ( + { + "role": "user", + "items": [ + {"content_type": "function_call", "name": "test-test"}, + ], + }, + { + "role": "user", + "content": [{"id": None, "type": "function", "function": {"name": "test-test", "arguments": None}}], + }, + ), + ( + { + "role": "user", + "items": [ + {"content_type": "function_call", "name": "test-test"}, + {"content_type": "function_result", "name": "test-test", "result": "test", "id": "test"}, + ], + }, + { + "role": "user", + "content": [ + {"id": None, "type": "function", "function": {"name": "test-test", "arguments": None}}, + {"tool_call_id": "test", "content": "test"}, + ], + }, + ), + ( + { + "role": "user", + "items": [ + {"content_type": "image", "uri": "http://test"}, + ], + }, + { + "role": "user", + "content": [{"image_url": {"url": "http://test/"}, "type": "image_url"}], + }, + ), + ], + ids=[ + "user_content", + "user_with_name", + "user_item", + "function_call", + "function_result", + "multiple_items", + "multiple_items_serialize", + "annotations_serialize", + "file_reference_serialize", + "function_call_serialize", + "function_result_serialize", + "image_serialize", ], - ids=["user_content", "user_with_name", "user_item", "function_call", "function_result", "multiple_items"], ) def test_cmc_to_dict_items(input_args, expected_dict): message = ChatMessageContent(**input_args) diff --git a/python/tests/unit/functions/test_kernel_arguments.py b/python/tests/unit/functions/test_kernel_arguments.py index 155fda0ec079..9e07a3a14ce7 100644 --- a/python/tests/unit/functions/test_kernel_arguments.py +++ b/python/tests/unit/functions/test_kernel_arguments.py @@ -35,3 +35,14 @@ def test_kernel_arguments_with_execution_settings(): kargs = KernelArguments(settings=[test_pes]) assert kargs is not None assert kargs.execution_settings == {"test": test_pes} + + +def test_kernel_arguments_bool(): + # An empty KernelArguments object should return False + assert not KernelArguments() + # An KernelArguments object with keyword arguments should return True + assert KernelArguments(input=10) + # An KernelArguments object with execution_settings should return True + assert KernelArguments(settings=PromptExecutionSettings(service_id="test")) + # An KernelArguments object with both keyword arguments and execution_settings should return True + assert KernelArguments(input=10, settings=PromptExecutionSettings(service_id="test")) From 972aff666abcf1343ff1afb6b7429e12c2087604 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:59:27 -0700 Subject: [PATCH 83/87] .Net: Update package version (#8496) --- dotnet/Directory.Build.props | 2 +- dotnet/nuget/nuget-package.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props index 452ceb8542ac..94d748c78057 100644 --- a/dotnet/Directory.Build.props +++ b/dotnet/Directory.Build.props @@ -13,7 +13,7 @@ - true + false diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index 67cec6d1f4dc..851e17bc86f9 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -1,7 +1,7 @@ - 1.18.1 + 1.18.2 $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix) From 541ca7ca6c50305fee5e7685bee7bc058664478a Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:47:30 -0700 Subject: [PATCH 84/87] .Net: [Feature Branch] Added required configuration (#8498) --- .github/workflows/dotnet-build-and-test.yml | 2 ++ dotnet/src/IntegrationTests/testsettings.json | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index c53353de8b3f..25b8ac3f79ed 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -96,6 +96,7 @@ jobs: AzureOpenAI__Label: azure-text-davinci-003 AzureOpenAIEmbedding__Label: azure-text-embedding-ada-002 AzureOpenAI__DeploymentName: ${{ vars.AZUREOPENAI__DEPLOYMENTNAME }} + AzureOpenAI__ChatDeploymentName: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }} AzureOpenAIEmbeddings__DeploymentName: ${{ vars.AZUREOPENAIEMBEDDING__DEPLOYMENTNAME }} AzureOpenAI__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }} AzureOpenAIEmbeddings__Endpoint: ${{ secrets.AZUREOPENAI_EASTUS__ENDPOINT }} @@ -118,6 +119,7 @@ jobs: AzureOpenAIAudioToText__DeploymentName: ${{ vars.AZUREOPENAIAUDIOTOTEXT__DEPLOYMENTNAME }} Bing__ApiKey: ${{ secrets.BING__APIKEY }} OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }} + OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }} # Generate test reports and check coverage - name: Generate test reports diff --git a/dotnet/src/IntegrationTests/testsettings.json b/dotnet/src/IntegrationTests/testsettings.json index fd551e3b5d84..11799c7d5fc1 100644 --- a/dotnet/src/IntegrationTests/testsettings.json +++ b/dotnet/src/IntegrationTests/testsettings.json @@ -2,12 +2,13 @@ "OpenAI": { "ServiceId": "gpt-3.5-turbo-instruct", "ModelId": "gpt-3.5-turbo-instruct", + "ChatModelId": "gpt-4o", "ApiKey": "" }, "AzureOpenAI": { "ServiceId": "azure-gpt-35-turbo-instruct", "DeploymentName": "gpt-35-turbo-instruct", - "ChatDeploymentName": "gpt-4", + "ChatDeploymentName": "gpt-4o", "Endpoint": "", "ApiKey": "" }, From 81ca740f3130f545bbe116ba972f5a84431ae0f0 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:32:20 -0700 Subject: [PATCH 85/87] .Net: [Feature Branch] Added configuration for text-to-image service (#8499) --- .github/workflows/dotnet-build-and-test.yml | 3 +++ dotnet/src/IntegrationTests/testsettings.json | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index 25b8ac3f79ed..f1a692b80074 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -117,6 +117,9 @@ jobs: AzureOpenAIAudioToText__ApiKey: ${{ secrets.AZUREOPENAIAUDIOTOTEXT__APIKEY }} AzureOpenAIAudioToText__Endpoint: ${{ secrets.AZUREOPENAIAUDIOTOTEXT__ENDPOINT }} AzureOpenAIAudioToText__DeploymentName: ${{ vars.AZUREOPENAIAUDIOTOTEXT__DEPLOYMENTNAME }} + AzureOpenAITextToImage__ApiKey: ${{ secrets.AZUREOPENAITEXTTOIMAGE__APIKEY }} + AzureOpenAITextToImage__Endpoint: ${{ secrets.AZUREOPENAITEXTTOIMAGE__ENDPOINT }} + AzureOpenAITextToImage__DeploymentName: ${{ vars.AZUREOPENAITEXTTOIMAGE__DEPLOYMENTNAME }} Bing__ApiKey: ${{ secrets.BING__APIKEY }} OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }} OpenAI__ChatModelId: ${{ vars.OPENAI__CHATMODELID }} diff --git a/dotnet/src/IntegrationTests/testsettings.json b/dotnet/src/IntegrationTests/testsettings.json index 11799c7d5fc1..c3a0c04301b5 100644 --- a/dotnet/src/IntegrationTests/testsettings.json +++ b/dotnet/src/IntegrationTests/testsettings.json @@ -45,6 +45,12 @@ "Endpoint": "", "ApiKey": "" }, + "AzureOpenAITextToImage": { + "ServiceId": "azure-dalle3", + "DeploymentName": "dall-e-3", + "Endpoint": "", + "ApiKey": "" + }, "HuggingFace": { "ApiKey": "" }, From 02d2208ef72ee68ba8208f52a9ca085627e28d34 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:25:34 -0700 Subject: [PATCH 86/87] .Net: [Feature Branch] Updated integration tests (#8500) --- .github/workflows/dotnet-build-and-test.yml | 2 ++ .../Planners/Handlebars/HandlebarsPlannerTests.cs | 6 +++--- dotnet/src/IntegrationTests/testsettings.json | 4 ++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index f1a692b80074..dd83478b508b 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -111,6 +111,8 @@ jobs: OpenAITextToAudio__ModelId: ${{ vars.OPENAITEXTTOAUDIO__MODELID }} OpenAIAudioToText__ApiKey: ${{ secrets.OPENAIAUDIOTOTEXT__APIKEY }} OpenAIAudioToText__ModelId: ${{ vars.OPENAIAUDIOTOTEXT__MODELID }} + OpenAITextToImage__ApiKey: ${{ secrets.OPENAITEXTTOIMAGE__APIKEY }} + OpenAITextToImage__ModelId: ${{ vars.OPENAITEXTTOIMAGE__MODELID }} AzureOpenAITextToAudio__ApiKey: ${{ secrets.AZUREOPENAITEXTTOAUDIO__APIKEY }} AzureOpenAITextToAudio__Endpoint: ${{ secrets.AZUREOPENAITEXTTOAUDIO__ENDPOINT }} AzureOpenAITextToAudio__DeploymentName: ${{ vars.AZUREOPENAITEXTTOAUDIO__DEPLOYMENTNAME }} diff --git a/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlannerTests.cs b/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlannerTests.cs index 5ed6d6364d6d..bae2d4b98742 100644 --- a/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlannerTests.cs +++ b/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlannerTests.cs @@ -16,7 +16,7 @@ namespace SemanticKernel.IntegrationTests.Planners.Handlebars; public sealed class HandlebarsPlannerTests { - [Theory] + [Theory(Skip = "This test is for manual verification.")] [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) { @@ -37,7 +37,7 @@ public async Task CreatePlanFunctionFlowAsync(string goal, string expectedFuncti ); } - [RetryTheory] + [RetryTheory(Skip = "This test is for manual verification.")] [InlineData("Write a novel about software development that is 3 chapters long.", "NovelChapter", "WriterPlugin")] public async Task CreatePlanWithDefaultsAsync(string goal, string expectedFunction, string expectedPlugin) { @@ -56,7 +56,7 @@ public async Task CreatePlanWithDefaultsAsync(string goal, string expectedFuncti ); } - [Theory] + [Theory(Skip = "This test is for manual verification.")] [InlineData("List each property of the default Qux object.", "## Complex types", """ ### Qux: { diff --git a/dotnet/src/IntegrationTests/testsettings.json b/dotnet/src/IntegrationTests/testsettings.json index c3a0c04301b5..131881900692 100644 --- a/dotnet/src/IntegrationTests/testsettings.json +++ b/dotnet/src/IntegrationTests/testsettings.json @@ -45,6 +45,10 @@ "Endpoint": "", "ApiKey": "" }, + "OpenAITextToImage": { + "ModelId": "dall-e-2", + "ApiKey": "" + }, "AzureOpenAITextToImage": { "ServiceId": "azure-dalle3", "DeploymentName": "dall-e-3", From 9c2ee04e7d2860cb89cba3c08cad487710edbc83 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:59:14 -0700 Subject: [PATCH 87/87] Added service id to OpenAI text-to-image config --- dotnet/src/IntegrationTests/testsettings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/src/IntegrationTests/testsettings.json b/dotnet/src/IntegrationTests/testsettings.json index 131881900692..40c064f078c5 100644 --- a/dotnet/src/IntegrationTests/testsettings.json +++ b/dotnet/src/IntegrationTests/testsettings.json @@ -46,6 +46,7 @@ "ApiKey": "" }, "OpenAITextToImage": { + "ServiceId": "dall-e-2", "ModelId": "dall-e-2", "ApiKey": "" },