mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
Merge remote-tracking branch 'upstream/main' into java-v1
This commit is contained in:
@@ -109,6 +109,16 @@ jobs:
|
||||
Planners__AzureOpenAI__DeploymentName: ${{ vars.PLANNERS__AZUREOPENAI__DEPLOYMENTNAME }}
|
||||
Planners__OpenAI__ApiKey: ${{ secrets.PLANNERS__OPENAI__APIKEY }}
|
||||
Planners__OpenAI__ModelId: ${{ vars.PLANNERS__OPENAI__MODELID }}
|
||||
OpenAITextToAudio__ApiKey: ${{ secrets.OPENAITEXTTOAUDIO__APIKEY }}
|
||||
OpenAITextToAudio__ModelId: ${{ vars.OPENAITEXTTOAUDIO__MODELID }}
|
||||
OpenAIAudioToText__ApiKey: ${{ secrets.OPENAIAUDIOTOTEXT__APIKEY }}
|
||||
OpenAIAudioToText__ModelId: ${{ vars.OPENAIAUDIOTOTEXT__MODELID }}
|
||||
AzureOpenAITextToAudio__ApiKey: ${{ secrets.AZUREOPENAITEXTTOAUDIO__APIKEY }}
|
||||
AzureOpenAITextToAudio__Endpoint: ${{ secrets.AZUREOPENAITEXTTOAUDIO__ENDPOINT }}
|
||||
AzureOpenAITextToAudio__DeploymentName: ${{ vars.AZUREOPENAITEXTTOAUDIO__DEPLOYMENTNAME }}
|
||||
AzureOpenAIAudioToText__ApiKey: ${{ secrets.AZUREOPENAIAUDIOTOTEXT__APIKEY }}
|
||||
AzureOpenAIAudioToText__Endpoint: ${{ secrets.AZUREOPENAIAUDIOTOTEXT__ENDPOINT }}
|
||||
AzureOpenAIAudioToText__DeploymentName: ${{ vars.AZUREOPENAIAUDIOTOTEXT__DEPLOYMENTNAME }}
|
||||
Bing__ApiKey: ${{ secrets.BING__APIKEY }}
|
||||
OpenAI__ApiKey: ${{ secrets.OPENAI__APIKEY }}
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
# These are optional elements. Feel free to remove any of them.
|
||||
status: proposed
|
||||
contact: crickman, mabolan, semenshi
|
||||
date: 2024-01-16
|
||||
---
|
||||
|
||||
# File Services
|
||||
|
||||
## Context and Problem Statement
|
||||
OpenAI provides a file service for uploading files to be used for *assistant retrieval* or *model fine-tuning*: `https://api.openai.com/v1/files`
|
||||
|
||||
Other providers may also offer some type of file-service, such as Gemini.
|
||||
|
||||
> Note: *Azure Open AI* does not currently support the OpenAI file service API.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. Add OpenAI file service support to `Microsoft.SemanticKernel.Experimental.Agents`
|
||||
2. Add a file service abstraction and implement support for OpenAI
|
||||
3. Add OpenAI file service support without abstraction
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
> Option 3. **Add OpenAI file service support without abstraction**
|
||||
> Mark code as experimental using label: `SKEXP0015`
|
||||
|
||||
Defining a generalized file service interface provides an extensibility point for other vendors, in addition to *OpenAI*.
|
||||
|
||||
## Pros and Cons of the Options
|
||||
|
||||
### Option 1. Add OpenAI file service support to `Microsoft.SemanticKernel.Experimental.Agents`
|
||||
**Pro:**
|
||||
1. No impact to existing AI connectors.
|
||||
|
||||
**Con:**
|
||||
1. No reuse via AI connectors.
|
||||
1. No common abstraction.
|
||||
1. Unnatural dependency binding for uses other than with OpenAI assistants.
|
||||
|
||||
### Option 2. Add a file service abstraction and implement support for OpenAI
|
||||
**Pro:**
|
||||
1. Defines a common interface for file service interactions.
|
||||
1. Allows for specialization for vendor specific services.
|
||||
|
||||
**Con:**
|
||||
1. Other systems may diverge from existing assumptions.
|
||||
|
||||
|
||||
### Option 3. Add OpenAI file service support without abstraction
|
||||
**Pro:**
|
||||
1. Provides support for OpenAI file-service.
|
||||
|
||||
**Con:**
|
||||
1. File service offerings from other vendors supported case-by-case without commonality.
|
||||
|
||||
|
||||
## More Information
|
||||
|
||||
### Signature of BinaryContent
|
||||
|
||||
> Note: `BinaryContent` object able to provide either `BinaryData` or `Stream` regardless of which constructor is invoked.
|
||||
|
||||
#### `Microsoft.SemanticKernel.Abstractions`
|
||||
|
||||
```csharp
|
||||
namespace Microsoft.SemanticKernel;
|
||||
|
||||
/// <summary>
|
||||
/// Represents binary content.
|
||||
/// </summary>
|
||||
public sealed class BinaryContent : KernelContent
|
||||
{
|
||||
public BinaryContent(
|
||||
BinaryData content,
|
||||
string? modelId = null,
|
||||
object? innerContent = null,
|
||||
IReadOnlyDictionary<string, object?>? metadata = null);
|
||||
|
||||
public BinaryContent(
|
||||
Func<Stream> streamProvider,
|
||||
string? modelId = null,
|
||||
object? innerContent = null,
|
||||
IReadOnlyDictionary<string, object?>? metadata = null);
|
||||
|
||||
public Task<BinaryData> GetContentAsync();
|
||||
|
||||
public Task<Stream> GetStreamAsync();
|
||||
}
|
||||
```
|
||||
### Signatures for Option 3:
|
||||
|
||||
#### `Microsoft.SemanticKernel.Connectors.OpenAI`
|
||||
```csharp
|
||||
namespace Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
public sealed class OpenAIFileService
|
||||
{
|
||||
public async Task<OpenAIFileReference> GetFileAsync(
|
||||
string id,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public async Task<IEnumerable<OpenAIFileReference>> GetFilesAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
public async Task<BinaryContent> GetFileContentAsync(
|
||||
string id,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public async Task DeleteFileAsync(
|
||||
string id,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public async Task<OpenAIFileReference> UploadContentAsync(
|
||||
BinaryContent content,
|
||||
OpenAIFileUploadExecutionSettings settings,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class OpenAIFileUploadExecutionSettings
|
||||
{
|
||||
public string FileName { get; }
|
||||
|
||||
public OpenAIFilePurpose Purpose { get; }
|
||||
}
|
||||
|
||||
public sealed class OpenAIFileReference
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public DateTime CreatedTimestamp { get; set; }
|
||||
|
||||
public string FileName { get; set; }
|
||||
|
||||
public OpenAIFilePurpose Purpose { get; set; }
|
||||
|
||||
public int SizeInBytes { get; set; }
|
||||
}
|
||||
|
||||
public enum OpenAIFilePurpose
|
||||
{
|
||||
Assistants,
|
||||
Finetuning,
|
||||
}
|
||||
```
|
||||
@@ -24,6 +24,7 @@
|
||||
<PackageVersion Include="Microsoft.Bcl.TimeProvider" Version="8.0.1" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Memory.Data" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Numerics.Tensors" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.1" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
|
||||
@@ -65,7 +66,8 @@
|
||||
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="[3.32.3, )" />
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="[2.28.0, )" />
|
||||
<PackageVersion Include="Microsoft.OpenApi" Version="1.6.12" />
|
||||
<PackageVersion Include="Microsoft.OpenApi.Readers" Version="1.6.11" />
|
||||
<PackageVersion Include="Microsoft.OpenApi.Readers" Version="1.6.12" />
|
||||
<PackageVersion Include="Microsoft.OpenApi.ApiManifest" Version="0.5.4-preview" />
|
||||
<PackageVersion Include="Google.Apis.CustomSearchAPI.v1" Version="[1.60.0.3001, )" />
|
||||
<PackageVersion Include="Grpc.Net.Client" Version="2.60.0" />
|
||||
<PackageVersion Include="protobuf-net" Version="3.2.30" />
|
||||
@@ -78,7 +80,7 @@
|
||||
<!-- Symbols -->
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||
<!-- Toolset -->
|
||||
<PackageVersion Include="Microsoft.Net.Compilers.Toolset" Version="4.8.0" />
|
||||
<PackageVersion Include="Microsoft.Net.Compilers.Toolset" Version="4.9.0-3.final" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -219,6 +219,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linq", "Linq", "{607DD6FA-F
|
||||
src\InternalUtilities\test\Linq\AsyncEnumerable.cs = src\InternalUtilities\test\Linq\AsyncEnumerable.cs
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.AzureAISearch.UnitTests", "src\Connectors\Connectors.AzureAISearch.UnitTests\Connectors.AzureAISearch.UnitTests.csproj", "{BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -500,6 +502,12 @@ Global
|
||||
{95CAA25F-A0DE-4A5B-92BA-7D56C0E822A8}.Publish|Any CPU.Build.0 = Publish|Any CPU
|
||||
{95CAA25F-A0DE-4A5B-92BA-7D56C0E822A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{95CAA25F-A0DE-4A5B-92BA-7D56C0E822A8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}.Publish|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BCDD5B96-CCC3-46B9-8217-89CD5885F6A2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -573,6 +581,7 @@ Global
|
||||
{95CAA25F-A0DE-4A5B-92BA-7D56C0E822A8} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}
|
||||
{EB2C141A-AE5F-4080-8790-13EB16323CEF} = {958AD708-F048-4FAF-94ED-D2F2B92748B9}
|
||||
{607DD6FA-FA0D-45E6-80BA-22A373609E89} = {5C246969-D794-4EC3-8E8F-F90D4D166420}
|
||||
{BCDD5B96-CCC3-46B9-8217-89CD5885F6A2} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
|
||||
|
||||
@@ -16,6 +16,7 @@ You can use the following diagnostic IDs to ignore warnings or errors for a part
|
||||
- SKEXP0002: Image services
|
||||
- SKEXP0003: Memory connectors
|
||||
- SKEXP0004: Kernel Filters
|
||||
- SKEXP0005: Audio services
|
||||
|
||||
## OpenAI and Azure OpenAI services
|
||||
|
||||
@@ -24,6 +25,7 @@ You can use the following diagnostic IDs to ignore warnings or errors for a part
|
||||
- SKEXP0012: OpenAI image service
|
||||
- SKEXP0013: OpenAI parameters
|
||||
- SKEXP0014: OpenAI chat history extension
|
||||
- SKEXP0015: OpenAI file service
|
||||
|
||||
## Memory connectors
|
||||
|
||||
@@ -46,6 +48,7 @@ You can use the following diagnostic IDs to ignore warnings or errors for a part
|
||||
- SKEXP0040: GRPC functions
|
||||
- SKEXP0041: Markdown functions
|
||||
- SKEXP0042: OpenAPI functions
|
||||
- SKEXP0043: OpenAPI function extensions
|
||||
|
||||
## Out-of-the-box plugins
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.3.1</VersionPrefix>
|
||||
<VersionPrefix>1.4.0</VersionPrefix>
|
||||
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix)</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)</PackageVersion>
|
||||
|
||||
@@ -72,7 +72,7 @@ her a beggar. My father came to her aid and two years later they married.
|
||||
WriteLine(excisionResult.GetValue<string>());
|
||||
}
|
||||
|
||||
[Fact(Skip = "Unreliable - Needs attention")]
|
||||
[Fact]
|
||||
public async Task PlanningWithGroundednessAsync()
|
||||
{
|
||||
var targetTopic = "people and places";
|
||||
@@ -81,10 +81,7 @@ her a beggar. My father came to her aid and two years later they married.
|
||||
related to {targetTopic} (such as {samples}) which are present in the summary.
|
||||
Take this list of entities, and from it make another list of those which are not
|
||||
grounded in the original input text. Finally, rewrite your summary to remove the entities
|
||||
which are not grounded in the original.
|
||||
|
||||
Text:\n{GroundingText};
|
||||
";
|
||||
which are not grounded in the original.";
|
||||
|
||||
WriteLine("\n======== Planning - Groundedness Checks ========");
|
||||
|
||||
@@ -112,12 +109,17 @@ Text:\n{GroundingText};
|
||||
TopP = 0.1,
|
||||
}
|
||||
});
|
||||
var plan = await planner.CreatePlanAsync(kernel, ask);
|
||||
|
||||
var initialArguments = new KernelArguments()
|
||||
{
|
||||
{ "groundingText", GroundingText}
|
||||
};
|
||||
var plan = await planner.CreatePlanAsync(kernel, ask, initialArguments);
|
||||
|
||||
WriteLine($"======== Goal: ========\n{ask}");
|
||||
WriteLine($"======== Plan ========\n{plan}");
|
||||
|
||||
var result = await plan.InvokeAsync(kernel);
|
||||
var result = await plan.InvokeAsync(kernel, initialArguments);
|
||||
|
||||
WriteLine("======== Result ========");
|
||||
WriteLine(result);
|
||||
@@ -190,36 +192,30 @@ his daughter. The daughter procured work to eek out a living, but after ten mont
|
||||
her a beggar. My father came to her aid and two years later they married.
|
||||
|
||||
======== Planning - Groundedness Checks ========
|
||||
Goal: Make a summary of input text. Then make a list of entities
|
||||
======== Goal: ========
|
||||
Make a summary of the following text. Then make a list of entities
|
||||
related to people and places (such as John, Jane, mother, brother, Paris, Rome) which are present in the summary.
|
||||
Take this list of entities, and from it make another list of those which are not
|
||||
grounded in the original input text. Finally, rewrite your summary to remove the entities
|
||||
which are not grounded in the original.
|
||||
======== Plan ========
|
||||
{{!-- Step 0: Extract key values --}}
|
||||
{{set "inputText" @root.groundingText}}
|
||||
|
||||
Text:
|
||||
{See GroundingText above}
|
||||
{{!-- Step 1: Summarize the input text --}}
|
||||
{{set "summary" (SummarizePlugin-Summarize input=inputText)}}
|
||||
|
||||
Plan:
|
||||
{{!-- Step 1: Set the input text --}}
|
||||
{{set "inputText" "I am by birth a Genevese, and my family is one of the most distinguished of that republic. My ancestors had been for many years counsellors and syndics, and my father had filled several public situations with honour and reputation.He was respected by all who knew him for his integrity and indefatigable attention to public business.He passed his younger days perpetually occupied by the affairs of his country; a variety of circumstances had prevented his marrying early, nor was it until the decline of life that he became a husband and the father of a family. As the circumstances of his marriage illustrate his character, I cannot refrain from relating them.One of his most intimate friends was a merchant who, from a flourishing state, fell, through numerous mischances, into poverty. This man, whose name was Beaufort, was of a proud and unbending disposition and could not bear to live in poverty and oblivion in the same country where he had formerly been distinguished for his rank and magnificence. Having paid his debts, therefore, in the most honourable manner, he retreated with his daughter to the town of Lucerne, where he lived unknown and in wretchedness.My father loved Beaufort with the truest friendship and was deeply grieved by his retreat in these unfortunate circumstances.He bitterly deplored the false pride which led his friend to a conduct so little worthy of the affection that united them.He lost no time in endeavouring to seek him out, with the hope of persuading him to begin the world again through his credit and assistance. Beaufort had taken effectual measures to conceal himself, and it was ten months before my father discovered his abode.Overjoyed at this discovery, he hastened to the house, which was situated in a mean street near the Reuss. But when he entered, misery and despair alone welcomed him. Beaufort had saved but a very small sum of money from the wreck of his fortunes, but it was sufficient to provide him with sustenance for some months, and in the meantime he hoped to procure some respectable employment in a merchant's house. The interval was, consequently, spent in inaction; his grief only became more deep and rankling when he had leisure for reflection, and at length it took so fast hold of his mind that at the end of three months he lay on a bed of sickness, incapable of any exertion. His daughter attended him with the greatest tenderness, but she saw with despair that their little fund was rapidly decreasing and that there was no other prospect of support.But Caroline Beaufort possessed a mind of an uncommon mould, and her courage rose to support her in her adversity. She procured plain work; she plaited straw and by various means contrived to earn a pittance scarcely sufficient to support life. Several months passed in this manner.Her father grew worse; her time was more entirely occupied in attending him; her means of subsistence decreased; and in the tenth month her father died in her arms, leaving her an orphan and a beggar.This last blow overcame her, and she knelt by Beaufort's coffin weeping bitterly, when my father entered the chamber. He came like a protecting spirit to the poor girl, who committed herself to his care; and after the interment of his friend he conducted her to Geneva and placed her under the protection of a relation.Two years after this event Caroline became his wife."}}
|
||||
{{!-- Step 2: Extract entities related to people and places from the summary --}}
|
||||
{{set "extractedEntities" (GroundingPlugin-ExtractEntities input=summary topic="people and places" example_entities="John, Jane, mother, brother, Paris, Rome")}}
|
||||
|
||||
{{!-- Step 2: Summarize the input text --}}
|
||||
{{set "summary" (SummarizePlugin-Summarize input=(get "inputText"))}}
|
||||
{{!-- Step 3: Check if extracted entities are grounded in the original input text --}}
|
||||
{{set "notGroundedEntities" (GroundingPlugin-ReferenceCheckEntities input=extractedEntities reference_context=inputText)}}
|
||||
|
||||
{{!-- Step 3: Extract entities related to people and places --}}
|
||||
{{set "exampleEntities" (array "John" "Jane" "mother" "brother" "Paris" "Rome")}}
|
||||
{{set "extractedEntities" (GroundingPlugin-ExtractEntities input=(get "summary") topic="people and places" example_entities=(get "exampleEntities"))}}
|
||||
{{!-- Step 4: Remove the not grounded entities from the summary --}}
|
||||
{{set "finalSummary" (GroundingPlugin-ExciseEntities input=summary ungrounded_entities=notGroundedEntities)}}
|
||||
|
||||
{{!-- Step 4: Check if the extracted entities are grounded in the original text --}}
|
||||
{{set "ungroundedEntities" (GroundingPlugin-ReferenceCheckEntities input=(get "extractedEntities") reference_context=(get "inputText"))}}
|
||||
|
||||
{{!-- Step 5: Remove ungrounded entities from the summary --}}
|
||||
{{set "finalSummary" (GroundingPlugin-ExciseEntities input=(get "summary") ungrounded_entities=(get "ungroundedEntities"))}}
|
||||
|
||||
{{!-- Step 6: Output the final summary --}}
|
||||
{{json (get "finalSummary")}}
|
||||
|
||||
A possible summary is:
|
||||
Born in Geneva to a distinguished family, the narrator's father married late in life. His close friend Beaufort, once wealthy, fell into poverty and moved to another city with his daughter, Caroline. The narrator's father searched for Beaufort, finding him after ten months, only to witness his decline and eventual death. Caroline, now an orphan, was taken in by the narrator's father, who married her two years later.
|
||||
== DONE ==
|
||||
{{!-- Step 5: Output the final summary --}}
|
||||
{{json finalSummary}}
|
||||
======== Result ========
|
||||
Born in Geneva to a distinguished family, the narrator's father held various honorable public positions. He married late in life after helping his impoverished friend Beaufort and his daughter Caroline. Beaufort, once wealthy, fell into poverty and moved to another location, where the narrator's father found him after ten months. Beaufort eventually fell ill and died, leaving his daughter Caroline an orphan. The narrator's father took her in, and two years later, they married.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
@@ -26,7 +28,8 @@ public class Example65_HandlebarsPlanner : BaseTest
|
||||
{
|
||||
WriteLine($"======== [Handlebars Planner] Sample {s_sampleIndex++} - Create and Execute Plan with: {name} ========");
|
||||
}
|
||||
private async Task RunSampleAsync(string goal, bool shouldPrintPrompt = false, params string[] pluginDirectoryNames)
|
||||
|
||||
private async Task<Kernel?> SetupKernelAsync(params string[] pluginDirectoryNames)
|
||||
{
|
||||
string apiKey = TestConfiguration.AzureOpenAI.ApiKey;
|
||||
string chatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName;
|
||||
@@ -36,7 +39,7 @@ public class Example65_HandlebarsPlanner : BaseTest
|
||||
if (apiKey == null || chatDeploymentName == null || chatModelId == null || endpoint == null)
|
||||
{
|
||||
WriteLine("Azure endpoint, apiKey, deploymentName, or modelId not found. Skipping example.");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
var kernel = Kernel.CreateBuilder()
|
||||
@@ -76,9 +79,34 @@ public class Example65_HandlebarsPlanner : BaseTest
|
||||
}
|
||||
}
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
private void PrintPlannerDetails(string goal, HandlebarsPlan plan, string result, bool shouldPrintPrompt = false)
|
||||
{
|
||||
WriteLine($"Goal: {goal}");
|
||||
WriteLine($"\nOriginal plan:\n{plan}");
|
||||
WriteLine($"\nResult:\n{result}\n");
|
||||
|
||||
// Print the prompt template
|
||||
if (shouldPrintPrompt && plan.Prompt is not null)
|
||||
{
|
||||
WriteLine("\n======== Prompt Template ========");
|
||||
WriteLine(plan.Prompt);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunSampleAsync(string goal, KernelArguments? initialContext = null, bool shouldPrintPrompt = false, params string[] pluginDirectoryNames)
|
||||
{
|
||||
var kernel = await SetupKernelAsync(pluginDirectoryNames);
|
||||
if (kernel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use gpt-4 or newer models if you want to test with loops.
|
||||
// Older models like gpt-35-turbo are less recommended. They do handle loops but are more prone to syntax errors.
|
||||
var allowLoopsInPlan = chatDeploymentName.Contains("gpt-4", StringComparison.OrdinalIgnoreCase);
|
||||
var allowLoopsInPlan = TestConfiguration.AzureOpenAI.ChatDeploymentName.Contains("gpt-4", StringComparison.OrdinalIgnoreCase);
|
||||
var planner = new HandlebarsPlanner(
|
||||
new HandlebarsPlannerOptions()
|
||||
{
|
||||
@@ -93,22 +121,13 @@ public class Example65_HandlebarsPlanner : BaseTest
|
||||
AllowLoops = allowLoopsInPlan
|
||||
});
|
||||
|
||||
WriteLine($"Goal: {goal}");
|
||||
|
||||
// Create the plan
|
||||
var plan = await planner.CreatePlanAsync(kernel, goal);
|
||||
|
||||
// Print the prompt template
|
||||
if (shouldPrintPrompt && plan.Prompt is not null)
|
||||
{
|
||||
WriteLine($"\nPrompt template:\n{plan.Prompt}");
|
||||
}
|
||||
|
||||
WriteLine($"\nOriginal plan:\n{plan}");
|
||||
var plan = await planner.CreatePlanAsync(kernel, goal, initialContext);
|
||||
|
||||
// Execute the plan
|
||||
var result = await plan.InvokeAsync(kernel);
|
||||
WriteLine($"\nResult:\n{result}\n");
|
||||
var result = await plan.InvokeAsync(kernel, initialContext);
|
||||
|
||||
PrintPlannerDetails(goal, plan, result, shouldPrintPrompt);
|
||||
}
|
||||
|
||||
[RetryTheory(typeof(HttpOperationException))]
|
||||
@@ -120,7 +139,7 @@ public class Example65_HandlebarsPlanner : BaseTest
|
||||
try
|
||||
{
|
||||
// Load additional plugins to enable planner but not enough for the given goal.
|
||||
await RunSampleAsync("Send Mary an email with the list of meetings I have scheduled today.", shouldPrintPrompt, "SummarizePlugin");
|
||||
await RunSampleAsync("Send Mary an email with the list of meetings I have scheduled today.", null, shouldPrintPrompt, "SummarizePlugin");
|
||||
}
|
||||
catch (KernelException ex) when (
|
||||
ex.Message.Contains(nameof(HandlebarsPlannerErrorCodes.InsufficientFunctionsForGoal), StringComparison.CurrentCultureIgnoreCase)
|
||||
@@ -128,13 +147,12 @@ public class Example65_HandlebarsPlanner : BaseTest
|
||||
|| ex.Message.Contains(nameof(HandlebarsPlannerErrorCodes.InvalidTemplate), StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
/*
|
||||
Unable to create plan for goal with available functions.
|
||||
Goal: Email me a list of meetings I have scheduled today.
|
||||
Available Functions: SummarizePlugin-Notegen, SummarizePlugin-Summarize, SummarizePlugin-MakeAbstractReadable, SummarizePlugin-Topics
|
||||
[InsufficientFunctionsForGoal] Unable to create plan for goal with available functions.
|
||||
Goal: Send Mary an email with the list of meetings I have scheduled today.
|
||||
Available Functions: SummarizePlugin-MakeAbstractReadable, SummarizePlugin-Notegen, SummarizePlugin-Summarize, SummarizePlugin-Topics
|
||||
Planner output:
|
||||
I'm sorry, but it seems that the provided helpers do not include any helper to fetch or filter meetings scheduled for today.
|
||||
Therefore, I cannot create a Handlebars template to achieve the specified goal with the available helpers.
|
||||
Additional helpers may be required.
|
||||
As the available helpers do not contain any functionality to send an email or interact with meeting scheduling data, I cannot create a template to achieve the stated goal.
|
||||
Additional helpers or information may be required.
|
||||
*/
|
||||
WriteLine($"\n{ex.Message}\n");
|
||||
}
|
||||
@@ -145,16 +163,40 @@ public class Example65_HandlebarsPlanner : BaseTest
|
||||
|
||||
public Task RunCourseraSampleAsync(bool shouldPrintPrompt = false)
|
||||
{
|
||||
this.WriteSampleHeading("Coursera OpenAPI Plugin");
|
||||
return RunSampleAsync("Show me courses about Artificial Intelligence.", shouldPrintPrompt, CourseraPluginName);
|
||||
WriteSampleHeading("Coursera OpenAPI Plugin");
|
||||
return RunSampleAsync("Show me courses about Artificial Intelligence.", null, shouldPrintPrompt, CourseraPluginName);
|
||||
/*
|
||||
Original plan:
|
||||
{{!-- Step 0: Extract key values --}}
|
||||
{{set "query" "Artificial Intelligence"}}
|
||||
|
||||
{{!-- Step 1: Call CourseraPlugin-search with the query --}}
|
||||
{{set "searchResults" (CourseraPlugin-search query=query)}}
|
||||
|
||||
{{!-- Step 2: Loop through the search results and display course information --}}
|
||||
{{#each searchResults.hits}}
|
||||
{{json (concat "Course Name: " this.name ", URL: " this.objectUrl)}}
|
||||
{{/each}}
|
||||
|
||||
Result:
|
||||
Course Name: Introduction to Artificial Intelligence (AI), URL: https://www.coursera.org/learn/introduction-to-ai?utm_source=rest_api
|
||||
Course Name: IBM Applied AI, URL: https://www.coursera.org/professional-certificates/applied-artifical-intelligence-ibm-watson-ai?utm_source=rest_api
|
||||
Course Name: AI For Everyone, URL: https://www.coursera.org/learn/ai-for-everyone?utm_source=rest_api
|
||||
Course Name: Python for Data Science, AI & Development, URL: https://www.coursera.org/learn/python-for-applied-data-science-ai?utm_source=rest_api
|
||||
Course Name: Introduction to Generative AI, URL: https://www.coursera.org/learn/introduction-to-generative-ai?utm_source=rest_api
|
||||
Course Name: Deep Learning, URL: https://www.coursera.org/specializations/deep-learning?utm_source=rest_api
|
||||
Course Name: Machine Learning, URL: https://www.coursera.org/specializations/machine-learning-introduction?utm_source=rest_api
|
||||
Course Name: IBM AI Engineering, URL: https://www.coursera.org/professional-certificates/ai-engineer?utm_source=rest_api
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
[RetryTheory(typeof(HttpOperationException))]
|
||||
[InlineData(false)]
|
||||
public Task RunDictionaryWithBasicTypesSampleAsync(bool shouldPrintPrompt = false)
|
||||
{
|
||||
this.WriteSampleHeading("Basic Types using Local Dictionary Plugin");
|
||||
return RunSampleAsync("Get a random word and its definition.", shouldPrintPrompt, StringParamsDictionaryPlugin.PluginName);
|
||||
WriteSampleHeading("Basic Types using Local Dictionary Plugin");
|
||||
return RunSampleAsync("Get a random word and its definition.", null, shouldPrintPrompt, StringParamsDictionaryPlugin.PluginName);
|
||||
/*
|
||||
Original plan:
|
||||
{{!-- Step 1: Get a random word --}}
|
||||
@@ -175,8 +217,8 @@ public class Example65_HandlebarsPlanner : BaseTest
|
||||
[InlineData(true)]
|
||||
public Task RunLocalDictionaryWithComplexTypesSampleAsync(bool shouldPrintPrompt = false)
|
||||
{
|
||||
this.WriteSampleHeading("Complex Types using Local Dictionary Plugin");
|
||||
return RunSampleAsync("Teach me two random words and their definition.", shouldPrintPrompt, ComplexParamsDictionaryPlugin.PluginName);
|
||||
WriteSampleHeading("Complex Types using Local Dictionary Plugin");
|
||||
return RunSampleAsync("Teach me two random words and their definition.", null, shouldPrintPrompt, ComplexParamsDictionaryPlugin.PluginName);
|
||||
/*
|
||||
Original Plan:
|
||||
{{!-- Step 1: Get two random dictionary entries --}}
|
||||
@@ -211,8 +253,8 @@ public class Example65_HandlebarsPlanner : BaseTest
|
||||
[InlineData(false)]
|
||||
public Task RunPoetrySampleAsync(bool shouldPrintPrompt = false)
|
||||
{
|
||||
this.WriteSampleHeading("Multiple Plugins");
|
||||
return RunSampleAsync("Write a poem about John Doe, then translate it into Italian.", shouldPrintPrompt, "SummarizePlugin", "WriterPlugin");
|
||||
WriteSampleHeading("Multiple Plugins");
|
||||
return RunSampleAsync("Write a poem about John Doe, then translate it into Italian.", null, shouldPrintPrompt, "SummarizePlugin", "WriterPlugin");
|
||||
/*
|
||||
Original plan:
|
||||
{{!-- Step 1: Initialize the scenario for the poem --}}
|
||||
@@ -240,8 +282,8 @@ public class Example65_HandlebarsPlanner : BaseTest
|
||||
[InlineData(false)]
|
||||
public Task RunBookSampleAsync(bool shouldPrintPrompt = false)
|
||||
{
|
||||
this.WriteSampleHeading("Loops and Conditionals");
|
||||
return RunSampleAsync("Create a book with 3 chapters about a group of kids in a club called 'The Thinking Caps.'", shouldPrintPrompt, "WriterPlugin", "MiscPlugin");
|
||||
WriteSampleHeading("Loops and Conditionals");
|
||||
return RunSampleAsync("Create a book with 3 chapters about a group of kids in a club called 'The Thinking Caps.'", null, shouldPrintPrompt, "WriterPlugin", "MiscPlugin");
|
||||
/*
|
||||
Original plan:
|
||||
{{!-- Step 1: Initialize the book title and chapter count --}}
|
||||
@@ -266,6 +308,142 @@ public class Example65_HandlebarsPlanner : BaseTest
|
||||
*/
|
||||
}
|
||||
|
||||
[RetryTheory(typeof(HttpOperationException))]
|
||||
[InlineData(true)]
|
||||
public Task RunPredefinedVariablesSample(bool shouldPrintPrompt = false)
|
||||
{
|
||||
WriteSampleHeading("CreatePlan Prompt With Predefined Variables");
|
||||
|
||||
// When using predefined variables, you must pass these arguments to both the CreatePlanAsync and InvokeAsync methods.
|
||||
var initialArguments = new KernelArguments()
|
||||
{
|
||||
{ "greetings", new List<string>(){ "hey", "bye" } },
|
||||
{ "someNumber", 1 },
|
||||
{ "person", new Dictionary<string, string>()
|
||||
{
|
||||
{"name", "John Doe" },
|
||||
{ "language", "Italian" },
|
||||
} }
|
||||
};
|
||||
|
||||
return RunSampleAsync("Write a poem about the given person, then translate it into French.", initialArguments, shouldPrintPrompt, "WriterPlugin", "MiscPlugin");
|
||||
/*
|
||||
Original plan:
|
||||
{{!-- Step 0: Set the given person --}}
|
||||
{{set "person" "John Doe"}}
|
||||
|
||||
{{!-- Step 1: Generate a short poem about the person --}}
|
||||
{{set "poem" (WriterPlugin-ShortPoem input=person)}}
|
||||
|
||||
{{!-- Step 2: Translate the poem into French --}}
|
||||
{{set "translatedPoem" (WriterPlugin-Translate input=poem language="French")}}
|
||||
|
||||
{{!-- Step 3: Output the translated poem --}}
|
||||
{{json translatedPoem}}
|
||||
|
||||
Result:
|
||||
Il était une fois un gars nommé Doe,
|
||||
Dont la vie était un spectacle comique,
|
||||
Il trébuchait et tombait,
|
||||
Mais riait à travers tout cela,
|
||||
Alors qu'il dansait dans la vie, de-ci de-là.
|
||||
*/
|
||||
}
|
||||
|
||||
[RetryTheory(typeof(HttpOperationException))]
|
||||
[InlineData(true)]
|
||||
public async Task RunPromptWithAdditionalContextSampleAsync(bool shouldPrintPrompt = false)
|
||||
{
|
||||
WriteSampleHeading("Prompt With Additional Context");
|
||||
|
||||
// Pulling the raw content from SK's README file as domain context.
|
||||
static async Task<string> getDomainContext()
|
||||
{
|
||||
// For demonstration purposes only, beware of token count.
|
||||
var repositoryUrl = "https://github.com/microsoft/semantic-kernel";
|
||||
var readmeUrl = $"{repositoryUrl}/main/README.md".Replace("github.com", "raw.githubusercontent.com", StringComparison.CurrentCultureIgnoreCase);
|
||||
try
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
// Send a GET request to the specified URL
|
||||
var response = await httpClient.GetAsync(new Uri(readmeUrl));
|
||||
response.EnsureSuccessStatusCode(); // Throw an exception if not successful
|
||||
|
||||
// Read the response content as a string
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
httpClient.Dispose();
|
||||
return "Content imported from the README of https://github.com/microsoft/semantic-kernel:\n" + content;
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
Console.WriteLine("\nException Caught!");
|
||||
Console.WriteLine("Message :{0} ", e.Message);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
var goal = "Help me onboard to the Semantic Kernel SDK by creating a quick guide that includes a brief overview of the SDK for C# developers and detailed set-up steps. Include relevant links where possible. Then, draft an email with this guide, so I can share it with my team.";
|
||||
|
||||
var kernel = await SetupKernelAsync("WriterPlugin");
|
||||
if (kernel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use gpt-4 or newer models if you want to test with loops.
|
||||
// Older models like gpt-35-turbo are less recommended. They do handle loops but are more prone to syntax errors.
|
||||
var allowLoopsInPlan = TestConfiguration.AzureOpenAI.ChatDeploymentName.Contains("gpt-4", StringComparison.OrdinalIgnoreCase);
|
||||
var planner = new HandlebarsPlanner(
|
||||
new HandlebarsPlannerOptions()
|
||||
{
|
||||
// Context to be used in the prompt template.
|
||||
GetAdditionalPromptContext = getDomainContext,
|
||||
AllowLoops = false
|
||||
});
|
||||
|
||||
// Create the plan
|
||||
var plan = await planner.CreatePlanAsync(kernel, goal);
|
||||
|
||||
// Execute the plan
|
||||
var result = await plan.InvokeAsync(kernel);
|
||||
|
||||
PrintPlannerDetails(goal, plan, result, shouldPrintPrompt);
|
||||
/*
|
||||
{{!-- Step 0: Extract Key Values --}}
|
||||
{{set "sdkLink" "https://learn.microsoft.com/en-us/semantic-kernel/overview/"}}
|
||||
{{set "nugetPackageLink" "https://www.nuget.org/packages/Microsoft.SemanticKernel/"}}
|
||||
{{set "csharpGetStartedLink" "dotnet/README.md"}}
|
||||
{{set "emailSubject" "Semantic Kernel SDK: Quick Guide for C# Developers"}}
|
||||
|
||||
{{!-- Step 1: Create a concise guide and store it in a variable --}}
|
||||
{{set "guide" (concat "The Semantic Kernel SDK provides seamless integration between large language models (LLMs) and programming languages such as C#. " "To get started with the C# SDK, please follow these steps:\n\n" "1. Read the SDK Overview for a brief introduction here: " sdkLink "\n" "2. Install the Nuget package in your project: " nugetPackageLink "\n" "3. Follow the detailed set-up steps in the C# 'Getting Started' guide: " csharpGetStartedLink "\n\n" "Feel free to share this quick guide with your team members to help them onboard quickly with the Semantic Kernel SDK. ")}}
|
||||
|
||||
{{!-- Step 2: Generate a draft email with the guide --}}
|
||||
{{set "emailBody" (concat "Hi Team,\n\n" "I have put together a quick guide to help you onboard to the Semantic Kernel SDK for C# developers. " "This guide includes a brief overview and detailed set-up steps:\n\n" guide "\n\n" "I have attached a more comprehensive guide as a document. Please review it and let me know if you have any questions. " "Let's start integrating the Semantic Kernel SDK into our projects!\n\n" "Best Regards,\n" "Your Name ")}}
|
||||
|
||||
{{json (concat "Subject: " emailSubject "\n\nBody:\n" emailBody)}}
|
||||
|
||||
Result:
|
||||
Subject: Semantic Kernel SDK: Quick Guide for C# Developers
|
||||
|
||||
Body:
|
||||
Hi Team,
|
||||
I have put together a quick guide to help you onboard to the Semantic Kernel SDK for C# developers. This guide includes a brief overview and detailed set-up steps:
|
||||
|
||||
The Semantic Kernel SDK provides seamless integration between large language models (LLMs) and programming languages such as C#. To get started with the C# SDK, please follow these steps:
|
||||
1. Read the SDK Overview for a brief introduction here: https://learn.microsoft.com/en-us/semantic-kernel/overview/
|
||||
2. Install the Nuget package in your project: https://www.nuget.org/packages/Microsoft.SemanticKernel/
|
||||
3. Follow the detailed set-up steps in the C# 'Getting Started' guide: dotnet/README.md
|
||||
|
||||
Feel free to share this quick guide with your team members to help them onboard quickly with the Semantic Kernel SDK.
|
||||
|
||||
I have attached a more comprehensive guide as a document. Please review it and let me know if you have any questions. Let's start integrating the Semantic Kernel SDK into our projects!
|
||||
|
||||
Best Regards,
|
||||
Your Name
|
||||
*/
|
||||
}
|
||||
|
||||
public Example65_HandlebarsPlanner(ITestOutputHelper output) : base(output)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -58,12 +58,7 @@ provides:
|
||||
");
|
||||
|
||||
[Fact(Skip = "Can take more than 1 minute")]
|
||||
public Task RunAsync()
|
||||
{
|
||||
return RunExampleAsync();
|
||||
}
|
||||
|
||||
private async Task RunExampleAsync()
|
||||
public async Task RunAsync()
|
||||
{
|
||||
var bingConnector = new BingConnector(TestConfiguration.Bing.ApiKey);
|
||||
var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector);
|
||||
@@ -76,18 +71,18 @@ provides:
|
||||
|
||||
FlowOrchestrator orchestrator = new(
|
||||
GetKernelBuilder(LoggerFactory),
|
||||
await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(false),
|
||||
await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()),
|
||||
plugins,
|
||||
config: GetOrchestratorConfig());
|
||||
var sessionId = Guid.NewGuid().ToString();
|
||||
|
||||
WriteLine("*****************************************************");
|
||||
WriteLine("Executing " + nameof(RunExampleAsync));
|
||||
WriteLine("Executing " + nameof(RunAsync));
|
||||
Stopwatch sw = new();
|
||||
sw.Start();
|
||||
WriteLine("Flow: " + s_flow.Name);
|
||||
var question = s_flow.Steps.First().Goal;
|
||||
var result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, question).ConfigureAwait(false);
|
||||
var result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, question);
|
||||
|
||||
WriteLine("Question: " + question);
|
||||
WriteLine("Answer: " + result.Metadata!["answer"]);
|
||||
@@ -105,7 +100,7 @@ provides:
|
||||
foreach (var t in userInputs)
|
||||
{
|
||||
WriteLine($"User: {t}");
|
||||
result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, t).ConfigureAwait(false);
|
||||
result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, t);
|
||||
var responses = result.GetValue<List<string>>()!;
|
||||
foreach (var response in responses)
|
||||
{
|
||||
|
||||
@@ -4,7 +4,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
using Microsoft.SemanticKernel.Experimental.Agents;
|
||||
using Resources;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
@@ -80,29 +83,27 @@ public sealed class Example75_AgentTools : BaseTest
|
||||
return;
|
||||
}
|
||||
|
||||
// REQUIRED:
|
||||
//
|
||||
// Use `curl` to upload document prior to running example and assign the
|
||||
// identifier to `fileId`.
|
||||
//
|
||||
// Powershell:
|
||||
// curl https://api.openai.com/v1/files `
|
||||
// -H "Authorization: Bearer $Env:OPENAI_APIKEY" `
|
||||
// -F purpose="assistants" `
|
||||
// -F file="@Resources/travelinfo.txt"
|
||||
var kernel = Kernel.CreateBuilder().AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey).Build();
|
||||
var fileService = kernel.GetRequiredService<OpenAIFileService>();
|
||||
var result =
|
||||
await fileService.UploadContentAsync(
|
||||
new BinaryContent(() => Task.FromResult(EmbeddedResource.ReadStream("travelinfo.txt")!)),
|
||||
new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants));
|
||||
|
||||
var fileId = "<see comment>";
|
||||
var fileId = result.Id;
|
||||
|
||||
var defaultAgent =
|
||||
await new AgentBuilder()
|
||||
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
|
||||
.BuildAsync();
|
||||
Track(
|
||||
await new AgentBuilder()
|
||||
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
|
||||
.BuildAsync());
|
||||
|
||||
var retrievalAgent =
|
||||
await new AgentBuilder()
|
||||
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
|
||||
.WithRetrieval(fileId)
|
||||
.BuildAsync();
|
||||
Track(
|
||||
await new AgentBuilder()
|
||||
.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
|
||||
.WithRetrieval(fileId)
|
||||
.BuildAsync());
|
||||
|
||||
try
|
||||
{
|
||||
@@ -115,7 +116,7 @@ public sealed class Example75_AgentTools : BaseTest
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()));
|
||||
await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()).Append(fileService.DeleteFileAsync(fileId)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
using Resources;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Examples;
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
/// <summary>
|
||||
/// Showcase usage of Open AI file-service.
|
||||
/// </summary>
|
||||
public sealed class Example79_OpenAIFiles : BaseTest
|
||||
{
|
||||
private const string ResourceFileName = "30-user-context.txt";
|
||||
|
||||
/// <summary>
|
||||
/// Show how to utilize OpenAI file-service.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RunFileLifecycleAsync()
|
||||
{
|
||||
this.WriteLine("======== OpenAI File-Service ========");
|
||||
|
||||
if (TestConfiguration.OpenAI.ApiKey == null)
|
||||
{
|
||||
this.WriteLine("OpenAI apiKey not found. Skipping example.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize file-service
|
||||
var kernel =
|
||||
Kernel.CreateBuilder()
|
||||
.AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey)
|
||||
.Build();
|
||||
|
||||
var fileService = kernel.GetRequiredService<OpenAIFileService>();
|
||||
|
||||
// Upload file
|
||||
var fileContent = new BinaryContent(() => Task.FromResult(EmbeddedResource.ReadStream(ResourceFileName)!));
|
||||
var fileReference =
|
||||
await fileService.UploadContentAsync(
|
||||
fileContent,
|
||||
new OpenAIFileUploadExecutionSettings(ResourceFileName, OpenAIFilePurpose.Assistants));
|
||||
|
||||
WriteLine("SOURCE:");
|
||||
WriteLine($"# Name: {fileReference.FileName}");
|
||||
WriteLine("# Content:");
|
||||
WriteLine(await fileContent.GetContentAsync());
|
||||
|
||||
try
|
||||
{
|
||||
// Retrieve file metadata for validation.
|
||||
var copyReference = await fileService.GetFileAsync(fileReference.Id);
|
||||
Assert.Equal(fileReference.Id, copyReference.Id);
|
||||
WriteLine("REFERENCE:");
|
||||
WriteLine($"# ID: {fileReference.Id}");
|
||||
WriteLine($"# Name: {fileReference.FileName}");
|
||||
WriteLine($"# Purpose: {fileReference.Purpose}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Remove file
|
||||
await fileService.DeleteFileAsync(fileReference.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public Example79_OpenAIFiles(ITestOutputHelper output) : base(output) { }
|
||||
}
|
||||
+3
-3
@@ -11,12 +11,12 @@ using Xunit.Abstractions;
|
||||
|
||||
namespace Examples;
|
||||
|
||||
public class Example78_TextEmbedding : BaseTest
|
||||
public class Example81_TextEmbedding : BaseTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunAsync()
|
||||
{
|
||||
this.WriteLine("======== Example76_TextEmbedding ========");
|
||||
this.WriteLine("======== Text Embedding ========");
|
||||
await RunExampleAsync();
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ public class Example78_TextEmbedding : BaseTest
|
||||
return tokens.Count;
|
||||
}
|
||||
|
||||
public Example78_TextEmbedding(ITestOutputHelper output) : base(output)
|
||||
public Example81_TextEmbedding(ITestOutputHelper output) : base(output)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.AudioToText;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
using Microsoft.SemanticKernel.Contents;
|
||||
using Microsoft.SemanticKernel.TextToAudio;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Examples;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a class that demonstrates audio processing functionality.
|
||||
/// </summary>
|
||||
public sealed class Example82_Audio : BaseTest
|
||||
{
|
||||
private const string TextToAudioModel = "tts-1";
|
||||
private const string AudioToTextModel = "whisper-1";
|
||||
private const string AudioFilePath = "audio.wav";
|
||||
|
||||
[Fact(Skip = "Uncomment the line to write the audio file output before running this test.")]
|
||||
public async Task TextToAudioAsync()
|
||||
{
|
||||
// Create a kernel with OpenAI text to audio service
|
||||
var kernel = Kernel.CreateBuilder()
|
||||
.AddOpenAITextToAudio(
|
||||
modelId: TextToAudioModel,
|
||||
apiKey: TestConfiguration.OpenAI.ApiKey)
|
||||
.Build();
|
||||
|
||||
var textToAudioService = kernel.GetRequiredService<ITextToAudioService>();
|
||||
|
||||
string sampleText = "Hello, my name is John. I am a software engineer. I am working on a project to convert text to audio.";
|
||||
|
||||
// Set execution settings (optional)
|
||||
OpenAITextToAudioExecutionSettings executionSettings = new()
|
||||
{
|
||||
Voice = "alloy", // The voice to use when generating the audio.
|
||||
// Supported voices are alloy, echo, fable, onyx, nova, and shimmer.
|
||||
ResponseFormat = "mp3", // The format to audio in.
|
||||
// Supported formats are mp3, opus, aac, and flac.
|
||||
Speed = 1.0f // The speed of the generated audio.
|
||||
// Select a value from 0.25 to 4.0. 1.0 is the default.
|
||||
};
|
||||
|
||||
// Convert text to audio
|
||||
AudioContent audioContent = await textToAudioService.GetAudioContentAsync(sampleText, executionSettings);
|
||||
|
||||
// Save audio content to a file
|
||||
// await File.WriteAllBytesAsync(AudioFilePath, audioContent.Data!.ToArray());
|
||||
}
|
||||
|
||||
[Fact(Skip = "Setup and run TextToAudioAsync before running this test.")]
|
||||
public async Task AudioToTextAsync()
|
||||
{
|
||||
// Create a kernel with OpenAI audio to text service
|
||||
var kernel = Kernel.CreateBuilder()
|
||||
.AddOpenAIAudioToText(
|
||||
modelId: AudioToTextModel,
|
||||
apiKey: TestConfiguration.OpenAI.ApiKey)
|
||||
.Build();
|
||||
|
||||
var audioToTextService = kernel.GetRequiredService<IAudioToTextService>();
|
||||
|
||||
// Set execution settings (optional)
|
||||
OpenAIAudioToTextExecutionSettings executionSettings = new(AudioFilePath)
|
||||
{
|
||||
Language = "en", // The language of the audio data as two-letter ISO-639-1 language code (e.g. 'en' or 'es').
|
||||
Prompt = "sample prompt", // An optional text to guide the model's style or continue a previous audio segment.
|
||||
// The prompt should match the audio language.
|
||||
ResponseFormat = "json", // The format to return the transcribed text in.
|
||||
// Supported formats are json, text, srt, verbose_json, or vtt. Default is 'json'.
|
||||
Temperature = 0.3f, // The randomness of the generated text.
|
||||
// Select a value from 0.0 to 1.0. 0 is the default.
|
||||
};
|
||||
|
||||
// Read audio content from a file
|
||||
ReadOnlyMemory<byte> audioData = await File.ReadAllBytesAsync(AudioFilePath);
|
||||
AudioContent audioContent = new(new BinaryData(audioData));
|
||||
|
||||
// Convert audio to text
|
||||
var textContent = await audioToTextService.GetTextContentAsync(audioContent, executionSettings);
|
||||
|
||||
// Output the transcribed text
|
||||
this.WriteLine(textContent.Text);
|
||||
}
|
||||
|
||||
public Example82_Audio(ITestOutputHelper output) : base(output) { }
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors.CredentialManagers;
|
||||
using Microsoft.SemanticKernel.Plugins.OpenApi;
|
||||
using Microsoft.SemanticKernel.Plugins.OpenApi.Extensions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
namespace Examples;
|
||||
|
||||
// This example shows how to use the ApiManifest based plugins
|
||||
public class Example83_ApiManifest : BaseTest
|
||||
{
|
||||
public Example83_ApiManifest(ITestOutputHelper output) : base(output)
|
||||
{
|
||||
}
|
||||
|
||||
public static readonly IEnumerable<object[]> s_parameters = new List<object[]>
|
||||
{
|
||||
// function names are sanitized operationIds from the OpenAPI document
|
||||
new object[] { "MessagesPlugin", "meListMessages", new KernelArguments { { "_top", "1" } }, "MessagesPlugin" },
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(s_parameters))]
|
||||
public async Task RunSampleWithPlannerAsync(string pluginToTest, string functionToTest, KernelArguments? arguments, params string[] pluginsToLoad)
|
||||
{
|
||||
WriteSampleHeadingToConsole(pluginToTest, functionToTest, arguments, pluginsToLoad);
|
||||
var kernel = Kernel.CreateBuilder().Build();
|
||||
await AddApiManifestPluginsAsync(kernel, pluginsToLoad);
|
||||
|
||||
var result = await kernel.InvokeAsync(pluginToTest, functionToTest, arguments);
|
||||
this.WriteLine("--------------------");
|
||||
this.WriteLine($"\nResult:\n{result}\n");
|
||||
this.WriteLine("--------------------");
|
||||
}
|
||||
|
||||
private void WriteSampleHeadingToConsole(string pluginToTest, string functionToTest, KernelArguments? arguments, params string[] pluginsToLoad)
|
||||
{
|
||||
this.WriteLine();
|
||||
this.WriteLine("======== [ApiManifest Plugins Sample] ========");
|
||||
this.WriteLine($"======== Loading Plugins: {string.Join(" ", pluginsToLoad)} ========");
|
||||
this.WriteLine($"======== Calling Plugin Function: {pluginToTest}.{functionToTest} with parameters {arguments?.Select(x => x.Key + " = " + x.Value).Aggregate((x, y) => x + ", " + y)} ========");
|
||||
this.WriteLine();
|
||||
}
|
||||
|
||||
private async Task AddApiManifestPluginsAsync(Kernel kernel, params string[] pluginNames)
|
||||
{
|
||||
#pragma warning disable SKEXP0053
|
||||
if (TestConfiguration.MSGraph.Scopes == null)
|
||||
{
|
||||
throw new InvalidOperationException("Missing Scopes configuration for Microsoft Graph API.");
|
||||
}
|
||||
|
||||
LocalUserMSALCredentialManager credentialManager = await LocalUserMSALCredentialManager.CreateAsync().ConfigureAwait(false);
|
||||
|
||||
var token = await credentialManager.GetTokenAsync(
|
||||
TestConfiguration.MSGraph.ClientId,
|
||||
TestConfiguration.MSGraph.TenantId,
|
||||
TestConfiguration.MSGraph.Scopes.ToArray(),
|
||||
TestConfiguration.MSGraph.RedirectUri).ConfigureAwait(false);
|
||||
#pragma warning restore SKEXP0053
|
||||
|
||||
BearerAuthenticationProviderWithCancellationToken authenticationProvider = new(() => Task.FromResult(token));
|
||||
|
||||
foreach (var pluginName in pluginNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
#pragma warning disable SKEXP0042
|
||||
#pragma warning disable SKEXP0043
|
||||
KernelPlugin plugin =
|
||||
await kernel.ImportPluginFromApiManifestAsync(
|
||||
pluginName,
|
||||
$"Plugins/ApiManifestPlugins/{pluginName}/apimanifest.json",
|
||||
new OpenApiFunctionExecutionParameters(authCallback: authenticationProvider.AuthenticateRequestAsync
|
||||
, serverUrlOverride: new Uri("https://graph.microsoft.com/v1.0")))
|
||||
.ConfigureAwait(false);
|
||||
this.WriteLine($">> {pluginName} is created.");
|
||||
#pragma warning restore SKEXP0042
|
||||
#pragma warning restore SKEXP0043
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
kernel.LoggerFactory.CreateLogger("Plugin Creation").LogError(ex, "Plugin creation failed. Message: {0}", ex.Message);
|
||||
throw new AggregateException($"Plugin creation failed for {pluginName}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a token via the provided delegate and applies it to HTTP requests using the
|
||||
/// "bearer" authentication scheme.
|
||||
/// </summary>
|
||||
public class BearerAuthenticationProviderWithCancellationToken
|
||||
{
|
||||
private readonly Func<Task<string>> _bearerToken;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="BearerAuthenticationProviderWithCancellationToken"/> class.
|
||||
/// </summary>
|
||||
/// <param name="bearerToken">Delegate to retrieve the bearer token.</param>
|
||||
public BearerAuthenticationProviderWithCancellationToken(Func<Task<string>> bearerToken)
|
||||
{
|
||||
this._bearerToken = bearerToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the token to the provided HTTP request message.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request message.</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
public async Task AuthenticateRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var token = await this._bearerToken().ConfigureAwait(false);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<IsPackable>false</IsPackable>
|
||||
<!-- Suppress: "Declare types in namespaces", "Require ConfigureAwait", "Experimental" -->
|
||||
<NoWarn>CS8618,IDE0009,CA1051,CA1050,CA1707,CA2007,VSTHRD111,CS1591,RCS1110,CA5394,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,,SKEXP0012,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0040,SKEXP0041,SKEXP0042,SKEXP0050,SKEXP0051,SKEXP0052,SKEXP0053,SKEXP0054,SKEXP0055,SKEXP0060,SKEXP0061,SKEXP0101,SKEXP0102</NoWarn>
|
||||
<NoWarn>CS8618,IDE0009,CA1051,CA1050,CA1707,CA2007,VSTHRD111,CS1591,RCS1110,CA5394,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0005,SKEXP0010,SKEXP0011,,SKEXP0012,SKEXP0015,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0040,SKEXP0041,SKEXP0042,SKEXP0050,SKEXP0051,SKEXP0052,SKEXP0053,SKEXP0054,SKEXP0055,SKEXP0060,SKEXP0061,SKEXP0101,SKEXP0102</NoWarn>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
@@ -68,7 +68,9 @@
|
||||
<ProjectReference Include="..\..\src\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\src\Plugins\Plugins.Core\Plugins.Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\Plugins\Plugins.Memory\Plugins.Memory.csproj" />
|
||||
<ProjectReference Include="..\..\src\Plugins\Plugins.MsGraph\Plugins.MsGraph.csproj" />
|
||||
<ProjectReference Include="..\..\src\Functions\Functions.OpenApi\Functions.OpenApi.csproj" />
|
||||
<ProjectReference Include="..\..\src\Functions\Functions.OpenApi.Extensions\Functions.OpenApi.Extensions.csproj" />
|
||||
<ProjectReference Include="..\..\src\Functions\Functions.Grpc\Functions.Grpc.csproj" />
|
||||
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Qdrant\Connectors.Memory.Qdrant.csproj" />
|
||||
<ProjectReference Include="..\..\src\Plugins\Plugins.Web\Plugins.Web.csproj" />
|
||||
@@ -85,4 +87,9 @@
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Plugins\ApiManifestPlugins\**\apimanifest.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"applicationName": "SemanticKernel",
|
||||
"publisher": {
|
||||
"name": "publisher-name",
|
||||
"contactEmail": "[email protected]"
|
||||
},
|
||||
"apiDependencies": {
|
||||
"microsoft.graph": {
|
||||
"apiDescriptionUrl": "https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/openapi/v1.0/graphexplorer.yaml",
|
||||
"requests": [
|
||||
{
|
||||
"method": "Get",
|
||||
"uriTemplate": "/me/messages"
|
||||
},
|
||||
{
|
||||
"method": "Post",
|
||||
"uriTemplate": "/me/messages"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,13 +23,15 @@ internal static class EmbeddedResource
|
||||
internal static string Read(string fileName)
|
||||
{
|
||||
// Get the current assembly. Note: this class is in the same assembly where the embedded resources are stored.
|
||||
Assembly? assembly = typeof(EmbeddedResource).GetTypeInfo().Assembly;
|
||||
if (assembly == null) { throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found"); }
|
||||
Assembly assembly =
|
||||
typeof(EmbeddedResource).GetTypeInfo().Assembly ??
|
||||
throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found");
|
||||
|
||||
// Resources are mapped like types, using the namespace and appending "." (dot) and the file name
|
||||
var resourceName = $"{s_namespace}." + fileName;
|
||||
using Stream? resource = assembly.GetManifestResourceStream(resourceName);
|
||||
if (resource == null) { throw new ConfigurationException($"{resourceName} resource not found"); }
|
||||
using Stream resource =
|
||||
assembly.GetManifestResourceStream(resourceName) ??
|
||||
throw new ConfigurationException($"{resourceName} resource not found");
|
||||
|
||||
// Return the resource content, in text format.
|
||||
using var reader = new StreamReader(resource);
|
||||
@@ -39,8 +41,9 @@ internal static class EmbeddedResource
|
||||
internal static Stream? ReadStream(string fileName)
|
||||
{
|
||||
// Get the current assembly. Note: this class is in the same assembly where the embedded resources are stored.
|
||||
Assembly? assembly = typeof(EmbeddedResource).GetTypeInfo().Assembly;
|
||||
if (assembly == null) { throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found"); }
|
||||
Assembly assembly =
|
||||
typeof(EmbeddedResource).GetTypeInfo().Assembly ??
|
||||
throw new ConfigurationException($"[{s_namespace}] {fileName} assembly not found");
|
||||
|
||||
// Resources are mapped like types, using the namespace and appending "." (dot) and the file name
|
||||
var resourceName = $"{s_namespace}." + fileName;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors.Client;
|
||||
using Reliability;
|
||||
|
||||
public sealed class TestConfiguration
|
||||
@@ -40,6 +41,7 @@ public sealed class TestConfiguration
|
||||
public static KustoConfig Kusto => LoadSection<KustoConfig>();
|
||||
public static MongoDBConfig MongoDB => LoadSection<MongoDBConfig>();
|
||||
public static ChatGPTRetrievalPluginConfig ChatGPTRetrievalPlugin => LoadSection<ChatGPTRetrievalPluginConfig>();
|
||||
public static MsGraphConfiguration MSGraph => LoadSection<MsGraphConfiguration>();
|
||||
|
||||
private static T LoadSection<T>([CallerMemberName] string? caller = null)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Azure;
|
||||
using Azure.Search.Documents;
|
||||
using Azure.Search.Documents.Indexes;
|
||||
using Azure.Search.Documents.Indexes.Models;
|
||||
using Azure.Search.Documents.Models;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Connectors.AzureAISearch;
|
||||
using Microsoft.SemanticKernel.Memory;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.UnitTests.Memory.AzureAISearch;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="AzureAISearchMemoryStore"/> class.
|
||||
/// </summary>
|
||||
public sealed class AzureAISearchMemoryStoreTests
|
||||
{
|
||||
private readonly Mock<SearchIndexClient> _mockSearchIndexClient = new();
|
||||
private readonly Mock<SearchClient> _mockSearchClient = new();
|
||||
|
||||
private readonly AzureAISearchMemoryStore _service;
|
||||
|
||||
public AzureAISearchMemoryStoreTests()
|
||||
{
|
||||
this._mockSearchIndexClient
|
||||
.Setup(x => x.GetSearchClient(It.IsAny<string>()))
|
||||
.Returns(this._mockSearchClient.Object);
|
||||
|
||||
this._service = new AzureAISearchMemoryStore(this._mockSearchIndexClient.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCollectionsReturnsIndexNamesAsync()
|
||||
{
|
||||
// Arrange
|
||||
Page<SearchIndex> page = Page<SearchIndex>.FromValues(new[]
|
||||
{
|
||||
new SearchIndex("index-1"),
|
||||
new SearchIndex("index-2"),
|
||||
}, null, Mock.Of<Response>());
|
||||
|
||||
var pageable = AsyncPageable<SearchIndex>.FromPages(new[] { page });
|
||||
|
||||
this._mockSearchIndexClient
|
||||
.Setup(x => x.GetIndexesAsync(It.IsAny<CancellationToken>()))
|
||||
.Returns(pageable);
|
||||
|
||||
// Act
|
||||
var indexes = new List<string>();
|
||||
|
||||
await foreach (var index in this._service.GetCollectionsAsync())
|
||||
{
|
||||
indexes.Add(index);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal("index-1", indexes[0]);
|
||||
Assert.Equal("index-2", indexes[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCollectionsOnErrorThrowsHttpOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._mockSearchIndexClient
|
||||
.Setup(x => x.GetIndexesAsync(It.IsAny<CancellationToken>()))
|
||||
.Throws(new RequestFailedException((int)HttpStatusCode.InternalServerError, "test error response"));
|
||||
|
||||
// Act & Assert
|
||||
var indexes = new List<string>();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<HttpOperationException>(async () =>
|
||||
{
|
||||
await foreach (var index in this._service.GetCollectionsAsync())
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Equal("test error response", exception.Message);
|
||||
Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("index-1", true)]
|
||||
[InlineData("index-2", true)]
|
||||
[InlineData("index-3", false)]
|
||||
public async Task DoesCollectionExistReturnsValidResultAsync(string collectionName, bool expectedResult)
|
||||
{
|
||||
// Arrange
|
||||
Page<SearchIndex> page = Page<SearchIndex>.FromValues(new[]
|
||||
{
|
||||
new SearchIndex("index-1"),
|
||||
new SearchIndex("index-2"),
|
||||
}, null, Mock.Of<Response>());
|
||||
|
||||
var pageable = AsyncPageable<SearchIndex>.FromPages(new[] { page });
|
||||
|
||||
this._mockSearchIndexClient
|
||||
.Setup(x => x.GetIndexesAsync(It.IsAny<CancellationToken>()))
|
||||
.Returns(pageable);
|
||||
|
||||
// Act
|
||||
var result = await this._service.DoesCollectionExistAsync(collectionName);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DoesCollectionExistOnErrorThrowsHttpOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._mockSearchIndexClient
|
||||
.Setup(x => x.GetIndexesAsync(It.IsAny<CancellationToken>()))
|
||||
.Throws(new RequestFailedException((int)HttpStatusCode.InternalServerError, "test error response"));
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<HttpOperationException>(() => this._service.DoesCollectionExistAsync("test-index"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test error response", exception.Message);
|
||||
Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteCollectionCallsDeleteIndexMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._mockSearchIndexClient
|
||||
.Setup(x => x.DeleteIndexAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(It.IsAny<Response>());
|
||||
|
||||
// Act
|
||||
await this._service.DeleteCollectionAsync("test-index");
|
||||
|
||||
// Assert
|
||||
this._mockSearchIndexClient.Verify(x => x.DeleteIndexAsync("test-index", It.IsAny<CancellationToken>()), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteCollectionOnErrorThrowsHttpOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._mockSearchIndexClient
|
||||
.Setup(x => x.DeleteIndexAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.Throws(new RequestFailedException((int)HttpStatusCode.InternalServerError, "test error response"));
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<HttpOperationException>(() => this._service.DeleteCollectionAsync("test-index"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test error response", exception.Message);
|
||||
Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertReturnsValidRecordKeyAsync()
|
||||
{
|
||||
// Arrange
|
||||
var indexingResult = SearchModelFactory.IndexingResult("record-id", null, true, 200);
|
||||
var results = SearchModelFactory.IndexDocumentsResult(new[] { indexingResult });
|
||||
|
||||
this._mockSearchClient
|
||||
.Setup(x => x.IndexDocumentsAsync<AzureAISearchMemoryRecord>(
|
||||
It.IsAny<IndexDocumentsBatch<AzureAISearchMemoryRecord>>(),
|
||||
It.IsAny<IndexDocumentsOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Response.FromValue(results, Mock.Of<Response>()));
|
||||
|
||||
// Act
|
||||
var result = await this._service.UpsertAsync("test-index", this.GetTestMemoryRecord("record-id"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("record-id", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertOnInternalServerErrorThrowsHttpOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._mockSearchClient
|
||||
.Setup(x => x.IndexDocumentsAsync<AzureAISearchMemoryRecord>(
|
||||
It.IsAny<IndexDocumentsBatch<AzureAISearchMemoryRecord>>(),
|
||||
It.IsAny<IndexDocumentsOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Throws(new RequestFailedException((int)HttpStatusCode.InternalServerError, "test error response"));
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<HttpOperationException>(() => this._service.UpsertAsync("test-index", this.GetTestMemoryRecord("record-id")));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test error response", exception.Message);
|
||||
Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpsertOnNotFoundErrorCreatesIndexAsync()
|
||||
{
|
||||
// Arrange
|
||||
var indexingResult = SearchModelFactory.IndexingResult("record-id", null, true, 200);
|
||||
var results = SearchModelFactory.IndexDocumentsResult(new[] { indexingResult });
|
||||
|
||||
this._mockSearchClient
|
||||
.SetupSequence(x => x.IndexDocumentsAsync<AzureAISearchMemoryRecord>(
|
||||
It.IsAny<IndexDocumentsBatch<AzureAISearchMemoryRecord>>(),
|
||||
It.IsAny<IndexDocumentsOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Throws(new RequestFailedException((int)HttpStatusCode.NotFound, "not found"))
|
||||
.ReturnsAsync(Response.FromValue(results, Mock.Of<Response>()));
|
||||
|
||||
this._mockSearchIndexClient
|
||||
.Setup(x => x.CreateIndexAsync(It.IsAny<SearchIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Response.FromValue(new SearchIndex("test-index"), Mock.Of<Response>()));
|
||||
|
||||
// Act
|
||||
var result = await this._service.UpsertAsync("test-index", this.GetTestMemoryRecord("record-id"));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("record-id", result);
|
||||
this._mockSearchIndexClient
|
||||
.Verify(x => x.CreateIndexAsync(It.IsAny<SearchIndex>(), It.IsAny<CancellationToken>()), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetReturnsValidRecordAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._mockSearchClient
|
||||
.Setup(x => x.GetDocumentAsync<AzureAISearchMemoryRecord>(It.IsAny<string>(), It.IsAny<GetDocumentOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Response.FromValue(new AzureAISearchMemoryRecord("record-id"), Mock.Of<Response>()));
|
||||
|
||||
// Act
|
||||
var result = await this._service.GetAsync("test-index", "record-id");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(AzureAISearchMemoryRecord.EncodeId("record-id"), result.Key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetReturnsNullWhenRecordDoesNotExistAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._mockSearchClient
|
||||
.Setup(x => x.GetDocumentAsync<AzureAISearchMemoryRecord>(It.IsAny<string>(), It.IsAny<GetDocumentOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new RequestFailedException((int)HttpStatusCode.NotFound, "test error response"));
|
||||
|
||||
// Act
|
||||
var result = await this._service.GetAsync("test-collection", "test-record");
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNearestMatchesReturnsValidRecordAsync()
|
||||
{
|
||||
// Arrange
|
||||
var searchMemoryRecord = new AzureAISearchMemoryRecord("record-id");
|
||||
var searchResult = SearchModelFactory.SearchResult<AzureAISearchMemoryRecord>(searchMemoryRecord, 0.8, null);
|
||||
var results = SearchModelFactory.SearchResults<AzureAISearchMemoryRecord>([searchResult], 1, null, 0.9, Mock.Of<Response>());
|
||||
|
||||
this._mockSearchClient
|
||||
.Setup(x => x.SearchAsync<AzureAISearchMemoryRecord>(It.IsAny<string>(), It.IsAny<SearchOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Response.FromValue(results, Mock.Of<Response>()));
|
||||
|
||||
ReadOnlyMemory<float> embedding = new[] { 1f, 3f, 5f };
|
||||
|
||||
// Act
|
||||
var recordKeys = new List<string>();
|
||||
|
||||
await foreach (var (record, score) in this._service.GetNearestMatchesAsync("test-index", embedding, 1))
|
||||
{
|
||||
recordKeys.Add(record.Key);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(AzureAISearchMemoryRecord.EncodeId("record-id"), recordKeys[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNearestMatchesOnInternalServerErrorThrowsHttpOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._mockSearchClient
|
||||
.Setup(x => x.SearchAsync<AzureAISearchMemoryRecord>(It.IsAny<string>(), It.IsAny<SearchOptions>(), It.IsAny<CancellationToken>()))
|
||||
.Throws(new RequestFailedException((int)HttpStatusCode.InternalServerError, "test error response"));
|
||||
|
||||
ReadOnlyMemory<float> embedding = new[] { 1f, 3f, 5f };
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<HttpOperationException>(async () =>
|
||||
{
|
||||
await foreach (var (record, score) in this._service.GetNearestMatchesAsync("test-index", embedding, 1))
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test error response", exception.Message);
|
||||
Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNearestMatchesOnNotFoundErrorReturnsEmptyAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._mockSearchClient
|
||||
.Setup(x => x.SearchAsync<AzureAISearchMemoryRecord>(It.IsAny<string>(), It.IsAny<SearchOptions>(), It.IsAny<CancellationToken>()))
|
||||
.Throws(new RequestFailedException((int)HttpStatusCode.NotFound, "not found"));
|
||||
|
||||
ReadOnlyMemory<float> embedding = new[] { 1f, 3f, 5f };
|
||||
|
||||
// Act
|
||||
var recordKeys = new List<string>();
|
||||
|
||||
await foreach (var (record, score) in this._service.GetNearestMatchesAsync("test-index", embedding, 1))
|
||||
{
|
||||
recordKeys.Add(record.Key);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Empty(recordKeys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveBatchCallsDeleteDocumentsMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
var indexingResult = SearchModelFactory.IndexingResult("record-id", null, true, 200);
|
||||
var results = SearchModelFactory.IndexDocumentsResult(new[] { indexingResult });
|
||||
|
||||
this._mockSearchClient
|
||||
.Setup(x => x.DeleteDocumentsAsync<AzureAISearchMemoryRecord>(
|
||||
It.IsAny<IEnumerable<AzureAISearchMemoryRecord>>(),
|
||||
It.IsAny<IndexDocumentsOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Response.FromValue(results, Mock.Of<Response>()));
|
||||
|
||||
// Act
|
||||
var exception = await Record.ExceptionAsync(() => this._service.RemoveBatchAsync("text-index", ["record-id"]));
|
||||
|
||||
// Assert
|
||||
Assert.Null(exception);
|
||||
|
||||
this._mockSearchClient
|
||||
.Verify(x => x.DeleteDocumentsAsync<AzureAISearchMemoryRecord>(
|
||||
It.IsAny<IEnumerable<AzureAISearchMemoryRecord>>(),
|
||||
It.IsAny<IndexDocumentsOptions>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveBatchOnNotFoundErrorDoesNotThrowExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._mockSearchClient
|
||||
.Setup(x => x.DeleteDocumentsAsync<AzureAISearchMemoryRecord>(
|
||||
It.IsAny<IEnumerable<AzureAISearchMemoryRecord>>(),
|
||||
It.IsAny<IndexDocumentsOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Throws(new RequestFailedException((int)HttpStatusCode.NotFound, "not found"));
|
||||
|
||||
// Act
|
||||
var exception = await Record.ExceptionAsync(() => this._service.RemoveBatchAsync("text-index", ["record-id"]));
|
||||
|
||||
// Assert
|
||||
Assert.Null(exception);
|
||||
|
||||
this._mockSearchClient
|
||||
.Verify(x => x.DeleteDocumentsAsync<AzureAISearchMemoryRecord>(
|
||||
It.IsAny<IEnumerable<AzureAISearchMemoryRecord>>(),
|
||||
It.IsAny<IndexDocumentsOptions>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveBatchOnInternalServerErrorThrowsHttpOperationExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._mockSearchClient
|
||||
.Setup(x => x.DeleteDocumentsAsync<AzureAISearchMemoryRecord>(
|
||||
It.IsAny<IEnumerable<AzureAISearchMemoryRecord>>(),
|
||||
It.IsAny<IndexDocumentsOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Throws(new RequestFailedException((int)HttpStatusCode.InternalServerError, "test error response"));
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<HttpOperationException>(() => this._service.RemoveBatchAsync("text-index", ["record-id"]));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test error response", exception.Message);
|
||||
Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode);
|
||||
|
||||
this._mockSearchClient
|
||||
.Verify(x => x.DeleteDocumentsAsync<AzureAISearchMemoryRecord>(
|
||||
It.IsAny<IEnumerable<AzureAISearchMemoryRecord>>(),
|
||||
It.IsAny<IndexDocumentsOptions>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once());
|
||||
}
|
||||
|
||||
#region private
|
||||
|
||||
private MemoryRecord GetTestMemoryRecord(string id)
|
||||
{
|
||||
ReadOnlyMemory<float> embedding = new[] { 1f, 3f, 5f };
|
||||
return MemoryRecord.LocalRecord(id, "text", "description", embedding);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SemanticKernel.Connectors.AzureAISearch.UnitTests</AssemblyName>
|
||||
<RootNamespace>SemanticKernel.Connectors.AzureAISearch.UnitTests</RootNamespace>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<LangVersion>12</LangVersion>
|
||||
<RollForward>LatestMajor</RollForward>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>SKEXP0003,SKEXP0021</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Use newest available compiler to permit LangVersion 12. -->
|
||||
<!-- This can be removed once we no longer target the .NET 6 SDK in CI. -->
|
||||
<PackageReference Include="Microsoft.Net.Compilers.Toolset" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(RepoRoot)/dotnet/src/InternalUtilities/test/AssertExtensions.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Connectors.Memory.AzureAISearch\Connectors.Memory.AzureAISearch.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -31,9 +31,8 @@ public class AzureAISearchMemoryStore : IMemoryStore
|
||||
/// <param name="endpoint">Azure AI Search URI, e.g. "https://contoso.search.windows.net"</param>
|
||||
/// <param name="apiKey">API Key</param>
|
||||
public AzureAISearchMemoryStore(string endpoint, string apiKey)
|
||||
: this(new SearchIndexClient(new Uri(endpoint), new AzureKeyCredential(apiKey), GetClientOptions()))
|
||||
{
|
||||
AzureKeyCredential credentials = new(apiKey);
|
||||
this._adminClient = new SearchIndexClient(new Uri(endpoint), credentials, GetClientOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -42,8 +41,17 @@ public class AzureAISearchMemoryStore : IMemoryStore
|
||||
/// <param name="endpoint">Azure AI Search URI, e.g. "https://contoso.search.windows.net"</param>
|
||||
/// <param name="credentials">Azure service</param>
|
||||
public AzureAISearchMemoryStore(string endpoint, TokenCredential credentials)
|
||||
: this(new SearchIndexClient(new Uri(endpoint), credentials, GetClientOptions()))
|
||||
{
|
||||
this._adminClient = new SearchIndexClient(new Uri(endpoint), credentials, GetClientOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of memory storage using Azure AI Search.
|
||||
/// </summary>
|
||||
/// <param name="searchIndexClient">Azure AI Search client that can be used to manage indexes on a Search service.</param>
|
||||
public AzureAISearchMemoryStore(SearchIndexClient searchIndexClient)
|
||||
{
|
||||
this._adminClient = searchIndexClient;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -56,7 +64,7 @@ public class AzureAISearchMemoryStore : IMemoryStore
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<string> GetCollectionsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return RunMemoryStoreOperation(() => this.GetIndexesAsync(cancellationToken));
|
||||
return RunMemoryStoreOperationAsync(() => this.GetIndexesAsync(cancellationToken));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -64,7 +72,7 @@ public class AzureAISearchMemoryStore : IMemoryStore
|
||||
{
|
||||
var normalizedIndexName = this.NormalizeIndexName(collectionName);
|
||||
|
||||
var indexes = RunMemoryStoreOperation(() => this.GetIndexesAsync(cancellationToken));
|
||||
var indexes = RunMemoryStoreOperationAsync(() => this.GetIndexesAsync(cancellationToken));
|
||||
|
||||
return await indexes
|
||||
.AnyAsync(index =>
|
||||
@@ -76,19 +84,21 @@ public class AzureAISearchMemoryStore : IMemoryStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteCollectionAsync(string collectionName, CancellationToken cancellationToken = default)
|
||||
public async Task DeleteCollectionAsync(string collectionName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedIndexName = this.NormalizeIndexName(collectionName);
|
||||
|
||||
return RunMemoryStoreOperation(() => this._adminClient.DeleteIndexAsync(normalizedIndexName, cancellationToken));
|
||||
await RunMemoryStoreOperationAsync(() => this._adminClient.DeleteIndexAsync(normalizedIndexName, cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> UpsertAsync(string collectionName, MemoryRecord record, CancellationToken cancellationToken = default)
|
||||
public async Task<string> UpsertAsync(string collectionName, MemoryRecord record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedIndexName = this.NormalizeIndexName(collectionName);
|
||||
|
||||
return RunMemoryStoreOperation(() => this.UpsertRecordAsync(normalizedIndexName, AzureAISearchMemoryRecord.FromMemoryRecord(record), cancellationToken));
|
||||
return await RunMemoryStoreOperationAsync(() => this.UpsertRecordAsync(normalizedIndexName, AzureAISearchMemoryRecord.FromMemoryRecord(record), cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -98,7 +108,8 @@ public class AzureAISearchMemoryStore : IMemoryStore
|
||||
|
||||
var searchRecords = records.Select(AzureAISearchMemoryRecord.FromMemoryRecord).ToList();
|
||||
|
||||
var result = await RunMemoryStoreOperation(() => this.UpsertBatchAsync(normalizedIndexName, searchRecords, cancellationToken)).ConfigureAwait(false);
|
||||
var result = await RunMemoryStoreOperationAsync(() => this.UpsertBatchAsync(normalizedIndexName, searchRecords, cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
foreach (var x in result) { yield return x; }
|
||||
}
|
||||
@@ -115,12 +126,8 @@ public class AzureAISearchMemoryStore : IMemoryStore
|
||||
|
||||
try
|
||||
{
|
||||
result = await RunMemoryStoreOperation(async () =>
|
||||
{
|
||||
return await client
|
||||
.GetDocumentAsync<AzureAISearchMemoryRecord>(encodedId, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
result = await RunMemoryStoreOperationAsync(() => client.GetDocumentAsync<AzureAISearchMemoryRecord>(encodedId, cancellationToken: cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpOperationException e) when (e.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
@@ -196,12 +203,8 @@ public class AzureAISearchMemoryStore : IMemoryStore
|
||||
Response<SearchResults<AzureAISearchMemoryRecord>>? searchResult = null;
|
||||
try
|
||||
{
|
||||
searchResult = await RunMemoryStoreOperation(async () =>
|
||||
{
|
||||
return await client
|
||||
.SearchAsync<AzureAISearchMemoryRecord>(null, options, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
searchResult = await RunMemoryStoreOperationAsync(() => client.SearchAsync<AzureAISearchMemoryRecord>(null, options, cancellationToken: cancellationToken))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpOperationException e) when (e.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
@@ -222,9 +225,9 @@ public class AzureAISearchMemoryStore : IMemoryStore
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RemoveAsync(string collectionName, string key, CancellationToken cancellationToken = default)
|
||||
public async Task RemoveAsync(string collectionName, string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return this.RemoveBatchAsync(collectionName, new[] { key }, cancellationToken);
|
||||
await this.RemoveBatchAsync(collectionName, new[] { key }, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -235,9 +238,10 @@ public class AzureAISearchMemoryStore : IMemoryStore
|
||||
var records = keys.Select(x => new AzureAISearchMemoryRecord(x));
|
||||
|
||||
var client = this.GetSearchClient(normalizedIndexName);
|
||||
|
||||
try
|
||||
{
|
||||
await RunMemoryStoreOperation(() => client.DeleteDocumentsAsync(records, cancellationToken: cancellationToken)).ConfigureAwait(false);
|
||||
await RunMemoryStoreOperationAsync(() => client.DeleteDocumentsAsync(records, cancellationToken: cancellationToken)).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpOperationException e) when (e.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
@@ -421,17 +425,11 @@ public class AzureAISearchMemoryStore : IMemoryStore
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a memory store operation by invoking the provided operation delegate.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The return type of the operation.</typeparam>
|
||||
/// <param name="operation">The operation delegate to be executed.</param>
|
||||
/// <returns>The result of the memory store operation.</returns>
|
||||
private static T RunMemoryStoreOperation<T>(Func<T> operation)
|
||||
private static async Task<T> RunMemoryStoreOperationAsync<T>(Func<Task<T>> operation)
|
||||
{
|
||||
try
|
||||
{
|
||||
return operation.Invoke();
|
||||
return await operation.Invoke().ConfigureAwait(false);
|
||||
}
|
||||
catch (RequestFailedException e)
|
||||
{
|
||||
@@ -439,6 +437,31 @@ public class AzureAISearchMemoryStore : IMemoryStore
|
||||
}
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<T> RunMemoryStoreOperationAsync<T>(Func<IAsyncEnumerable<T>> operation)
|
||||
{
|
||||
IAsyncEnumerator<T> enumerator = operation.Invoke().GetAsyncEnumerator();
|
||||
|
||||
await using (enumerator.ConfigureAwait(false))
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await enumerator.MoveNextAsync().ConfigureAwait(false))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (RequestFailedException e)
|
||||
{
|
||||
throw e.ToHttpOperationException();
|
||||
}
|
||||
|
||||
yield return enumerator.Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static double ScoreToCosineSimilarity(double score)
|
||||
{
|
||||
// Azure AI Search score formula. The min value is 0.333 for cosine similarity -1.
|
||||
|
||||
+4
@@ -24,6 +24,10 @@
|
||||
<PackageReference Include="Azure.Search.Documents" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SemanticKernel.Connectors.AzureAISearch.UnitTests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SemanticKernel.Core\SemanticKernel.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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.Contents;
|
||||
using Microsoft.SemanticKernel.Services;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Azure OpenAI audio-to-text service.
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0005")]
|
||||
public sealed class AzureOpenAIAudioToTextService : IAudioToTextService
|
||||
{
|
||||
/// <summary>Core implementation shared by Azure OpenAI services.</summary>
|
||||
private readonly AzureOpenAIClientCore _core;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyDictionary<string, object?> Attributes => this._core.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="AzureOpenAIAudioToTextService"/> with API key auth.
|
||||
/// </summary>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="endpoint">Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="apiKey">Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="modelId">Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="AzureOpenAIAudioToTextService"/> with AAD auth.
|
||||
/// </summary>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="endpoint">Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="credentials">Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc.</param>
|
||||
/// <param name="modelId">Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="AzureOpenAIAudioToTextService"/> using the specified <see cref="OpenAIClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="openAIClient">Custom <see cref="OpenAIClient"/>.</param>
|
||||
/// <param name="modelId">Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<TextContent> GetTextContentAsync(
|
||||
AudioContent content,
|
||||
PromptExecutionSettings? executionSettings = null,
|
||||
Kernel? kernel = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> this._core.GetTextContentFromAudioAsync(content, executionSettings, cancellationToken);
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Execution settings for OpenAI audio-to-text request.
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0005")]
|
||||
public sealed class OpenAIAudioToTextExecutionSettings : PromptExecutionSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Filename or identifier associated with audio data.
|
||||
/// Should be in format {filename}.{extension}
|
||||
/// </summary>
|
||||
[JsonPropertyName("filename")]
|
||||
public string Filename
|
||||
{
|
||||
get => this._filename;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._filename = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An optional language of the audio data as two-letter ISO-639-1 language code (e.g. 'en' or 'es').
|
||||
/// </summary>
|
||||
[JsonPropertyName("language")]
|
||||
public string? Language
|
||||
{
|
||||
get => this._language;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._language = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language.
|
||||
/// </summary>
|
||||
[JsonPropertyName("prompt")]
|
||||
public string? Prompt
|
||||
{
|
||||
get => this._prompt;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._prompt = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt. Default is 'json'.
|
||||
/// </summary>
|
||||
[JsonPropertyName("response_format")]
|
||||
public string ResponseFormat
|
||||
{
|
||||
get => this._responseFormat;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._responseFormat = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[JsonPropertyName("temperature")]
|
||||
public float Temperature
|
||||
{
|
||||
get => this._temperature;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._temperature = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <see cref="OpenAIAudioToTextExecutionSettings"/> class with default filename - "file.mp3".
|
||||
/// </summary>
|
||||
public OpenAIAudioToTextExecutionSettings()
|
||||
: this(DefaultFilename)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <see cref="OpenAIAudioToTextExecutionSettings"/> class.
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename or identifier associated with audio data. Should be in format {filename}.{extension}</param>
|
||||
public OpenAIAudioToTextExecutionSettings(string filename)
|
||||
{
|
||||
this._filename = filename;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override PromptExecutionSettings Clone()
|
||||
{
|
||||
return new OpenAIAudioToTextExecutionSettings(this.Filename)
|
||||
{
|
||||
ModelId = this.ModelId,
|
||||
ExtensionData = this.ExtensionData is not null ? new Dictionary<string, object>(this.ExtensionData) : null,
|
||||
Temperature = this.Temperature,
|
||||
ResponseFormat = this.ResponseFormat,
|
||||
Language = this.Language,
|
||||
Prompt = this.Prompt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts <see cref="PromptExecutionSettings"/> to derived <see cref="OpenAIAudioToTextExecutionSettings"/> type.
|
||||
/// </summary>
|
||||
/// <param name="executionSettings">Instance of <see cref="PromptExecutionSettings"/>.</param>
|
||||
/// <returns>Instance of <see cref="OpenAIAudioToTextExecutionSettings"/>.</returns>
|
||||
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<OpenAIAudioToTextExecutionSettings>(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
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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.Contents;
|
||||
using Microsoft.SemanticKernel.Services;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAI audio-to-text service.
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0005")]
|
||||
public sealed class OpenAIAudioToTextService : IAudioToTextService
|
||||
{
|
||||
/// <summary>Core implementation shared by OpenAI services.</summary>
|
||||
private readonly OpenAIClientCore _core;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyDictionary<string, object?> Attributes => this._core.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="OpenAIAudioToTextService"/> with API key auth.
|
||||
/// </summary>
|
||||
/// <param name="modelId">Model name</param>
|
||||
/// <param name="apiKey">OpenAI API Key</param>
|
||||
/// <param name="organization">OpenAI Organization Id (usually optional)</param>
|
||||
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
|
||||
public OpenAIAudioToTextService(
|
||||
string modelId,
|
||||
string apiKey,
|
||||
string? organization = null,
|
||||
HttpClient? httpClient = null,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
this._core = new(modelId, apiKey, organization, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIAudioToTextService)));
|
||||
|
||||
this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId);
|
||||
this._core.AddAttribute(OpenAIClientCore.OrganizationKey, organization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="OpenAIAudioToTextService"/> using the specified <see cref="OpenAIClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="modelId">Model name</param>
|
||||
/// <param name="openAIClient">Custom <see cref="OpenAIClient"/> for HTTP requests.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
|
||||
public OpenAIAudioToTextService(
|
||||
string modelId,
|
||||
OpenAIClient openAIClient,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
this._core = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAIAudioToTextService)));
|
||||
|
||||
this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<TextContent> GetTextContentAsync(
|
||||
AudioContent content,
|
||||
PromptExecutionSettings? executionSettings = null,
|
||||
Kernel? kernel = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> this._core.GetTextContentFromAudioAsync(content, executionSettings, cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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.Extensions.Logging.Abstractions;
|
||||
using Microsoft.SemanticKernel.Contents;
|
||||
using Microsoft.SemanticKernel.Http;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Azure OpenAI text-to-audio client for HTTP operations.
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0005")]
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Storage for AI service attributes.
|
||||
/// </summary>
|
||||
internal Dictionary<string, object?> Attributes { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="AzureOpenAITextToAudioClient"/> with API key auth.
|
||||
/// </summary>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="endpoint">Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="apiKey">Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="modelId">Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use for logging. If null, no logging will be performed.</param>
|
||||
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<AudioContent> GetAudioContentAsync(
|
||||
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);
|
||||
using var stream = await response.Content.ReadAsStreamAndTranslateExceptionAsync().ConfigureAwait(false);
|
||||
|
||||
var binaryData = await BinaryData.FromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new AudioContent(binaryData, modelId);
|
||||
}
|
||||
|
||||
internal void AddAttribute(string key, string? value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
this.Attributes.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
#region private
|
||||
|
||||
private async Task<HttpResponseMessage> SendRequestAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
request.Headers.Add("User-Agent", HttpHeaderValues.UserAgent);
|
||||
request.Headers.Add("Api-Key", this._apiKey);
|
||||
|
||||
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
|
||||
}
|
||||
@@ -17,6 +17,7 @@ using Azure.Core.Pipeline;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Contents;
|
||||
using Microsoft.SemanticKernel.Http;
|
||||
|
||||
#pragma warning disable CA2208 // Instantiate argument exceptions correctly
|
||||
@@ -197,6 +198,16 @@ internal abstract class ClientCore
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> GetResponseMetadata(AudioTranscription audioTranscription)
|
||||
{
|
||||
return new Dictionary<string, object?>(3)
|
||||
{
|
||||
{ nameof(audioTranscription.Language), audioTranscription.Language },
|
||||
{ nameof(audioTranscription.Duration), audioTranscription.Duration },
|
||||
{ nameof(audioTranscription.Segments), audioTranscription.Segments }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an embedding from the given <paramref name="data"/>.
|
||||
/// </summary>
|
||||
@@ -230,6 +241,33 @@ internal abstract class ClientCore
|
||||
return result;
|
||||
}
|
||||
|
||||
internal async Task<TextContent> GetTextContentFromAudioAsync(
|
||||
AudioContent content,
|
||||
PromptExecutionSettings? executionSettings,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Verify.NotNull(content.Data);
|
||||
|
||||
OpenAIAudioToTextExecutionSettings? audioExecutionSettings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings);
|
||||
|
||||
Verify.ValidFilename(audioExecutionSettings?.Filename);
|
||||
|
||||
var audioOptions = new AudioTranscriptionOptions
|
||||
{
|
||||
AudioData = content.Data,
|
||||
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 TextContent(responseData.Text, this.DeploymentOrModelName, metadata: GetResponseMetadata(responseData));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new chat message
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// 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.Extensions.Logging.Abstractions;
|
||||
using Microsoft.SemanticKernel.Contents;
|
||||
using Microsoft.SemanticKernel.Http;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAI text-to-audio client for HTTP operations.
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0005")]
|
||||
internal sealed class OpenAITextToAudioClient
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private readonly string _modelId;
|
||||
private readonly string _apiKey;
|
||||
private readonly string? _organization;
|
||||
|
||||
/// <summary>
|
||||
/// Storage for AI service attributes.
|
||||
/// </summary>
|
||||
internal Dictionary<string, object?> Attributes { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="OpenAITextToAudioClient"/> with API key auth.
|
||||
/// </summary>
|
||||
/// <param name="modelId">Model name</param>
|
||||
/// <param name="apiKey">OpenAI API Key</param>
|
||||
/// <param name="organization">OpenAI Organization Id (usually optional)</param>
|
||||
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use for logging. If null, no logging will be performed.</param>
|
||||
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<AudioContent> GetAudioContentAsync(
|
||||
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);
|
||||
using var stream = await response.Content.ReadAsStreamAndTranslateExceptionAsync().ConfigureAwait(false);
|
||||
|
||||
var binaryData = await BinaryData.FromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new AudioContent(binaryData, this._modelId);
|
||||
}
|
||||
|
||||
internal void AddAttribute(string key, string? value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
this.Attributes.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
#region private
|
||||
|
||||
private async Task<HttpResponseMessage> SendRequestAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
request.Headers.Add("User-Agent", HttpHeaderValues.UserAgent);
|
||||
request.Headers.Add("Authorization", $"Bearer {this._apiKey}");
|
||||
|
||||
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
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>$(AssemblyName)</RootNamespace>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<NoWarn>$(NoWarn);NU5104;SKEXP0013,SKEXP0014</NoWarn>
|
||||
<NoWarn>$(NoWarn);NU5104;SKEXP0005,SKEXP0013,SKEXP0014</NoWarn>
|
||||
<EnablePackageValidation>true</EnablePackageValidation>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the purpose associated with the uploaded file.
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0015")]
|
||||
public enum OpenAIFilePurpose
|
||||
{
|
||||
/// <summary>
|
||||
/// File to be used by assistants for model processing.
|
||||
/// </summary>
|
||||
Assistants,
|
||||
|
||||
/// <summary>
|
||||
/// File to be used by fine-tuning jobs.
|
||||
/// </summary>
|
||||
FineTune,
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// References an uploaded file by id.
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0015")]
|
||||
public sealed class OpenAIFileReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The file identifier.
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp the file was uploaded.s
|
||||
/// </summary>
|
||||
public DateTime CreatedTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the file.s
|
||||
/// </summary>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the associated purpose of the file.
|
||||
/// </summary>
|
||||
public OpenAIFilePurpose Purpose { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The file size, in bytes.
|
||||
/// </summary>
|
||||
public int SizeInBytes { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// File service access for OpenAI: https://api.openai.com/v1/files
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0015")]
|
||||
public sealed class OpenAIFileService
|
||||
{
|
||||
private const string OpenAIApiEndpoint = "https://api.openai.com/v1/";
|
||||
private const string OpenAIApiRouteFiles = "files";
|
||||
|
||||
private readonly string _apiKey;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Uri _serviceUri;
|
||||
private readonly string? _organization;
|
||||
|
||||
/// <summary>
|
||||
/// Create an instance of the OpenAI chat completion connector
|
||||
/// </summary>
|
||||
/// <param name="apiKey">OpenAI API Key</param>
|
||||
/// <param name="organization">OpenAI Organization Id (usually optional)</param>
|
||||
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a previously uploaded file.
|
||||
/// </summary>
|
||||
/// <param name="id">The uploaded file identifier.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
public async Task DeleteFileAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Verify.NotNull(id, nameof(id));
|
||||
|
||||
await this.ExecuteDeleteRequestAsync($"{this._serviceUri}/{id}", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the file content from a previously uploaded file.
|
||||
/// </summary>
|
||||
/// <param name="id">The uploaded file identifier.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The file content as <see cref="BinaryContent"/></returns>
|
||||
/// <remarks>
|
||||
/// Files uploaded with <see cref="OpenAIFilePurpose.Assistants"/> do not support content retrieval.
|
||||
/// </remarks>
|
||||
public BinaryContent GetFileContent(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Verify.NotNull(id, nameof(id));
|
||||
|
||||
return new BinaryContent(() => this.StreamGetRequestAsync($"{this._serviceUri}/{id}/content", cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve metadata for a previously uploaded file.
|
||||
/// </summary>
|
||||
/// <param name="id">The uploaded file identifier.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>Thet metadata associated with the specified file identifier.</returns>
|
||||
public async Task<OpenAIFileReference> GetFileAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Verify.NotNull(id, nameof(id));
|
||||
|
||||
var result = await this.ExecuteGetRequestAsync<FileInfo>($"{this._serviceUri}/{id}", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return this.ConvertFileReference(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve metadata for all previously uploaded files.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>Thet metadata of all uploaded files.</returns>
|
||||
public async Task<IEnumerable<OpenAIFileReference>> GetFilesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await this.ExecuteGetRequestAsync<FileInfoList>(this._serviceUri.ToString(), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return result.Data.Select(r => this.ConvertFileReference(r)).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload a file.
|
||||
/// </summary>
|
||||
/// <param name="fileContent">The file content as <see cref="BinaryContent"/></param>
|
||||
/// <param name="settings">The upload settings</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>The file metadata.</returns>
|
||||
public async Task<OpenAIFileReference> UploadContentAsync(BinaryContent fileContent, OpenAIFileUploadExecutionSettings settings, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Verify.NotNull(settings, nameof(settings));
|
||||
|
||||
using var formData = new MultipartFormDataContent();
|
||||
using var contentPurpose = new StringContent(this.ConvertPurpose(settings.Purpose));
|
||||
using var contentStream = await fileContent.GetStreamAsync().ConfigureAwait(false);
|
||||
using var contentFile = new StreamContent(contentStream);
|
||||
formData.Add(contentPurpose, "purpose");
|
||||
formData.Add(contentFile, "file", settings.FileName);
|
||||
|
||||
var result = await this.ExecutePostRequestAsync<FileInfo>(this._serviceUri.ToString(), formData, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return this.ConvertFileReference(result);
|
||||
}
|
||||
|
||||
private async Task ExecuteDeleteRequestAsync(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = HttpRequest.CreateDeleteRequest(url);
|
||||
this.AddRequestHeaders(request);
|
||||
using var _ = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<TModel> ExecuteGetRequestAsync<TModel>(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = HttpRequest.CreateGetRequest(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<TModel>(body);
|
||||
|
||||
return
|
||||
model ??
|
||||
throw new KernelException($"Unexpected response from {url}")
|
||||
{
|
||||
Data = { { "ResponseData", body } },
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Stream> StreamGetRequestAsync(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = HttpRequest.CreateGetRequest(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);
|
||||
}
|
||||
catch
|
||||
{
|
||||
response.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TModel> ExecutePostRequestAsync<TModel>(string url, HttpContent payload, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, 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<TModel>(body);
|
||||
|
||||
return
|
||||
model ??
|
||||
throw new KernelException($"Unexpected response from {url}")
|
||||
{
|
||||
Data = { { "ResponseData", body } },
|
||||
};
|
||||
}
|
||||
|
||||
private void AddRequestHeaders(HttpRequestMessage request)
|
||||
{
|
||||
request.Headers.Add("User-Agent", HttpHeaderValues.UserAgent);
|
||||
request.Headers.Add("Authorization", $"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 = this.ConvertPurpose(result.Purpose),
|
||||
};
|
||||
}
|
||||
|
||||
private OpenAIFilePurpose ConvertPurpose(string purpose) =>
|
||||
purpose.ToUpperInvariant() switch
|
||||
{
|
||||
"ASSISTANTS" => OpenAIFilePurpose.Assistants,
|
||||
"FINE-TUNE" => OpenAIFilePurpose.FineTune,
|
||||
_ => throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."),
|
||||
};
|
||||
|
||||
private string ConvertPurpose(OpenAIFilePurpose purpose) =>
|
||||
purpose switch
|
||||
{
|
||||
OpenAIFilePurpose.Assistants => "assistants",
|
||||
OpenAIFilePurpose.FineTune => "fine-tune",
|
||||
_ => throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."),
|
||||
};
|
||||
|
||||
private class FileInfoList
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public FileInfo[] Data { get; set; } = Array.Empty<FileInfo>();
|
||||
|
||||
[JsonPropertyName("object")]
|
||||
public string Object { get; set; } = "list";
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Execution serttings associated with Open AI file upload <see cref="OpenAIFileService.UploadContentAsync"/>.
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0015")]
|
||||
public sealed class OpenAIFileUploadExecutionSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OpenAIFileUploadExecutionSettings"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The file name</param>
|
||||
/// <param name="purpose">The file purpose</param>
|
||||
public OpenAIFileUploadExecutionSettings(string fileName, OpenAIFilePurpose purpose)
|
||||
{
|
||||
Verify.NotNull(fileName, nameof(fileName));
|
||||
|
||||
this.FileName = fileName;
|
||||
this.Purpose = purpose;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The file name.
|
||||
/// </summary>
|
||||
public string FileName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The file purpose.
|
||||
/// </summary>
|
||||
public OpenAIFilePurpose Purpose { get; }
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -23,7 +24,16 @@ public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings
|
||||
/// Default is 1.0.
|
||||
/// </summary>
|
||||
[JsonPropertyName("temperature")]
|
||||
public double Temperature { get; set; } = 1;
|
||||
public double Temperature
|
||||
{
|
||||
get => this._temperature;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._temperature = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TopP controls the diversity of the completion.
|
||||
@@ -31,7 +41,16 @@ public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings
|
||||
/// Default is 1.0.
|
||||
/// </summary>
|
||||
[JsonPropertyName("top_p")]
|
||||
public double TopP { get; set; } = 1;
|
||||
public double TopP
|
||||
{
|
||||
get => this._topP;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._topP = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Number between -2.0 and 2.0. Positive values penalize new tokens
|
||||
@@ -39,7 +58,16 @@ public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings
|
||||
/// model's likelihood to talk about new topics.
|
||||
/// </summary>
|
||||
[JsonPropertyName("presence_penalty")]
|
||||
public double PresencePenalty { get; set; }
|
||||
public double PresencePenalty
|
||||
{
|
||||
get => this._presencePenalty;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._presencePenalty = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Number between -2.0 and 2.0. Positive values penalize new tokens
|
||||
@@ -47,19 +75,46 @@ public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings
|
||||
/// the model's likelihood to repeat the same line verbatim.
|
||||
/// </summary>
|
||||
[JsonPropertyName("frequency_penalty")]
|
||||
public double FrequencyPenalty { get; set; }
|
||||
public double FrequencyPenalty
|
||||
{
|
||||
get => this._frequencyPenalty;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._frequencyPenalty = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of tokens to generate in the completion.
|
||||
/// </summary>
|
||||
[JsonPropertyName("max_tokens")]
|
||||
public int? MaxTokens { get; set; }
|
||||
public int? MaxTokens
|
||||
{
|
||||
get => this._maxTokens;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._maxTokens = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sequences where the completion will stop generating further tokens.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stop_sequences")]
|
||||
public IList<string>? StopSequences { get; set; }
|
||||
public IList<string>? StopSequences
|
||||
{
|
||||
get => this._stopSequences;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._stopSequences = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How many completions to generate for each prompt. Default is 1.
|
||||
@@ -67,7 +122,16 @@ public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings
|
||||
/// Use carefully and ensure that you have reasonable settings for max_tokens and stop.
|
||||
/// </summary>
|
||||
[JsonPropertyName("results_per_prompt")]
|
||||
public int ResultsPerPrompt { get; set; } = 1;
|
||||
public int ResultsPerPrompt
|
||||
{
|
||||
get => this._resultsPerPrompt;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._resultsPerPrompt = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If specified, the system will make a best effort to sample deterministically such that repeated requests with the
|
||||
@@ -75,7 +139,16 @@ public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0013")]
|
||||
[JsonPropertyName("seed")]
|
||||
public long? Seed { get; set; }
|
||||
public long? Seed
|
||||
{
|
||||
get => this._seed;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._seed = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the response format to use for the completion.
|
||||
@@ -85,7 +158,16 @@ public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings
|
||||
/// </remarks>
|
||||
[Experimental("SKEXP0013")]
|
||||
[JsonPropertyName("response_format")]
|
||||
public object? ResponseFormat { get; set; }
|
||||
public object? ResponseFormat
|
||||
{
|
||||
get => this._responseFormat;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._responseFormat = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The system prompt to use when generating text using a chat model.
|
||||
@@ -95,8 +177,11 @@ public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings
|
||||
public string ChatSystemPrompt
|
||||
{
|
||||
get => this._chatSystemPrompt;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
value = DefaultChatSystemPrompt;
|
||||
@@ -109,7 +194,16 @@ public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings
|
||||
/// Modify the likelihood of specified tokens appearing in the completion.
|
||||
/// </summary>
|
||||
[JsonPropertyName("token_selection_biases")]
|
||||
public IDictionary<int, int>? TokenSelectionBiases { get; set; }
|
||||
public IDictionary<int, int>? TokenSelectionBiases
|
||||
{
|
||||
get => this._tokenSelectionBiases;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._tokenSelectionBiases = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the behavior for how tool calls are handled.
|
||||
@@ -141,12 +235,68 @@ public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings
|
||||
/// the function, and sending back the result. The intermediate messages will be retained in the
|
||||
/// <see cref="ChatHistory"/> if an instance was provided.
|
||||
/// </remarks>
|
||||
public ToolCallBehavior? ToolCallBehavior { get; set; }
|
||||
public ToolCallBehavior? ToolCallBehavior
|
||||
{
|
||||
get => this._toolCallBehavior;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._toolCallBehavior = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse
|
||||
/// </summary>
|
||||
public string? User { get; set; }
|
||||
public string? User
|
||||
{
|
||||
get => this._user;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._user = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Freeze()
|
||||
{
|
||||
base.Freeze();
|
||||
|
||||
if (this._stopSequences is not null)
|
||||
{
|
||||
this._stopSequences = new ReadOnlyCollection<string>(this._stopSequences);
|
||||
}
|
||||
if (this._tokenSelectionBiases is not null)
|
||||
{
|
||||
this._tokenSelectionBiases = new ReadOnlyDictionary<int, int>(this._tokenSelectionBiases);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override PromptExecutionSettings Clone()
|
||||
{
|
||||
return new OpenAIPromptExecutionSettings()
|
||||
{
|
||||
ModelId = this.ModelId,
|
||||
ExtensionData = this.ExtensionData is not null ? new Dictionary<string, object>(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<string>(this.StopSequences) : null,
|
||||
ResultsPerPrompt = this.ResultsPerPrompt,
|
||||
Seed = this.Seed,
|
||||
ResponseFormat = this.ResponseFormat,
|
||||
TokenSelectionBiases = this.TokenSelectionBiases is not null ? new Dictionary<int, int>(this.TokenSelectionBiases) : null,
|
||||
ToolCallBehavior = this.ToolCallBehavior,
|
||||
User = this.User,
|
||||
ChatSystemPrompt = this.ChatSystemPrompt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default value for chat system property.
|
||||
@@ -212,6 +362,18 @@ public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings
|
||||
|
||||
#region private ================================================================================
|
||||
|
||||
private double _temperature = 1;
|
||||
private double _topP = 1;
|
||||
private double _presencePenalty;
|
||||
private double _frequencyPenalty;
|
||||
private int? _maxTokens;
|
||||
private IList<string>? _stopSequences;
|
||||
private int _resultsPerPrompt = 1;
|
||||
private long? _seed;
|
||||
private object? _responseFormat;
|
||||
private IDictionary<int, int>? _tokenSelectionBiases;
|
||||
private ToolCallBehavior? _toolCallBehavior;
|
||||
private string? _user;
|
||||
private string _chatSystemPrompt = DefaultChatSystemPrompt;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -8,11 +8,13 @@ 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
|
||||
@@ -1124,7 +1126,7 @@ public static class OpenAIServiceCollectionExtensions
|
||||
/// <summary>
|
||||
/// Add the OpenAI Dall-E text to image service to the list
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IServiceCollection"/> instance to augment.</param>
|
||||
/// <param name="builder">The <see cref="IKernelBuilder"/> instance to augment.</param>
|
||||
/// <param name="apiKey">OpenAI API key, see https://platform.openai.com/account/api-keys</param>
|
||||
/// <param name="orgId">OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
@@ -1178,6 +1180,546 @@ public static class OpenAIServiceCollectionExtensions
|
||||
|
||||
#endregion
|
||||
|
||||
#region Files
|
||||
|
||||
/// <summary>
|
||||
/// Add the OpenAI file service to the list
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IServiceCollection"/> instance to augment.</param>
|
||||
/// <param name="apiKey">OpenAI API key, see https://platform.openai.com/account/api-keys</param>
|
||||
/// <param name="orgId">OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <param name="httpClient">The HttpClient to use with this service.</param>
|
||||
/// <returns>The same instance as <paramref name="builder"/>.</returns>
|
||||
[Experimental("SKEXP0015")]
|
||||
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<ILoggerFactory>()));
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the OpenAI file service to the list
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> instance to augment.</param>
|
||||
/// <param name="apiKey">OpenAI API key, see https://platform.openai.com/account/api-keys</param>
|
||||
/// <param name="orgId">OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <returns>The same instance as <paramref name="services"/>.</returns>
|
||||
[Experimental("SKEXP0015")]
|
||||
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<ILoggerFactory>()));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Text-to-Audio
|
||||
|
||||
/// <summary>
|
||||
/// Adds the Azure OpenAI text-to-audio service to the list.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IKernelBuilder"/> instance to augment.</param>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name</param>
|
||||
/// <param name="endpoint">Azure OpenAI deployment URL</param>
|
||||
/// <param name="apiKey">Azure OpenAI API key</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <param name="modelId">Model identifier</param>
|
||||
/// <param name="httpClient">The HttpClient to use with this service.</param>
|
||||
/// <returns>The same instance as <paramref name="builder"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
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<ITextToAudioService>(serviceId, (serviceProvider, _) =>
|
||||
new AzureOpenAITextToAudioService(
|
||||
deploymentName,
|
||||
endpoint,
|
||||
apiKey,
|
||||
modelId,
|
||||
HttpClientProvider.GetHttpClient(httpClient, serviceProvider),
|
||||
serviceProvider.GetService<ILoggerFactory>()));
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the Azure OpenAI text-to-audio service to the list.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> instance to augment.</param>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name</param>
|
||||
/// <param name="endpoint">Azure OpenAI deployment URL</param>
|
||||
/// <param name="apiKey">Azure OpenAI API key</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <param name="modelId">Model identifier</param>
|
||||
/// <param name="httpClient">The HttpClient to use with this service.</param>
|
||||
/// <returns>The same instance as <paramref name="services"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
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<ITextToAudioService>(serviceId, (serviceProvider, _) =>
|
||||
new AzureOpenAITextToAudioService(
|
||||
deploymentName,
|
||||
endpoint,
|
||||
apiKey,
|
||||
modelId,
|
||||
HttpClientProvider.GetHttpClient(serviceProvider),
|
||||
serviceProvider.GetService<ILoggerFactory>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the OpenAI text-to-audio service to the list.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IKernelBuilder"/> instance to augment.</param>
|
||||
/// <param name="modelId">OpenAI model name, see https://platform.openai.com/docs/models</param>
|
||||
/// <param name="apiKey">OpenAI API key, see https://platform.openai.com/account/api-keys</param>
|
||||
/// <param name="orgId">OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <param name="httpClient">The HttpClient to use with this service.</param>
|
||||
/// <returns>The same instance as <paramref name="builder"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
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<ITextToAudioService>(serviceId, (serviceProvider, _) =>
|
||||
new OpenAITextToAudioService(
|
||||
modelId,
|
||||
apiKey,
|
||||
orgId,
|
||||
HttpClientProvider.GetHttpClient(httpClient, serviceProvider),
|
||||
serviceProvider.GetService<ILoggerFactory>()));
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the OpenAI text-to-audio service to the list.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> instance to augment.</param>
|
||||
/// <param name="modelId">OpenAI model name, see https://platform.openai.com/docs/models</param>
|
||||
/// <param name="apiKey">OpenAI API key, see https://platform.openai.com/account/api-keys</param>
|
||||
/// <param name="orgId">OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <returns>The same instance as <paramref name="services"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
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<ITextToAudioService>(serviceId, (serviceProvider, _) =>
|
||||
new OpenAITextToAudioService(
|
||||
modelId,
|
||||
apiKey,
|
||||
orgId,
|
||||
HttpClientProvider.GetHttpClient(serviceProvider),
|
||||
serviceProvider.GetService<ILoggerFactory>()));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Audio-to-Text
|
||||
|
||||
/// <summary>
|
||||
/// Adds the Azure OpenAI audio-to-text service to the list.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IKernelBuilder"/> instance to augment.</param>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="endpoint">Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="apiKey">Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <param name="modelId">Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="httpClient">The HttpClient to use with this service.</param>
|
||||
/// <returns>The same instance as <paramref name="builder"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
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<IServiceProvider, object?, AzureOpenAIAudioToTextService> factory = (serviceProvider, _) =>
|
||||
{
|
||||
OpenAIClient client = CreateAzureOpenAIClient(
|
||||
endpoint,
|
||||
new AzureKeyCredential(apiKey),
|
||||
HttpClientProvider.GetHttpClient(httpClient, serviceProvider));
|
||||
return new(deploymentName, client, modelId, serviceProvider.GetService<ILoggerFactory>());
|
||||
};
|
||||
|
||||
builder.Services.AddKeyedSingleton<IAudioToTextService>(serviceId, factory);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the Azure OpenAI audio-to-text service to the list.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> instance to augment.</param>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="endpoint">Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="apiKey">Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <param name="modelId">Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <returns>The same instance as <paramref name="services"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
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<IServiceProvider, object?, AzureOpenAIAudioToTextService> factory = (serviceProvider, _) =>
|
||||
{
|
||||
OpenAIClient client = CreateAzureOpenAIClient(
|
||||
endpoint,
|
||||
new AzureKeyCredential(apiKey),
|
||||
HttpClientProvider.GetHttpClient(serviceProvider));
|
||||
return new(deploymentName, client, modelId, serviceProvider.GetService<ILoggerFactory>());
|
||||
};
|
||||
|
||||
services.AddKeyedSingleton<IAudioToTextService>(serviceId, factory);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the Azure OpenAI audio-to-text service to the list.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IServiceCollection"/> instance to augment.</param>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="endpoint">Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="credentials">Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <param name="modelId">Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="httpClient">The HttpClient to use with this service.</param>
|
||||
/// <returns>The same instance as <paramref name="builder"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
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<IServiceProvider, object?, AzureOpenAIAudioToTextService> factory = (serviceProvider, _) =>
|
||||
{
|
||||
OpenAIClient client = CreateAzureOpenAIClient(
|
||||
endpoint,
|
||||
credentials,
|
||||
HttpClientProvider.GetHttpClient(httpClient, serviceProvider));
|
||||
return new(deploymentName, client, modelId, serviceProvider.GetService<ILoggerFactory>());
|
||||
};
|
||||
|
||||
builder.Services.AddKeyedSingleton<IAudioToTextService>(serviceId, factory);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the Azure OpenAI audio-to-text service to the list.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> instance to augment.</param>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="endpoint">Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="credentials">Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <param name="modelId">Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <returns>The same instance as <paramref name="services"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
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<IServiceProvider, object?, AzureOpenAIAudioToTextService> factory = (serviceProvider, _) =>
|
||||
{
|
||||
OpenAIClient client = CreateAzureOpenAIClient(
|
||||
endpoint,
|
||||
credentials,
|
||||
HttpClientProvider.GetHttpClient(serviceProvider));
|
||||
return new(deploymentName, client, modelId, serviceProvider.GetService<ILoggerFactory>());
|
||||
};
|
||||
|
||||
services.AddKeyedSingleton<IAudioToTextService>(serviceId, factory);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the Azure OpenAI audio-to-text service to the list.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IKernelBuilder"/> instance to augment.</param>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="openAIClient"><see cref="OpenAIClient"/> to use for the service. If null, one must be available in the service provider when this service is resolved.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <param name="modelId">Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <returns>The same instance as <paramref name="builder"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
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<IServiceProvider, object?, AzureOpenAIAudioToTextService> factory = (serviceProvider, _) =>
|
||||
new(deploymentName, openAIClient ?? serviceProvider.GetRequiredService<OpenAIClient>(), modelId, serviceProvider.GetService<ILoggerFactory>());
|
||||
|
||||
builder.Services.AddKeyedSingleton<IAudioToTextService>(serviceId, factory);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the Azure OpenAI audio-to-text service to the list.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> instance to augment.</param>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="openAIClient"><see cref="OpenAIClient"/> to use for the service. If null, one must be available in the service provider when this service is resolved.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <param name="modelId">Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <returns>The same instance as <paramref name="services"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
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<IServiceProvider, object?, AzureOpenAIAudioToTextService> factory = (serviceProvider, _) =>
|
||||
new(deploymentName, openAIClient ?? serviceProvider.GetRequiredService<OpenAIClient>(), modelId, serviceProvider.GetService<ILoggerFactory>());
|
||||
|
||||
services.AddKeyedSingleton<IAudioToTextService>(serviceId, factory);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the OpenAI audio-to-text service to the list.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IKernelBuilder"/> instance to augment.</param>
|
||||
/// <param name="modelId">OpenAI model name, see https://platform.openai.com/docs/models</param>
|
||||
/// <param name="apiKey">OpenAI API key, see https://platform.openai.com/account/api-keys</param>
|
||||
/// <param name="orgId">OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <param name="httpClient">The HttpClient to use with this service.</param>
|
||||
/// <returns>The same instance as <paramref name="builder"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
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<IServiceProvider, object?, OpenAIAudioToTextService> factory = (serviceProvider, _) =>
|
||||
new(modelId,
|
||||
apiKey,
|
||||
orgId,
|
||||
HttpClientProvider.GetHttpClient(httpClient, serviceProvider),
|
||||
serviceProvider.GetService<ILoggerFactory>());
|
||||
|
||||
builder.Services.AddKeyedSingleton<IAudioToTextService>(serviceId, factory);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the OpenAI audio-to-text service to the list.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> instance to augment.</param>
|
||||
/// <param name="modelId">OpenAI model name, see https://platform.openai.com/docs/models</param>
|
||||
/// <param name="apiKey">OpenAI API key, see https://platform.openai.com/account/api-keys</param>
|
||||
/// <param name="orgId">OpenAI organization id. This is usually optional unless your account belongs to multiple organizations.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <returns>The same instance as <paramref name="services"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
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<IServiceProvider, object?, OpenAIAudioToTextService> factory = (serviceProvider, _) =>
|
||||
new(modelId,
|
||||
apiKey,
|
||||
orgId,
|
||||
HttpClientProvider.GetHttpClient(serviceProvider),
|
||||
serviceProvider.GetService<ILoggerFactory>());
|
||||
|
||||
services.AddKeyedSingleton<IAudioToTextService>(serviceId, factory);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the OpenAI audio-to-text service to the list.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IKernelBuilder"/> instance to augment.</param>
|
||||
/// <param name="modelId">OpenAI model id</param>
|
||||
/// <param name="openAIClient"><see cref="OpenAIClient"/> to use for the service. If null, one must be available in the service provider when this service is resolved.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <returns>The same instance as <paramref name="builder"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
public static IKernelBuilder AddOpenAIAudioToText(
|
||||
this IKernelBuilder builder,
|
||||
string modelId,
|
||||
OpenAIClient? openAIClient = null,
|
||||
string? serviceId = null)
|
||||
{
|
||||
Verify.NotNull(builder);
|
||||
Verify.NotNullOrWhiteSpace(modelId);
|
||||
|
||||
Func<IServiceProvider, object?, OpenAIAudioToTextService> factory = (serviceProvider, _) =>
|
||||
new(modelId, openAIClient ?? serviceProvider.GetRequiredService<OpenAIClient>(), serviceProvider.GetService<ILoggerFactory>());
|
||||
|
||||
builder.Services.AddKeyedSingleton<IAudioToTextService>(serviceId, factory);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the OpenAI audio-to-text service to the list.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> instance to augment.</param>
|
||||
/// <param name="modelId">OpenAI model id</param>
|
||||
/// <param name="openAIClient"><see cref="OpenAIClient"/> to use for the service. If null, one must be available in the service provider when this service is resolved.</param>
|
||||
/// <param name="serviceId">A local identifier for the given AI service</param>
|
||||
/// <returns>The same instance as <paramref name="services"/>.</returns>
|
||||
[Experimental("SKEXP0005")]
|
||||
public static IServiceCollection AddOpenAIAudioToText(
|
||||
this IServiceCollection services,
|
||||
string modelId,
|
||||
OpenAIClient? openAIClient = null,
|
||||
string? serviceId = null)
|
||||
{
|
||||
Verify.NotNull(services);
|
||||
Verify.NotNullOrWhiteSpace(modelId);
|
||||
|
||||
Func<IServiceProvider, object?, OpenAIAudioToTextService> factory = (serviceProvider, _) =>
|
||||
new(modelId, openAIClient ?? serviceProvider.GetRequiredService<OpenAIClient>(), serviceProvider.GetService<ILoggerFactory>());
|
||||
|
||||
services.AddKeyedSingleton<IAudioToTextService>(serviceId, factory);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static OpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) =>
|
||||
new(new Uri(endpoint), credentials, ClientCore.GetOpenAIClientOptions(httpClient));
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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.Contents;
|
||||
using Microsoft.SemanticKernel.Services;
|
||||
using Microsoft.SemanticKernel.TextToAudio;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Azure OpenAI text-to-audio service.
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0005")]
|
||||
public sealed class AzureOpenAITextToAudioService : ITextToAudioService
|
||||
{
|
||||
/// <summary>
|
||||
/// Azure OpenAI text-to-audio client for HTTP operations.
|
||||
/// </summary>
|
||||
private readonly AzureOpenAITextToAudioClient _client;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyDictionary<string, object?> Attributes => this._client.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key used to store the deployment name in the <see cref="IAIService.Attributes"/> dictionary.
|
||||
/// </summary>
|
||||
public static string DeploymentNameKey => "DeploymentName";
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="AzureOpenAITextToAudioService"/> connector with API key auth.
|
||||
/// </summary>
|
||||
/// <param name="deploymentName">Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="endpoint">Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="apiKey">Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart</param>
|
||||
/// <param name="modelId">Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource</param>
|
||||
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<AudioContent> GetAudioContentAsync(
|
||||
string text,
|
||||
PromptExecutionSettings? executionSettings = null,
|
||||
Kernel? kernel = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> this._client.GetAudioContentAsync(text, executionSettings, cancellationToken);
|
||||
}
|
||||
+130
@@ -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.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// Execution settings for OpenAI text-to-audio request.
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0005")]
|
||||
public sealed class OpenAITextToAudioExecutionSettings : PromptExecutionSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer.
|
||||
/// </summary>
|
||||
[JsonPropertyName("voice")]
|
||||
public string Voice
|
||||
{
|
||||
get => this._voice;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._voice = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The format to audio in. Supported formats are mp3, opus, aac, and flac.
|
||||
/// </summary>
|
||||
[JsonPropertyName("response_format")]
|
||||
public string ResponseFormat
|
||||
{
|
||||
get => this._responseFormat;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._responseFormat = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default.
|
||||
/// </summary>
|
||||
[JsonPropertyName("speed")]
|
||||
public float Speed
|
||||
{
|
||||
get => this._speed;
|
||||
|
||||
set
|
||||
{
|
||||
this.ThrowIfFrozen();
|
||||
this._speed = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <see cref="OpenAITextToAudioExecutionSettings"/> class with default voice - "alloy".
|
||||
/// </summary>
|
||||
public OpenAITextToAudioExecutionSettings()
|
||||
: this(DefaultVoice)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <see cref="OpenAITextToAudioExecutionSettings"/> class.
|
||||
/// </summary>
|
||||
/// <param name="voice">The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer.</param>
|
||||
public OpenAITextToAudioExecutionSettings(string voice)
|
||||
{
|
||||
this._voice = voice;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override PromptExecutionSettings Clone()
|
||||
{
|
||||
return new OpenAITextToAudioExecutionSettings(this.Voice)
|
||||
{
|
||||
ModelId = this.ModelId,
|
||||
ExtensionData = this.ExtensionData is not null ? new Dictionary<string, object>(this.ExtensionData) : null,
|
||||
Speed = this.Speed,
|
||||
ResponseFormat = this.ResponseFormat
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts <see cref="PromptExecutionSettings"/> to derived <see cref="OpenAITextToAudioExecutionSettings"/> type.
|
||||
/// </summary>
|
||||
/// <param name="executionSettings">Instance of <see cref="PromptExecutionSettings"/>.</param>
|
||||
/// <returns>Instance of <see cref="OpenAITextToAudioExecutionSettings"/>.</returns>
|
||||
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<OpenAITextToAudioExecutionSettings>(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
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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.Contents;
|
||||
using Microsoft.SemanticKernel.Services;
|
||||
using Microsoft.SemanticKernel.TextToAudio;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAI text-to-audio service.
|
||||
/// </summary>
|
||||
[Experimental("SKEXP0005")]
|
||||
public sealed class OpenAITextToAudioService : ITextToAudioService
|
||||
{
|
||||
/// <summary>
|
||||
/// OpenAI text-to-audio client for HTTP operations.
|
||||
/// </summary>
|
||||
private readonly OpenAITextToAudioClient _client;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the attribute name used to store the organization in the <see cref="IAIService.Attributes"/> dictionary.
|
||||
/// </summary>
|
||||
public static string OrganizationKey => "Organization";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyDictionary<string, object?> Attributes => this._client.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="OpenAITextToAudioService"/> with API key auth.
|
||||
/// </summary>
|
||||
/// <param name="modelId">Model name</param>
|
||||
/// <param name="apiKey">OpenAI API Key</param>
|
||||
/// <param name="organization">OpenAI Organization Id (usually optional)</param>
|
||||
/// <param name="httpClient">Custom <see cref="HttpClient"/> for HTTP requests.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for logging. If null, no logging will be performed.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<AudioContent> GetAudioContentAsync(
|
||||
string text,
|
||||
PromptExecutionSettings? executionSettings = null,
|
||||
Kernel? kernel = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> this._client.GetAudioContentAsync(text, executionSettings, cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAI text-to-audio request model, see <see href="https://platform.openai.com/docs/api-reference/audio/createSpeech"/>.
|
||||
/// </summary>
|
||||
internal sealed class TextToAudioRequest
|
||||
{
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; set; }
|
||||
|
||||
[JsonPropertyName("input")]
|
||||
public string Input { get; set; }
|
||||
|
||||
[JsonPropertyName("voice")]
|
||||
public string Voice { get; set; }
|
||||
|
||||
[JsonPropertyName("response_format")]
|
||||
public string ResponseFormat { get; set; } = "mp3";
|
||||
|
||||
[JsonPropertyName("speed")]
|
||||
public float Speed { get; set; } = 1.0f;
|
||||
|
||||
public TextToAudioRequest(string model, string input, string voice)
|
||||
{
|
||||
this.Model = model;
|
||||
this.Input = input;
|
||||
this.Voice = voice;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,SKEXP0012,SKEXP0013,SKEXP0014,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0052</NoWarn>
|
||||
<NoWarn>CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0005,SKEXP0010,SKEXP0011,SKEXP0012,SKEXP0013,SKEXP0014,SKEXP0015,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0052</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
// 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.Connectors.OpenAI;
|
||||
using Microsoft.SemanticKernel.Contents;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.UnitTests.OpenAI.AudioToText;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="AzureOpenAIAudioToTextService"/> class.
|
||||
/// </summary>
|
||||
public sealed class AzureOpenAIAudioToTextServiceTests : IDisposable
|
||||
{
|
||||
private readonly HttpMessageHandlerStub _messageHandlerStub;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
|
||||
|
||||
public AzureOpenAIAudioToTextServiceTests()
|
||||
{
|
||||
this._messageHandlerStub = new HttpMessageHandlerStub();
|
||||
this._httpClient = new HttpClient(this._messageHandlerStub, false);
|
||||
this._mockLoggerFactory = new Mock<ILoggerFactory>();
|
||||
}
|
||||
|
||||
[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.GetTextContentAsync(new AudioContent(new BinaryData("data")), 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.GetTextContentAsync(new AudioContent(new BinaryData("data")), new OpenAIAudioToTextExecutionSettings("file.mp3"));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("Test audio-to-text response", result.Text);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._httpClient.Dispose();
|
||||
this._messageHandlerStub.Dispose();
|
||||
}
|
||||
|
||||
public static TheoryData<OpenAIAudioToTextExecutionSettings?, Type> ExecutionSettings => new()
|
||||
{
|
||||
{ new OpenAIAudioToTextExecutionSettings(""), typeof(ArgumentException) },
|
||||
{ new OpenAIAudioToTextExecutionSettings("file"), typeof(ArgumentException) }
|
||||
};
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.UnitTests.OpenAI.AudioToText;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="OpenAIAudioToTextExecutionSettings"/> class.
|
||||
/// </summary>
|
||||
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<PromptExecutionSettings>(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);
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// 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.Connectors.OpenAI;
|
||||
using Microsoft.SemanticKernel.Contents;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.UnitTests.OpenAI.AudioToText;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="OpenAIAudioToTextService"/> class.
|
||||
/// </summary>
|
||||
public sealed class OpenAIAudioToTextServiceTests : IDisposable
|
||||
{
|
||||
private readonly HttpMessageHandlerStub _messageHandlerStub;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
|
||||
|
||||
public OpenAIAudioToTextServiceTests()
|
||||
{
|
||||
this._messageHandlerStub = new HttpMessageHandlerStub();
|
||||
this._httpClient = new HttpClient(this._messageHandlerStub, false);
|
||||
this._mockLoggerFactory = new Mock<ILoggerFactory>();
|
||||
}
|
||||
|
||||
[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.GetTextContentAsync(new AudioContent(new BinaryData("data")), new OpenAIAudioToTextExecutionSettings("file.mp3"));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("Test audio-to-text response", result.Text);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._httpClient.Dispose();
|
||||
this._messageHandlerStub.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="OpenAITextToImageService"/> class.
|
||||
/// </summary>
|
||||
public sealed class OpenAIFileServiceTests : IDisposable
|
||||
{
|
||||
private readonly HttpMessageHandlerStub _messageHandlerStub;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
|
||||
|
||||
public OpenAIFileServiceTests()
|
||||
{
|
||||
this._messageHandlerStub = new HttpMessageHandlerStub();
|
||||
this._httpClient = new HttpClient(this._messageHandlerStub, false);
|
||||
this._mockLoggerFactory = new Mock<ILoggerFactory>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void ConstructorWorksCorrectly(bool includeLoggerFactory)
|
||||
{
|
||||
// Arrange & Act
|
||||
var service = includeLoggerFactory ?
|
||||
new OpenAIFileService("api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) :
|
||||
new OpenAIFileService("api-key", "organization");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(service);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task DeleteFileWorksCorrectlyAsync(bool isFailedRequest)
|
||||
{
|
||||
// Arrange
|
||||
var service = new OpenAIFileService("api-key", "organization", this._httpClient);
|
||||
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<HttpOperationException>(() => service.DeleteFileAsync("file-id"));
|
||||
}
|
||||
else
|
||||
{
|
||||
await service.DeleteFileAsync("file-id");
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task GetFileWorksCorrectlyAsync(bool isFailedRequest)
|
||||
{
|
||||
// Arrange
|
||||
var service = new OpenAIFileService("api-key", "organization", this._httpClient);
|
||||
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<HttpOperationException>(() => 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 isFailedRequest)
|
||||
{
|
||||
// Arrange
|
||||
var service = new OpenAIFileService("api-key", "organization", this._httpClient);
|
||||
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<HttpOperationException>(() => service.GetFilesAsync());
|
||||
}
|
||||
else
|
||||
{
|
||||
var files = (await service.GetFilesAsync()).ToArray();
|
||||
Assert.NotNull(files);
|
||||
Assert.NotEmpty(files);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFileContentWorksCorrectlyAsync()
|
||||
{
|
||||
// Arrange
|
||||
var data = BinaryData.FromString("Hello AI!");
|
||||
var service = new OpenAIFileService("api-key", "organization", this._httpClient);
|
||||
this._messageHandlerStub.ResponseToReturn =
|
||||
new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new ByteArrayContent(data.ToArray())
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var content = service.GetFileContent("file-id");
|
||||
var result = await content.GetContentAsync();
|
||||
Assert.Equal(data.ToArray(), result.ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task UploadContentWorksCorrectlyAsync(bool isFailedRequest)
|
||||
{
|
||||
// Arrange
|
||||
var service = new OpenAIFileService("api-key", "organization", this._httpClient);
|
||||
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(() => Task.FromResult<Stream>(stream));
|
||||
|
||||
// Act & Assert
|
||||
if (isFailedRequest)
|
||||
{
|
||||
await Assert.ThrowsAsync<HttpOperationException>(() => 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 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();
|
||||
}
|
||||
}
|
||||
+47
-1
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Microsoft.SemanticKernel;
|
||||
@@ -63,7 +64,7 @@ public class OpenAIPromptExecutionSettingsTests
|
||||
// Arrange
|
||||
PromptExecutionSettings actualSettings = new()
|
||||
{
|
||||
ExtensionData = new() {
|
||||
ExtensionData = new Dictionary<string, object>() {
|
||||
{ "max_tokens", 1000 },
|
||||
{ "temperature", 0 }
|
||||
}
|
||||
@@ -170,6 +171,51 @@ public class OpenAIPromptExecutionSettingsTests
|
||||
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<OpenAIPromptExecutionSettings>(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
|
||||
}";
|
||||
var executionSettings = JsonSerializer.Deserialize<OpenAIPromptExecutionSettings>(configPayload);
|
||||
|
||||
// Act
|
||||
executionSettings!.Freeze();
|
||||
|
||||
// Assert
|
||||
Assert.True(executionSettings.IsFrozen);
|
||||
Assert.Throws<InvalidOperationException>(() => executionSettings.ModelId = "gpt-4");
|
||||
Assert.Throws<InvalidOperationException>(() => executionSettings.ResultsPerPrompt = 2);
|
||||
Assert.Throws<InvalidOperationException>(() => executionSettings.Temperature = 1);
|
||||
}
|
||||
|
||||
private static void AssertExecutionSettings(OpenAIPromptExecutionSettings executionSettings)
|
||||
{
|
||||
Assert.NotNull(executionSettings);
|
||||
|
||||
+184
@@ -6,10 +6,12 @@ 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;
|
||||
|
||||
@@ -467,6 +469,188 @@ public sealed class OpenAIServiceCollectionExtensionsTests : IDisposable
|
||||
Assert.True(service is OpenAITextToImageService);
|
||||
}
|
||||
|
||||
[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<ITextToAudioService>();
|
||||
|
||||
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<ITextToAudioService>();
|
||||
|
||||
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<ITextToAudioService>();
|
||||
|
||||
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<ITextToAudioService>();
|
||||
|
||||
Assert.NotNull(service);
|
||||
Assert.True(service is OpenAITextToAudioService);
|
||||
}
|
||||
|
||||
[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<OpenAIClient>(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<IAudioToTextService>();
|
||||
|
||||
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<OpenAIClient>(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<IAudioToTextService>();
|
||||
|
||||
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<OpenAIClient>(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<IAudioToTextService>();
|
||||
|
||||
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<OpenAIClient>(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<IAudioToTextService>();
|
||||
|
||||
Assert.NotNull(service);
|
||||
Assert.True(service is OpenAIAudioToTextService);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._httpClient.Dispose();
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="AzureOpenAITextToAudioService"/> class.
|
||||
/// </summary>
|
||||
public sealed class AzureOpenAITextToAudioServiceTests : IDisposable
|
||||
{
|
||||
private readonly HttpMessageHandlerStub _messageHandlerStub;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
|
||||
|
||||
public AzureOpenAITextToAudioServiceTests()
|
||||
{
|
||||
this._messageHandlerStub = new HttpMessageHandlerStub();
|
||||
this._httpClient = new HttpClient(this._messageHandlerStub, false);
|
||||
this._mockLoggerFactory = new Mock<ILoggerFactory>();
|
||||
}
|
||||
|
||||
[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.GetAudioContentAsync("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.GetAudioContentAsync("Some text", new OpenAITextToAudioExecutionSettings("voice"));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result?.Data);
|
||||
Assert.True(result.Data.ToArray().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.GetAudioContentAsync("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<OpenAITextToAudioExecutionSettings?, Type> ExecutionSettings => new()
|
||||
{
|
||||
{ new OpenAITextToAudioExecutionSettings(""), typeof(ArgumentException) },
|
||||
};
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextToAudio;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="OpenAITextToAudioExecutionSettings"/> class.
|
||||
/// </summary>
|
||||
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<PromptExecutionSettings>(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);
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="OpenAITextToAudioService"/> class.
|
||||
/// </summary>
|
||||
public sealed class OpenAITextToAudioServiceTests : IDisposable
|
||||
{
|
||||
private readonly HttpMessageHandlerStub _messageHandlerStub;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
|
||||
|
||||
public OpenAITextToAudioServiceTests()
|
||||
{
|
||||
this._messageHandlerStub = new HttpMessageHandlerStub();
|
||||
this._httpClient = new HttpClient(this._messageHandlerStub, false);
|
||||
this._mockLoggerFactory = new Mock<ILoggerFactory>();
|
||||
}
|
||||
|
||||
[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.GetAudioContentAsync("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.GetAudioContentAsync("Some text", new OpenAITextToAudioExecutionSettings("voice"));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result?.Data);
|
||||
Assert.True(result.Data.ToArray().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.GetAudioContentAsync("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<OpenAITextToAudioExecutionSettings?, Type> ExecutionSettings => new()
|
||||
{
|
||||
{ new OpenAITextToAudioExecutionSettings(""), typeof(ArgumentException) },
|
||||
};
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SemanticKernel.Experimental.Assistants.UnitTests</AssemblyName>
|
||||
<RootNamespace>SemanticKernel.Experimental.Assistants.UnitTests</RootNamespace>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RollForward>LatestMajor</RollForward>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>CS1591;SKEXP0101</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Assistants\Experimental.Assistants.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="Templates\GameAssistant.yaml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Templates\PoetAssistant.yaml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="testsettings.development.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="testsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Experimental.Assistants.UnitTests;
|
||||
|
||||
[Trait("Category", "Unit Tests")]
|
||||
[Trait("Feature", "Assistant")]
|
||||
public sealed class KernelExtensionTests
|
||||
{
|
||||
private const string TwoPartToolName = "Fake-Bogus";
|
||||
|
||||
[Fact]
|
||||
public static void InvokeTwoPartTool()
|
||||
{
|
||||
//Arrange
|
||||
var function = KernelFunctionFactory.CreateFromMethod(() => { }, functionName: "Bogus");
|
||||
|
||||
var kernel = new Kernel();
|
||||
kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("Fake", "Fake functions", new[] { function }));
|
||||
|
||||
//Act
|
||||
var tool = kernel.GetAssistantTool(TwoPartToolName);
|
||||
|
||||
//Assert
|
||||
Assert.NotNull(tool);
|
||||
Assert.Equal("Bogus", tool.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Bogus")]
|
||||
[InlineData("i-am-not-valid")]
|
||||
public static void InvokeInvalidSinglePartTool(string toolName)
|
||||
{
|
||||
//Arrange
|
||||
var kernel = new Kernel();
|
||||
|
||||
//Act & Assert
|
||||
Assert.Throws<AssistantException>(() => kernel.GetAssistantTool(toolName));
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Experimental.Assistants.UnitTests;
|
||||
|
||||
[Trait("Category", "Unit Tests")]
|
||||
[Trait("Feature", "Assistant")]
|
||||
public sealed class KernelFunctionExtensionTests
|
||||
{
|
||||
private const string ToolName = "Bogus";
|
||||
private const string PluginName = "Fake";
|
||||
|
||||
[Fact]
|
||||
public static void GetTwoPartName()
|
||||
{
|
||||
var function = KernelFunctionFactory.CreateFromMethod(() => true, ToolName);
|
||||
|
||||
string qualifiedName = function.GetQualifiedName(PluginName);
|
||||
|
||||
Assert.Equal($"{PluginName}-{ToolName}", qualifiedName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void GetToolModelFromFunction()
|
||||
{
|
||||
const string FunctionDescription = "Bogus description";
|
||||
const string RequiredParamName = "required";
|
||||
const string OptionalParamName = "optional";
|
||||
|
||||
var requiredParam = new KernelParameterMetadata("required") { IsRequired = true };
|
||||
var optionalParam = new KernelParameterMetadata("optional");
|
||||
var parameters = new List<KernelParameterMetadata> { requiredParam, optionalParam };
|
||||
var function = KernelFunctionFactory.CreateFromMethod(() => true, ToolName, FunctionDescription, parameters);
|
||||
|
||||
var toolModel = function.ToToolModel(PluginName);
|
||||
var properties = toolModel.Function?.Parameters.Properties;
|
||||
var required = toolModel.Function?.Parameters.Required;
|
||||
|
||||
Assert.Equal("function", toolModel.Type);
|
||||
Assert.Equal($"{PluginName}-{ToolName}", toolModel.Function?.Name);
|
||||
Assert.Equal(FunctionDescription, toolModel.Function?.Description);
|
||||
Assert.Equal(2, properties?.Count);
|
||||
Assert.True(properties?.ContainsKey(RequiredParamName));
|
||||
Assert.True(properties?.ContainsKey(OptionalParamName));
|
||||
Assert.Equal(1, required?.Count ?? 0);
|
||||
Assert.True(required?.Contains(RequiredParamName) ?? false);
|
||||
}
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Experimental.Assistants.UnitTests;
|
||||
|
||||
[Trait("Category", "Unit Tests")]
|
||||
[Trait("Feature", "Assistant")]
|
||||
public sealed class OpenAIRestExtensionsAssistantTests
|
||||
{
|
||||
private const string BogusApiKey = "bogus";
|
||||
private const string TestAssistantId = "assistantId";
|
||||
|
||||
private readonly AssistantModel _assistantModel = new();
|
||||
private readonly OpenAIRestContext _restContext;
|
||||
private readonly Mock<HttpMessageHandler> _mockHttpMessageHandler = new();
|
||||
|
||||
public OpenAIRestExtensionsAssistantTests()
|
||||
{
|
||||
this._mockHttpMessageHandler
|
||||
.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") });
|
||||
this._restContext = new(BogusApiKey, () => new HttpClient(this._mockHttpMessageHandler.Object));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAssistantModelAsync()
|
||||
{
|
||||
await this._restContext.CreateAssistantModelAsync(this._assistantModel).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.BaseAssistantUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAssistantModelAsync()
|
||||
{
|
||||
await this._restContext.GetAssistantModelAsync(TestAssistantId).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetAssistantUrl(TestAssistantId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListAssistantModelsAsync()
|
||||
{
|
||||
await this._restContext.ListAssistantModelsAsync(10, false, "20").ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, $"{OpenAIRestExtensions.BaseAssistantUrl}?limit=10&order=desc&after=20");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAssistantModelAsync()
|
||||
{
|
||||
await this._restContext.DeleteAssistantModelAsync(TestAssistantId).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Delete, 1, OpenAIRestExtensions.GetAssistantUrl(TestAssistantId));
|
||||
}
|
||||
}
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Experimental.Assistants.UnitTests;
|
||||
|
||||
[Trait("Category", "Unit Tests")]
|
||||
[Trait("Feature", "Assistant")]
|
||||
public sealed class OpenAIRestExtensionsMessagesTests
|
||||
{
|
||||
private const string BogusApiKey = "bogus";
|
||||
private const string TestThreadId = "threadId";
|
||||
private const string TestMessageId = "msgId";
|
||||
private const string TestContent = "Blah blah";
|
||||
|
||||
private readonly OpenAIRestContext _restContext;
|
||||
private readonly Mock<HttpMessageHandler> _mockHttpMessageHandler = new();
|
||||
|
||||
public OpenAIRestExtensionsMessagesTests()
|
||||
{
|
||||
this._mockHttpMessageHandler
|
||||
.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") });
|
||||
this._restContext = new(BogusApiKey, () => new HttpClient(this._mockHttpMessageHandler.Object));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateMessageModelAsync()
|
||||
{
|
||||
await this._restContext.CreateUserTextMessageAsync(TestThreadId, TestContent).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.GetMessagesUrl(TestThreadId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMessageModelAsync()
|
||||
{
|
||||
await this._restContext.GetMessageAsync(TestThreadId, TestMessageId).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetMessagesUrl(TestThreadId, TestMessageId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMessageModelsAsync()
|
||||
{
|
||||
await this._restContext.GetMessagesAsync(TestThreadId).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetMessagesUrl(TestThreadId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSpecificMessageModelsAsync()
|
||||
{
|
||||
var messageIDs = new string[] { "1", "2", "3" };
|
||||
|
||||
await this._restContext.GetMessagesAsync(TestThreadId, messageIDs).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, messageIDs.Length);
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Experimental.Assistants.UnitTests;
|
||||
|
||||
[Trait("Category", "Unit Tests")]
|
||||
[Trait("Feature", "Assistant")]
|
||||
public sealed class OpenAIRestExtensionsRunTests
|
||||
{
|
||||
private const string BogusApiKey = "bogus";
|
||||
private const string TestAssistantId = "assistantId";
|
||||
private const string TestThreadId = "threadId";
|
||||
private const string TestRunId = "runId";
|
||||
|
||||
private readonly OpenAIRestContext _restContext;
|
||||
private readonly Mock<HttpMessageHandler> _mockHttpMessageHandler = new();
|
||||
|
||||
public OpenAIRestExtensionsRunTests()
|
||||
{
|
||||
this._mockHttpMessageHandler
|
||||
.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") });
|
||||
this._restContext = new(BogusApiKey, () => new HttpClient(this._mockHttpMessageHandler.Object));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateRunAsync()
|
||||
{
|
||||
await this._restContext.CreateRunAsync(TestThreadId, TestAssistantId).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.GetRunUrl(TestThreadId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRunAsync()
|
||||
{
|
||||
await this._restContext.GetRunAsync(TestThreadId, TestRunId).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetRunUrl(TestThreadId, TestRunId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRunStepsAsync()
|
||||
{
|
||||
await this._restContext.GetRunStepsAsync(TestThreadId, TestRunId).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetRunStepsUrl(TestThreadId, TestRunId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddToolOutputsAsync()
|
||||
{
|
||||
var toolResults = Array.Empty<ToolResultModel>();
|
||||
|
||||
await this._restContext.AddToolOutputsAsync(TestThreadId, TestRunId, toolResults).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.GetRunToolOutput(TestThreadId, TestRunId));
|
||||
}
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Experimental.Assistants.UnitTests;
|
||||
|
||||
[Trait("Category", "Unit Tests")]
|
||||
[Trait("Feature", "Assistant")]
|
||||
public sealed class OpenAIRestExtensionsThreadTests
|
||||
{
|
||||
private const string BogusApiKey = "bogus";
|
||||
private const string TestThreadId = "threadId";
|
||||
|
||||
private readonly OpenAIRestContext _restContext;
|
||||
private readonly Mock<HttpMessageHandler> _mockHttpMessageHandler = new();
|
||||
|
||||
public OpenAIRestExtensionsThreadTests()
|
||||
{
|
||||
this._mockHttpMessageHandler
|
||||
.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") });
|
||||
this._restContext = new(BogusApiKey, () => new HttpClient(this._mockHttpMessageHandler.Object));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateThreadModelAsync()
|
||||
{
|
||||
await this._restContext.CreateThreadModelAsync().ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Post, 1, OpenAIRestExtensions.BaseThreadUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetThreadModelAsync()
|
||||
{
|
||||
await this._restContext.GetThreadModelAsync(TestThreadId).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Get, 1, OpenAIRestExtensions.GetThreadUrl(TestThreadId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteThreadModelAsync()
|
||||
{
|
||||
await this._restContext.DeleteThreadModelAsync(TestThreadId).ConfigureAwait(true);
|
||||
|
||||
this._mockHttpMessageHandler.VerifyMock(HttpMethod.Delete, 1, OpenAIRestExtensions.GetThreadUrl(TestThreadId));
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#define DISABLEHOST // Comment line to enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace SemanticKernel.Experimental.Assistants.UnitTests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Dev harness for manipulating assistants.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Comment out DISABLEHOST definition to enable tests.
|
||||
/// Not enabled by default.
|
||||
/// </remarks>
|
||||
[Trait("Category", "Integration Tests")]
|
||||
[Trait("Feature", "Assistant")]
|
||||
public sealed class AssistantHarness
|
||||
{
|
||||
#if DISABLEHOST
|
||||
private const string SkipReason = "Harness only for local/dev environment";
|
||||
#else
|
||||
private const string SkipReason = null;
|
||||
#endif
|
||||
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
/// <summary>
|
||||
/// Test constructor.
|
||||
/// </summary>
|
||||
public AssistantHarness(ITestOutputHelper output)
|
||||
{
|
||||
this._output = output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify creation and retrieval of assistant.
|
||||
/// </summary>
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task VerifyAssistantLifecycleAsync()
|
||||
{
|
||||
var assistant =
|
||||
await AssistantBuilder.NewAsync(
|
||||
apiKey: TestConfig.OpenAIApiKey,
|
||||
model: TestConfig.SupportedGpt35TurboModel,
|
||||
instructions: "say something funny",
|
||||
name: "Fred",
|
||||
description: "test assistant").ConfigureAwait(true);
|
||||
|
||||
this.DumpAssistant(assistant);
|
||||
|
||||
var copy =
|
||||
await AssistantBuilder.GetAssistantAsync(
|
||||
apiKey: TestConfig.OpenAIApiKey,
|
||||
assistantId: assistant.Id).ConfigureAwait(true);
|
||||
|
||||
this.DumpAssistant(copy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify creation and retrieval of assistant.
|
||||
/// </summary>
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task VerifyAssistantDefinitionAsync()
|
||||
{
|
||||
var assistant =
|
||||
await new AssistantBuilder()
|
||||
.WithOpenAIChatCompletion(TestConfig.SupportedGpt35TurboModel, TestConfig.OpenAIApiKey)
|
||||
.FromTemplatePath("Templates/PoetAssistant.yaml")
|
||||
.BuildAsync()
|
||||
.ConfigureAwait(true);
|
||||
|
||||
this.DumpAssistant(assistant);
|
||||
|
||||
var copy =
|
||||
await AssistantBuilder.GetAssistantAsync(
|
||||
apiKey: TestConfig.OpenAIApiKey,
|
||||
assistantId: assistant.Id).ConfigureAwait(true);
|
||||
|
||||
this.DumpAssistant(copy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify creation and retrieval of assistant.
|
||||
/// </summary>
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task VerifyAssistantListAsync()
|
||||
{
|
||||
var context = new OpenAIRestContext(TestConfig.OpenAIApiKey);
|
||||
var assistants = await context.ListAssistantModelsAsync().ConfigureAwait(true);
|
||||
foreach (var assistant in assistants)
|
||||
{
|
||||
this.DumpAssistant(assistant);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify creation and retrieval of assistant.
|
||||
/// </summary>
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task VerifyAssistantDeleteAsync()
|
||||
{
|
||||
var names =
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"Fred",
|
||||
"Barney",
|
||||
"DeleteMe",
|
||||
"Poet",
|
||||
"Math Tutor",
|
||||
};
|
||||
|
||||
var context = new OpenAIRestContext(TestConfig.OpenAIApiKey);
|
||||
var assistants = await context.ListAssistantModelsAsync().ConfigureAwait(true);
|
||||
foreach (var assistant in assistants)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(assistant.Name) && names.Contains(assistant.Name))
|
||||
{
|
||||
this._output.WriteLine($"Removing: {assistant.Name} - {assistant.Id}");
|
||||
await context.DeleteAssistantModelAsync(assistant.Id).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DumpAssistant(AssistantModel assistant)
|
||||
{
|
||||
this._output.WriteLine($"# {assistant.Id}");
|
||||
this._output.WriteLine($"# {assistant.Model}");
|
||||
this._output.WriteLine($"# {assistant.Instructions}");
|
||||
this._output.WriteLine($"# {assistant.Name}");
|
||||
this._output.WriteLine($"# {assistant.Description}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
private void DumpAssistant(IAssistant assistant)
|
||||
{
|
||||
this._output.WriteLine($"# {assistant.Id}");
|
||||
this._output.WriteLine($"# {assistant.Model}");
|
||||
this._output.WriteLine($"# {assistant.Instructions}");
|
||||
this._output.WriteLine($"# {assistant.Name}");
|
||||
this._output.WriteLine($"# {assistant.Description}{Environment.NewLine}");
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#define DISABLEHOST // Comment line to enable
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
#pragma warning disable CA1812 // Uninstantiated internal types
|
||||
|
||||
namespace SemanticKernel.Experimental.Assistants.UnitTests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Dev harness for manipulating runs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Comment out DISABLEHOST definition to enable tests.
|
||||
/// Not enabled by default.
|
||||
/// </remarks>
|
||||
[Trait("Category", "Integration Tests")]
|
||||
[Trait("Feature", "Assistant")]
|
||||
public sealed class RunHarness
|
||||
{
|
||||
#if DISABLEHOST
|
||||
private const string SkipReason = "Harness only for local/dev environment";
|
||||
#else
|
||||
private const string SkipReason = null;
|
||||
#endif
|
||||
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
/// <summary>
|
||||
/// Test constructor.
|
||||
/// </summary>
|
||||
public RunHarness(ITestOutputHelper output)
|
||||
{
|
||||
this._output = output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify creation of run.
|
||||
/// </summary>
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task VerifyRunLifecycleAsync()
|
||||
{
|
||||
var assistant =
|
||||
await AssistantBuilder.NewAsync(
|
||||
apiKey: TestConfig.OpenAIApiKey,
|
||||
model: TestConfig.SupportedGpt35TurboModel,
|
||||
instructions: "say something funny",
|
||||
name: "Fred",
|
||||
description: "funny assistant").ConfigureAwait(true);
|
||||
|
||||
var thread = await assistant.NewThreadAsync().ConfigureAwait(true);
|
||||
|
||||
await this.ChatAsync(
|
||||
thread,
|
||||
assistant,
|
||||
"I was on my way to the store this morning and...",
|
||||
"That was great! Tell me another.").ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify creation of run.
|
||||
/// </summary>
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task VerifyRunFromDefinitionAsync()
|
||||
{
|
||||
var assistant =
|
||||
await new AssistantBuilder()
|
||||
.WithOpenAIChatCompletion(TestConfig.SupportedGpt35TurboModel, TestConfig.OpenAIApiKey)
|
||||
.FromTemplatePath("Templates/PoetAssistant.yaml")
|
||||
.BuildAsync()
|
||||
.ConfigureAwait(true);
|
||||
|
||||
var thread = await assistant.NewThreadAsync().ConfigureAwait(true);
|
||||
|
||||
await this.ChatAsync(
|
||||
thread,
|
||||
assistant,
|
||||
"Eggs are yummy and beautiful geometric gems.",
|
||||
"It rains a lot in Seattle.").ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify creation of run.
|
||||
/// </summary>
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task VerifyFunctionLifecycleAsync()
|
||||
{
|
||||
var gamePlugin = KernelPluginFactory.CreateFromType<GuessingGame>();
|
||||
|
||||
var assistant =
|
||||
await new AssistantBuilder()
|
||||
.WithOpenAIChatCompletion(TestConfig.SupportedGpt35TurboModel, TestConfig.OpenAIApiKey)
|
||||
.FromTemplatePath("Templates/GameAssistant.yaml")
|
||||
.WithPlugin(gamePlugin)
|
||||
.BuildAsync()
|
||||
.ConfigureAwait(true);
|
||||
|
||||
var thread = await assistant.NewThreadAsync().ConfigureAwait(true);
|
||||
|
||||
await this.ChatAsync(
|
||||
thread,
|
||||
assistant,
|
||||
"What is the question for the guessing game?",
|
||||
"Is it 'RED'?",
|
||||
"What is the answer?").ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private async Task ChatAsync(IChatThread thread, IAssistant assistant, params string[] messages)
|
||||
{
|
||||
foreach (var message in messages)
|
||||
{
|
||||
var messageUser = await thread.AddUserMessageAsync(message).ConfigureAwait(true);
|
||||
this.LogMessage(messageUser);
|
||||
|
||||
var assistantMessages = await thread.InvokeAsync(assistant).ToArrayAsync().ConfigureAwait(true);
|
||||
this.LogMessages(assistantMessages);
|
||||
}
|
||||
}
|
||||
|
||||
private void LogMessages(IEnumerable<IChatMessage> messages)
|
||||
{
|
||||
foreach (var message in messages)
|
||||
{
|
||||
this.LogMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void LogMessage(IChatMessage message)
|
||||
{
|
||||
this._output.WriteLine($"# {message.Id}");
|
||||
this._output.WriteLine($"# {message.Content}");
|
||||
this._output.WriteLine($"# {message.Role}");
|
||||
this._output.WriteLine($"# {message.AssistantId}");
|
||||
}
|
||||
|
||||
private sealed class GuessingGame
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the question
|
||||
/// </summary>
|
||||
[KernelFunction, Description("Get the guessing game question")]
|
||||
public string GetQuestion() => "What color am I thinking of?";
|
||||
|
||||
/// <summary>
|
||||
/// Get the answer
|
||||
/// </summary>
|
||||
[KernelFunction, Description("Get the answer to the guessing game question.")]
|
||||
public string GetAnswer() => "Blue";
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
#define DISABLEHOST // Comment line to enable
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace SemanticKernel.Experimental.Assistants.UnitTests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Dev harness for manipulating threads.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Comment out DISABLEHOST definition to enable tests.
|
||||
/// Not enabled by default.
|
||||
/// </remarks>
|
||||
[Trait("Category", "Integration Tests")]
|
||||
[Trait("Feature", "Assistant")]
|
||||
public sealed class ThreadHarness
|
||||
{
|
||||
#if DISABLEHOST
|
||||
private const string SkipReason = "Harness only for local/dev environment";
|
||||
#else
|
||||
private const string SkipReason = null;
|
||||
#endif
|
||||
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
/// <summary>
|
||||
/// Test constructor.
|
||||
/// </summary>
|
||||
public ThreadHarness(ITestOutputHelper output)
|
||||
{
|
||||
this._output = output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify creation and retrieval of thread.
|
||||
/// </summary>
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task VerifyThreadLifecycleAsync()
|
||||
{
|
||||
var assistant =
|
||||
await new AssistantBuilder()
|
||||
.WithOpenAIChatCompletion(TestConfig.SupportedGpt35TurboModel, TestConfig.OpenAIApiKey)
|
||||
.WithName("DeleteMe")
|
||||
.BuildAsync()
|
||||
.ConfigureAwait(true);
|
||||
|
||||
var thread = await assistant.NewThreadAsync().ConfigureAwait(true);
|
||||
|
||||
Assert.NotNull(thread.Id);
|
||||
|
||||
this._output.WriteLine($"# {thread.Id}");
|
||||
|
||||
var message = await thread.AddUserMessageAsync("I'm so confused!").ConfigureAwait(true);
|
||||
Assert.NotNull(message);
|
||||
|
||||
this._output.WriteLine($"# {message.Id}");
|
||||
|
||||
var context = new OpenAIRestContext(TestConfig.OpenAIApiKey);
|
||||
var copy = await context.GetThreadModelAsync(thread.Id).ConfigureAwait(true);
|
||||
|
||||
await context.DeleteThreadModelAsync(thread.Id).ConfigureAwait(true);
|
||||
|
||||
await Assert.ThrowsAsync<KernelException>(() => context.GetThreadModelAsync(thread.Id)).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
|
||||
namespace SemanticKernel.Experimental.Assistants.UnitTests;
|
||||
|
||||
internal static class MockExtensions
|
||||
{
|
||||
public static void VerifyMock(this Mock<HttpMessageHandler> mockHandler, HttpMethod method, int times, string? uri = null)
|
||||
{
|
||||
mockHandler.Protected().Verify(
|
||||
"SendAsync",
|
||||
Times.Exactly(times),
|
||||
ItExpr.Is<HttpRequestMessage>(req => req.Method == method && (uri == null || req.RequestUri == new Uri(uri))),
|
||||
ItExpr.IsAny<CancellationToken>());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
name: Fred
|
||||
instructions: |
|
||||
Run a guessing game where the user tries to guess the answer to a question but don't tell them the answer unless they give up by asking for the answer.
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
name: Poet
|
||||
instructions: |
|
||||
Compose a sonnet inspired by the user input.
|
||||
description: You are a poet that composes poems based on user input.
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace SemanticKernel.Experimental.Assistants.UnitTests;
|
||||
|
||||
internal static class TestConfig
|
||||
{
|
||||
public const string SupportedGpt35TurboModel = "gpt-3.5-turbo-1106";
|
||||
|
||||
public static IConfiguration Configuration { get; } = CreateConfiguration();
|
||||
|
||||
public static string OpenAIApiKey =>
|
||||
TestConfig.Configuration.GetValue<string>("OpenAIApiKey") ??
|
||||
throw new TestClassException("Missing OpenAI APIKey.");
|
||||
|
||||
private static IConfiguration CreateConfiguration()
|
||||
{
|
||||
return
|
||||
new ConfigurationBuilder()
|
||||
.AddEnvironmentVariables()
|
||||
.AddJsonFile("testsettings.json")
|
||||
.AddJsonFile("testsettings.development.json", optional: true)
|
||||
.AddUserSecrets(Assembly.GetExecutingAssembly())
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"OpenAIApiKey": ""
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Context for interacting with OpenAI REST API.
|
||||
/// </summary>
|
||||
public partial class AssistantBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new assistant.
|
||||
/// </summary>
|
||||
/// <param name="apiKey">The OpenAI API key</param>
|
||||
/// <param name="model">The assistant chat model (required)</param>
|
||||
/// <param name="instructions">The assistant instructions (required)</param>
|
||||
/// <param name="name">The assistant name (optional)</param>
|
||||
/// <param name="description">The assistant description(optional)</param>
|
||||
/// <returns>The requested <see cref="IAssistant">.</see></returns>
|
||||
public static async Task<IAssistant> NewAsync(
|
||||
string apiKey,
|
||||
string model,
|
||||
string instructions,
|
||||
string? name = null,
|
||||
string? description = null)
|
||||
{
|
||||
return
|
||||
await new AssistantBuilder()
|
||||
.WithOpenAIChatCompletion(model, apiKey)
|
||||
.WithInstructions(instructions)
|
||||
.WithName(name)
|
||||
.WithDescription(description)
|
||||
.BuildAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve an existing assistant, by identifier.
|
||||
/// </summary>
|
||||
/// <param name="apiKey">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="assistantId">The assistant identifier</param>
|
||||
/// <param name="plugins">Plugins to initialize as assistant tools</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>An initialized <see cref="Assistant"> instance.</see></returns>
|
||||
public static async Task<IAssistant> GetAssistantAsync(
|
||||
string apiKey,
|
||||
string assistantId,
|
||||
IEnumerable<KernelPlugin>? plugins = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var restContext = new OpenAIRestContext(apiKey);
|
||||
var resultModel = await restContext.GetAssistantModelAsync(assistantId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new Assistant(resultModel, restContext, plugins);
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Fluent builder for initializing an <see cref="IAssistant"/> instance.
|
||||
/// </summary>
|
||||
public partial class AssistantBuilder
|
||||
{
|
||||
private readonly AssistantModel _model;
|
||||
private readonly KernelPluginCollection _plugins;
|
||||
|
||||
private string? _apiKey;
|
||||
private Func<HttpClient>? _httpClientProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AssistantBuilder"/> class.
|
||||
/// </summary>
|
||||
public AssistantBuilder()
|
||||
{
|
||||
this._model = new AssistantModel();
|
||||
this._plugins = new KernelPluginCollection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="IAssistant"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A new <see cref="IAssistant"/> instance.</returns>
|
||||
public async Task<IAssistant> BuildAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(this._model.Model))
|
||||
{
|
||||
throw new AssistantException("Model must be defined for assistant.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(this._apiKey))
|
||||
{
|
||||
throw new AssistantException("ApiKey must be provided for assistant.");
|
||||
}
|
||||
|
||||
return
|
||||
await Assistant.CreateAsync(
|
||||
new OpenAIRestContext(this._apiKey!, this._httpClientProvider),
|
||||
this._model,
|
||||
this._plugins,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Define the OpenAI chat completion service (required).
|
||||
/// </summary>
|
||||
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
|
||||
public AssistantBuilder WithOpenAIChatCompletion(string model, string apiKey)
|
||||
{
|
||||
this._apiKey = apiKey;
|
||||
this._model.Model = model;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new assistant from a yaml formatted string.
|
||||
/// </summary>
|
||||
/// <param name="template">YAML assistant definition.</param>
|
||||
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
|
||||
public AssistantBuilder FromTemplate(string template)
|
||||
{
|
||||
var deserializer = new DeserializerBuilder().Build();
|
||||
|
||||
var assistantKernelModel = deserializer.Deserialize<AssistantConfigurationModel>(template);
|
||||
|
||||
return
|
||||
this
|
||||
.WithInstructions(assistantKernelModel.Instructions.Trim())
|
||||
.WithName(assistantKernelModel.Name.Trim())
|
||||
.WithDescription(assistantKernelModel.Description.Trim());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new assistant from a yaml template.
|
||||
/// </summary>
|
||||
/// <param name="templatePath">Path to a configuration file.</param>
|
||||
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
|
||||
public AssistantBuilder FromTemplatePath(string templatePath)
|
||||
{
|
||||
var yamlContent = File.ReadAllText(templatePath);
|
||||
|
||||
return this.FromTemplate(yamlContent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provide an httpclient (optional).
|
||||
/// </summary>
|
||||
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
|
||||
public AssistantBuilder WithHttpClient(HttpClient httpClient)
|
||||
{
|
||||
this._httpClientProvider ??= () => httpClient;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Define the assistant description (optional).
|
||||
/// </summary>
|
||||
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
|
||||
public AssistantBuilder WithDescription(string? description)
|
||||
{
|
||||
this._model.Description = description;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Define the assistant instructions (optional).
|
||||
/// </summary>
|
||||
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
|
||||
public AssistantBuilder WithInstructions(string instructions)
|
||||
{
|
||||
this._model.Instructions = instructions;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Define the assistant metadata (optional).
|
||||
/// </summary>
|
||||
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
|
||||
public AssistantBuilder WithMetadata(string key, object value)
|
||||
{
|
||||
this._model.Metadata[key] = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Define the assistant metadata (optional).
|
||||
/// </summary>
|
||||
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
|
||||
public AssistantBuilder WithMetadata(IDictionary<string, object> metadata)
|
||||
{
|
||||
foreach (var kvp in metadata)
|
||||
{
|
||||
this._model.Metadata[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Define the assistant name (optional).
|
||||
/// </summary>
|
||||
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
|
||||
public AssistantBuilder WithName(string? name)
|
||||
{
|
||||
this._model.Name = name;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Define functions associated with assistant instance (optional).
|
||||
/// </summary>
|
||||
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
|
||||
public AssistantBuilder WithPlugin(KernelPlugin? plugin)
|
||||
{
|
||||
if (plugin != null)
|
||||
{
|
||||
this._plugins.Add(plugin);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Define functions associated with assistant instance (optional).
|
||||
/// </summary>
|
||||
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
|
||||
public AssistantBuilder WithPlugins(IEnumerable<KernelPlugin> plugins)
|
||||
{
|
||||
this._plugins.AddRange(plugins);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Specialization of <see cref="KernelPlugin"/> for <see cref="IAssistant"/>
|
||||
/// </summary>
|
||||
public abstract class AssistantPlugin : KernelPlugin
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected AssistantPlugin(string name, string? description = null)
|
||||
: base(name, description)
|
||||
{
|
||||
// No specialization...
|
||||
}
|
||||
|
||||
internal abstract Assistant Assistant { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoke plugin with user input
|
||||
/// </summary>
|
||||
/// <param name="input">The user input</param>
|
||||
/// <param name="cancellationToken">A cancel token</param>
|
||||
/// <returns>The assistant response</returns>
|
||||
public async Task<string> InvokeAsync(string input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var args = new KernelArguments { { "input", input } };
|
||||
var result = await this.First().InvokeAsync(this.Assistant.Kernel, args, cancellationToken).ConfigureAwait(false);
|
||||
var response = result.GetValue<AssistantResponse>()!;
|
||||
|
||||
return response.Message;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Response from assistant when called as a <see cref="KernelFunction"/>.
|
||||
/// </summary>
|
||||
public class AssistantResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The thread-id for the assistant conversation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("thread_id")]
|
||||
public string ThreadId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The assistant response.
|
||||
/// </summary>
|
||||
[JsonPropertyName("response")]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Instructions from assistant on next steps.
|
||||
/// </summary>
|
||||
[JsonPropertyName("system_instructions")]
|
||||
public string Instructions { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Assistant specific <see cref="KernelException"/>.
|
||||
/// </summary>
|
||||
public class AssistantException : KernelException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AssistantException"/> class.
|
||||
/// </summary>
|
||||
public AssistantException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AssistantException"/> class with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
public AssistantException(string? message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AssistantException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
|
||||
public AssistantException(string? message, Exception? innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<!-- THIS PROPERTY GROUP MUST COME FIRST -->
|
||||
<AssemblyName>Microsoft.SemanticKernel.Experimental.Assistants</AssemblyName>
|
||||
<RootNamespace>Microsoft.SemanticKernel.Experimental.Assistants</RootNamespace>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
<LangVersion>Latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
<Import Project="$(RepoRoot)/dotnet/src/InternalUtilities/src/InternalUtilities.props" />
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Semantic Kernel Assistants</Title>
|
||||
<Description>Semantic Kernel Assistants</Description>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SemanticKernel.Experimental.Assistants.UnitTests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Linq.Async" />
|
||||
<PackageReference Include="YamlDotNet" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj" />
|
||||
<ProjectReference Include="..\..\SemanticKernel.Core\SemanticKernel.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SemanticKernel.Experimental.Assistants.UnitTests" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
|
||||
|
||||
internal static class AssistantsKernelExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve a kernel function based on the tool name.
|
||||
/// </summary>
|
||||
public static KernelFunction GetAssistantTool(this Kernel kernel, string toolName)
|
||||
{
|
||||
string[] nameParts = toolName.Split('-');
|
||||
return nameParts.Length switch
|
||||
{
|
||||
2 => kernel.Plugins.GetFunction(nameParts[0], nameParts[1]),
|
||||
_ => throw new AssistantException($"Unknown tool: {toolName}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Json.More;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
internal static class AssistantsKernelFunctionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Produce a fully qualified toolname.
|
||||
/// </summary>
|
||||
public static string GetQualifiedName(this KernelFunction function, string pluginName)
|
||||
{
|
||||
return $"{pluginName}-{function.Name}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert <see cref="KernelFunction"/> to an OpenAI tool model.
|
||||
/// </summary>
|
||||
/// <param name="function">The source function</param>
|
||||
/// <param name="pluginName">The plugin name</param>
|
||||
/// <returns>An OpenAI tool model</returns>
|
||||
public static ToolModel ToToolModel(this KernelFunction function, string pluginName)
|
||||
{
|
||||
var metadata = function.Metadata;
|
||||
var required = new List<string>(metadata.Parameters.Count);
|
||||
var properties =
|
||||
metadata.Parameters.ToDictionary(
|
||||
p => p.Name,
|
||||
p =>
|
||||
{
|
||||
if (p.IsRequired)
|
||||
{
|
||||
required.Add(p.Name);
|
||||
}
|
||||
|
||||
return
|
||||
new OpenAIParameter
|
||||
{
|
||||
Type = ConvertType(p.ParameterType),
|
||||
Description = p.Description,
|
||||
};
|
||||
});
|
||||
|
||||
var payload =
|
||||
new ToolModel
|
||||
{
|
||||
Type = "function",
|
||||
Function =
|
||||
new()
|
||||
{
|
||||
Name = function.GetQualifiedName(pluginName),
|
||||
Description = function.Description,
|
||||
Parameters =
|
||||
new OpenAIParameters
|
||||
{
|
||||
Properties = properties,
|
||||
Required = required,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
private static string ConvertType(Type? type)
|
||||
{
|
||||
if (type == null || type == typeof(string))
|
||||
{
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (type.IsNumber())
|
||||
{
|
||||
return "number";
|
||||
}
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
return "enum";
|
||||
}
|
||||
|
||||
return type.Name;
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Supported OpenAI REST API actions for assistants.
|
||||
/// </summary>
|
||||
internal static partial class OpenAIRestExtensions
|
||||
{
|
||||
internal const string BaseAssistantUrl = $"{BaseUrl}/assistants";
|
||||
|
||||
/// <summary>
|
||||
/// Create a new assistant.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="model">The assistant definition</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>An assistant definition</returns>
|
||||
public static Task<AssistantModel> CreateAssistantModelAsync(
|
||||
this OpenAIRestContext context,
|
||||
AssistantModel model,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var payload =
|
||||
new
|
||||
{
|
||||
model = model.Model,
|
||||
name = model.Name,
|
||||
description = model.Description,
|
||||
instructions = model.Instructions,
|
||||
tools = model.Tools,
|
||||
file_ids = model.FileIds,
|
||||
metadata = model.Metadata,
|
||||
};
|
||||
|
||||
return
|
||||
context.ExecutePostAsync<AssistantModel>(
|
||||
BaseAssistantUrl,
|
||||
payload,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve an assistant by identifier.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="assistantId">The assistant identifier</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>An assistant definition</returns>
|
||||
public static Task<AssistantModel> GetAssistantModelAsync(
|
||||
this OpenAIRestContext context,
|
||||
string assistantId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return
|
||||
context.ExecuteGetAsync<AssistantModel>(
|
||||
GetAssistantUrl(assistantId),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all assistants.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="limit">A limit on the number of objects to be returned.
|
||||
/// Limit can range between 1 and 100, and the default is 20.</param>
|
||||
/// <param name="ascending">Set to true to sort by ascending created_at timestamp
|
||||
/// instead of descending.</param>
|
||||
/// <param name="after">A cursor for use in pagination. This is an object ID that defines
|
||||
/// your place in the list. For instance, if you make a list request and receive 100 objects,
|
||||
/// ending with obj_foo, your subsequent call can include after=obj_foo in order to
|
||||
/// fetch the next page of the list.</param>
|
||||
/// <param name="before">A cursor for use in pagination. This is an object ID that defines
|
||||
/// your place in the list. For instance, if you make a list request and receive 100 objects,
|
||||
/// ending with obj_foo, your subsequent call can include before=obj_foo in order to
|
||||
/// fetch the previous page of the list.</param>
|
||||
/// <returns>List of retrieved Assistants</returns>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>An enumeration of assistant definitions</returns>
|
||||
public static async Task<IList<AssistantModel>> ListAssistantModelsAsync(
|
||||
this OpenAIRestContext context,
|
||||
int limit = 20,
|
||||
bool ascending = false,
|
||||
string? after = null,
|
||||
string? before = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = HttpUtility.ParseQueryString(string.Empty);
|
||||
query["limit"] = limit.ToString(CultureInfo.InvariantCulture);
|
||||
query["order"] = ascending ? "asc" : "desc";
|
||||
if (!string.IsNullOrWhiteSpace(after))
|
||||
{
|
||||
query["after"] = after;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(before))
|
||||
{
|
||||
query["before"] = before;
|
||||
}
|
||||
|
||||
string requestUrl = string.Join("?", BaseAssistantUrl, query.ToString());
|
||||
|
||||
var result =
|
||||
await context.ExecuteGetAsync<AssistantListModel>(
|
||||
requestUrl,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return result.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete an existing assistant
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="id">Identifier of assistant to delete</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
public static Task DeleteAssistantModelAsync(
|
||||
this OpenAIRestContext context,
|
||||
string id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.ExecuteDeleteAsync(GetAssistantUrl(id), cancellationToken);
|
||||
}
|
||||
|
||||
internal static string GetAssistantUrl(string assistantId)
|
||||
{
|
||||
return $"{BaseAssistantUrl}/{assistantId}";
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Supported OpenAI REST API actions for thread messages.
|
||||
/// </summary>
|
||||
internal static partial class OpenAIRestExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new message.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="threadId">The thread identifier</param>
|
||||
/// <param name="content">The message text</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A message definition</returns>
|
||||
public static Task<ThreadMessageModel> CreateUserTextMessageAsync(
|
||||
this OpenAIRestContext context,
|
||||
string threadId,
|
||||
string content,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var payload =
|
||||
new
|
||||
{
|
||||
role = AuthorRole.User.Label,
|
||||
content,
|
||||
};
|
||||
|
||||
return
|
||||
context.ExecutePostAsync<ThreadMessageModel>(
|
||||
GetMessagesUrl(threadId),
|
||||
payload,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve an message by identifier.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="threadId">The thread identifier</param>
|
||||
/// <param name="messageId">The message identifier</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A message definition</returns>
|
||||
public static Task<ThreadMessageModel> GetMessageAsync(
|
||||
this OpenAIRestContext context,
|
||||
string threadId,
|
||||
string messageId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return
|
||||
context.ExecuteGetAsync<ThreadMessageModel>(
|
||||
GetMessagesUrl(threadId, messageId),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all thread messages.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="threadId">The thread identifier</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A message list definition</returns>
|
||||
public static Task<ThreadMessageListModel> GetMessagesAsync(
|
||||
this OpenAIRestContext context,
|
||||
string threadId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return
|
||||
context.ExecuteGetAsync<ThreadMessageListModel>(
|
||||
GetMessagesUrl(threadId),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all thread messages.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="threadId">The thread identifier</param>
|
||||
/// <param name="messageIds">The set of message identifiers to retrieve</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A message list definition</returns>
|
||||
public static async Task<IEnumerable<ThreadMessageModel>> GetMessagesAsync(
|
||||
this OpenAIRestContext context,
|
||||
string threadId,
|
||||
IEnumerable<string> messageIds,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tasks =
|
||||
messageIds.Select(
|
||||
id =>
|
||||
context.ExecuteGetAsync<ThreadMessageModel>(
|
||||
GetMessagesUrl(threadId, id),
|
||||
cancellationToken)).ToArray();
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
return tasks.Select(t => t.Result).ToArray();
|
||||
}
|
||||
|
||||
internal static string GetMessagesUrl(string threadId)
|
||||
{
|
||||
return $"{BaseThreadUrl}/{threadId}/messages";
|
||||
}
|
||||
|
||||
internal static string GetMessagesUrl(string threadId, string messageId)
|
||||
{
|
||||
return $"{BaseThreadUrl}/{threadId}/messages/{messageId}";
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Supported OpenAI REST API actions for thread runs.
|
||||
/// </summary>
|
||||
internal static partial class OpenAIRestExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new run.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="threadId">A thread identifier</param>
|
||||
/// <param name="assistantId">The assistant identifier</param>
|
||||
/// <param name="instructions">Optional instruction override</param>
|
||||
/// <param name="tools">The assistant tools</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A run definition</returns>
|
||||
public static Task<ThreadRunModel> CreateRunAsync(
|
||||
this OpenAIRestContext context,
|
||||
string threadId,
|
||||
string assistantId,
|
||||
string? instructions = null,
|
||||
IEnumerable<ToolModel>? tools = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var payload =
|
||||
new
|
||||
{
|
||||
assistant_id = assistantId,
|
||||
instructions,
|
||||
tools,
|
||||
};
|
||||
|
||||
return
|
||||
context.ExecutePostAsync<ThreadRunModel>(
|
||||
GetRunUrl(threadId),
|
||||
payload,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve an run by identifier.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="threadId">A thread identifier</param>
|
||||
/// <param name="runId">A run identifier</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A run definition</returns>
|
||||
public static Task<ThreadRunModel> GetRunAsync(
|
||||
this OpenAIRestContext context,
|
||||
string threadId,
|
||||
string runId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return
|
||||
context.ExecuteGetAsync<ThreadRunModel>(
|
||||
GetRunUrl(threadId, runId),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve run steps by identifier.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="threadId">A thread identifier</param>
|
||||
/// <param name="runId">A run identifier</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A set of run steps</returns>
|
||||
public static Task<ThreadRunStepListModel> GetRunStepsAsync(
|
||||
this OpenAIRestContext context,
|
||||
string threadId,
|
||||
string runId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return
|
||||
context.ExecuteGetAsync<ThreadRunStepListModel>(
|
||||
GetRunStepsUrl(threadId, runId),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a function result for a run.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="threadId">A thread identifier</param>
|
||||
/// <param name="runId">The run identifier</param>
|
||||
/// <param name="results">The function/tool results.</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A run definition</returns>
|
||||
public static Task<ThreadRunModel> AddToolOutputsAsync(
|
||||
this OpenAIRestContext context,
|
||||
string threadId,
|
||||
string runId,
|
||||
IEnumerable<ToolResultModel> results,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var payload =
|
||||
new
|
||||
{
|
||||
tool_outputs = results
|
||||
};
|
||||
|
||||
return
|
||||
context.ExecutePostAsync<ThreadRunModel>(
|
||||
GetRunToolOutput(threadId, runId),
|
||||
payload,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
internal static string GetRunUrl(string threadId)
|
||||
{
|
||||
return $"{BaseThreadUrl}/{threadId}/runs";
|
||||
}
|
||||
|
||||
internal static string GetRunUrl(string threadId, string runId)
|
||||
{
|
||||
return $"{BaseThreadUrl}/{threadId}/runs/{runId}";
|
||||
}
|
||||
|
||||
internal static string GetRunStepsUrl(string threadId, string runId)
|
||||
{
|
||||
return $"{BaseThreadUrl}/{threadId}/runs/{runId}/steps";
|
||||
}
|
||||
|
||||
internal static string GetRunToolOutput(string threadId, string runId)
|
||||
{
|
||||
return $"{BaseThreadUrl}/{threadId}/runs/{runId}/submit_tool_outputs";
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Supported OpenAI REST API actions for threads.
|
||||
/// </summary>
|
||||
internal static partial class OpenAIRestExtensions
|
||||
{
|
||||
internal const string BaseThreadUrl = $"{BaseUrl}/threads";
|
||||
|
||||
/// <summary>
|
||||
/// Create a new thread.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A thread definition</returns>
|
||||
public static Task<ThreadModel> CreateThreadModelAsync(
|
||||
this OpenAIRestContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return
|
||||
context.ExecutePostAsync<ThreadModel>(
|
||||
BaseThreadUrl,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve an thread by identifier.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="threadId">The thread identifier</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A thread definition</returns>
|
||||
public static Task<ThreadModel> GetThreadModelAsync(
|
||||
this OpenAIRestContext context,
|
||||
string threadId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return
|
||||
context.ExecuteGetAsync<ThreadModel>(
|
||||
GetThreadUrl(threadId),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete an existing thread.
|
||||
/// </summary>
|
||||
/// <param name="context">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="id">Identifier of thread to delete</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
public static Task DeleteThreadModelAsync(
|
||||
this OpenAIRestContext context,
|
||||
string id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.ExecuteDeleteAsync(GetThreadUrl(id), cancellationToken);
|
||||
}
|
||||
|
||||
internal static string GetThreadUrl(string threadId)
|
||||
{
|
||||
return $"{BaseThreadUrl}/{threadId}";
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
internal static partial class OpenAIRestExtensions
|
||||
{
|
||||
private const string BaseUrl = "https://api.openai.com/v1";
|
||||
private const string HeaderNameOpenAIAssistant = "OpenAI-Beta";
|
||||
private const string HeaderNameAuthorization = "Authorization";
|
||||
private const string HeaderOpenAIValueAssistant = "assistants=v1";
|
||||
|
||||
private static async Task<TResult> ExecuteGetAsync<TResult>(
|
||||
this OpenAIRestContext context,
|
||||
string url,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var request = HttpRequest.CreateGetRequest(url);
|
||||
|
||||
request.Headers.Add(HeaderNameAuthorization, $"Bearer {context.ApiKey}");
|
||||
request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant);
|
||||
|
||||
using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new AssistantException($"Unexpected failure: {response.StatusCode} [{url}]");
|
||||
}
|
||||
|
||||
string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
|
||||
// Common case is for failure exception to be raised by REST invocation.
|
||||
// Null result is a logical possibility, but unlikely edge case.
|
||||
// Might occur due to model alignment issues over time.
|
||||
return
|
||||
JsonSerializer.Deserialize<TResult>(responseBody) ??
|
||||
throw new AssistantException($"Null result processing: {typeof(TResult).Name}");
|
||||
}
|
||||
|
||||
private static Task<TResult> ExecutePostAsync<TResult>(
|
||||
this OpenAIRestContext context,
|
||||
string url,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.ExecutePostAsync<TResult>(url, payload: null, cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<TResult> ExecutePostAsync<TResult>(
|
||||
this OpenAIRestContext context,
|
||||
string url,
|
||||
object? payload,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var request = HttpRequest.CreatePostRequest(url, payload);
|
||||
|
||||
request.Headers.Add(HeaderNameAuthorization, $"Bearer {context.ApiKey}");
|
||||
request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant);
|
||||
|
||||
using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new AssistantException($"Unexpected failure: {response.StatusCode} [{url}]");
|
||||
}
|
||||
|
||||
string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
return
|
||||
JsonSerializer.Deserialize<TResult>(responseBody) ??
|
||||
throw new AssistantException($"Null result processing: {typeof(TResult).Name}");
|
||||
}
|
||||
|
||||
private static async Task ExecuteDeleteAsync(
|
||||
this OpenAIRestContext context,
|
||||
string url,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var request = HttpRequest.CreateDeleteRequest(url);
|
||||
|
||||
request.Headers.Add(HeaderNameAuthorization, $"Bearer {context.ApiKey}");
|
||||
request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant);
|
||||
|
||||
using var response = await context.GetHttpClient().SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new AssistantException($"Unexpected failure: {response.StatusCode} [{url}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an assistant that can call the model and use tools.
|
||||
/// </summary>
|
||||
public interface IAssistant
|
||||
{
|
||||
/// <summary>
|
||||
/// The assistant identifier (which can be referenced in API endpoints).
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Always "assistant"
|
||||
/// </summary>
|
||||
#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema
|
||||
#pragma warning disable CA1716 // Identifiers should not match keywords
|
||||
string Object { get; }
|
||||
#pragma warning restore CA1716 // Identifiers should not match keywords
|
||||
#pragma warning restore CA1720 // Identifier contains type name
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the assistant was created
|
||||
/// </summary>
|
||||
long CreatedAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the assistant
|
||||
/// </summary>
|
||||
string? Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The description of the assistant
|
||||
/// </summary>
|
||||
string? Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ID of the model to use
|
||||
/// </summary>
|
||||
string Model { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The system instructions that the assistant uses
|
||||
/// </summary>
|
||||
string Instructions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A semantic-kernel <see cref="Kernel"/> instance associated with the assistant.
|
||||
/// </summary>
|
||||
internal Kernel Kernel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tools defined for run execution.
|
||||
/// </summary>
|
||||
public KernelPluginCollection Plugins { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Expose the assistant as a plugin.
|
||||
/// </summary>
|
||||
public AssistantPlugin AsPlugin();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new assistant chat thread.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
Task<IChatThread> NewThreadAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an existing assistant chat thread.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the existing chat thread.</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
Task<IChatThread> GetThreadAsync(string id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an existing assistant chat thread.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the existing chat thread. Allows for null-fallthrough to simplify caller patterns.</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
Task DeleteThreadAsync(string? id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Delete current assistant. Terminal state - Unable to perform any
|
||||
/// subsequent actions.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
Task DeleteAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience actions for <see cref="IAssistant"/>.
|
||||
/// </summary>
|
||||
public static class IAssistantExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoke assistant with user input
|
||||
/// </summary>
|
||||
/// <param name="assistant">the assistant</param>
|
||||
/// <param name="input">the user input</param>
|
||||
/// <param name="cancellationToken">a cancel token</param>
|
||||
/// <returns>chat messages</returns>
|
||||
public static async IAsyncEnumerable<IChatMessage> InvokeAsync(
|
||||
this IAssistant assistant,
|
||||
string input,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
IChatThread thread = await assistant.NewThreadAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await foreach (var message in thread.InvokeAsync(assistant, input, cancellationToken))
|
||||
{
|
||||
yield return message;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await thread.DeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a message that is part of an assistant thread.
|
||||
/// </summary>
|
||||
public interface IChatMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The message identifier (which can be referenced in API endpoints).
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The id of the assistant associated with the a message where role = "assistant", otherwise null.
|
||||
/// </summary>
|
||||
string? AssistantId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The chat message content.
|
||||
/// </summary>
|
||||
string Content { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The role associated with the chat message.
|
||||
/// </summary>
|
||||
string Role { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Properties associated with the message.
|
||||
/// </summary>
|
||||
ReadOnlyDictionary<string, object> Properties { get; }
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a thread that contains messages.
|
||||
/// </summary>
|
||||
public interface IChatThread
|
||||
{
|
||||
/// <summary>
|
||||
/// The thread identifier (which can be referenced in API endpoints).
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Add a textual user message to the thread.
|
||||
/// </summary>
|
||||
/// <param name="message">The user message</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<IChatMessage> AddUserMessageAsync(string message, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Advance the thread with the specified assistant.
|
||||
/// </summary>
|
||||
/// <param name="assistant">An assistant instance.</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>The resulting assistant message(s)</returns>
|
||||
IAsyncEnumerable<IChatMessage> InvokeAsync(IAssistant assistant, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Advance the thread with the specified assistant.
|
||||
/// </summary>
|
||||
/// <param name="assistant">An assistant instance.</param>
|
||||
/// <param name="userMessage">The user message</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>The resulting assistant message(s)</returns>
|
||||
IAsyncEnumerable<IChatMessage> InvokeAsync(IAssistant assistant, string userMessage, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Delete current thread. Terminal state - Unable to perform any
|
||||
/// subsequent actions.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
Task DeleteAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an assistant that can call the model and use tools.
|
||||
/// </summary>
|
||||
internal sealed class Assistant : IAssistant
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Id => this._model.Id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Kernel Kernel { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public KernelPluginCollection Plugins => this.Kernel.Plugins;
|
||||
|
||||
/// <inheritdoc/>
|
||||
#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema
|
||||
#pragma warning disable CA1716 // Identifiers should not match keywords
|
||||
public string Object => this._model.Object;
|
||||
#pragma warning restore CA1720 // Identifier contains type name - We don't control the schema
|
||||
#pragma warning restore CA1716 // Identifiers should not match keywords
|
||||
|
||||
/// <inheritdoc/>
|
||||
public long CreatedAt => this._model.CreatedAt;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? Name => this._model.Name;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? Description => this._model.Description;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Model => this._model.Model;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Instructions => this._model.Instructions;
|
||||
|
||||
private static readonly Regex s_removeInvalidCharsRegex = new("[^0-9A-Za-z-]");
|
||||
|
||||
private readonly OpenAIRestContext _restContext;
|
||||
private readonly AssistantModel _model;
|
||||
|
||||
private AssistantPlugin? _assistantPlugin;
|
||||
private bool _isDeleted;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new assistant.
|
||||
/// </summary>
|
||||
/// <param name="restContext">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="assistantModel">The assistant definition</param>
|
||||
/// <param name="plugins">Plugins to initialize as assistant tools</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>An initialized <see cref="Assistant"> instance.</see></returns>
|
||||
public static async Task<IAssistant> CreateAsync(
|
||||
OpenAIRestContext restContext,
|
||||
AssistantModel assistantModel,
|
||||
IEnumerable<KernelPlugin>? plugins = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var resultModel = await restContext.CreateAssistantModelAsync(assistantModel, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new Assistant(resultModel, restContext, plugins);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Assistant"/> class.
|
||||
/// </summary>
|
||||
internal Assistant(
|
||||
AssistantModel model,
|
||||
OpenAIRestContext restContext,
|
||||
IEnumerable<KernelPlugin>? plugins = null)
|
||||
{
|
||||
this._model = model;
|
||||
this._restContext = restContext;
|
||||
|
||||
IKernelBuilder builder = Kernel.CreateBuilder();
|
||||
;
|
||||
this.Kernel =
|
||||
Kernel
|
||||
.CreateBuilder()
|
||||
.AddOpenAIChatCompletion(this._model.Model, this._restContext.ApiKey)
|
||||
.Build();
|
||||
|
||||
if (plugins is not null)
|
||||
{
|
||||
this.Kernel.Plugins.AddRange(plugins);
|
||||
}
|
||||
}
|
||||
|
||||
public AssistantPlugin AsPlugin() => this._assistantPlugin ??= this.DefinePlugin();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IChatThread> NewThreadAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.ThrowIfDeleted();
|
||||
|
||||
return ChatThread.CreateAsync(this._restContext, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IChatThread> GetThreadAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.ThrowIfDeleted();
|
||||
|
||||
return ChatThread.GetAsync(this._restContext, id, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task DeleteThreadAsync(string? id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await this._restContext.DeleteThreadModelAsync(id!, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task DeleteAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._isDeleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await this._restContext.DeleteAssistantModelAsync(this.Id, cancellationToken).ConfigureAwait(false);
|
||||
this._isDeleted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marshal thread run through <see cref="KernelFunction"/> interface.
|
||||
/// </summary>
|
||||
/// <param name="input">The user input</param>
|
||||
/// <param name="cancellationToken">A cancellation token.</param>
|
||||
/// <returns>An assistant response (<see cref="AssistantResponse"/></returns>
|
||||
private async Task<AssistantResponse> AskAsync(
|
||||
[Description("The user message provided to the assistant.")]
|
||||
string input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var thread = await this.NewThreadAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await thread.AddUserMessageAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var messages = await thread.InvokeAsync(this, cancellationToken).ToArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||
var response =
|
||||
new AssistantResponse
|
||||
{
|
||||
ThreadId = thread.Id,
|
||||
Message = string.Concat(messages.Select(m => m.Content)),
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await thread.DeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private AssistantPluginImpl DefinePlugin()
|
||||
{
|
||||
var functionAsk = KernelFunctionFactory.CreateFromMethod(this.AskAsync, description: this.Description);
|
||||
|
||||
return new AssistantPluginImpl(this, functionAsk);
|
||||
}
|
||||
|
||||
private void ThrowIfDeleted()
|
||||
{
|
||||
if (this._isDeleted)
|
||||
{
|
||||
throw new AssistantException($"{nameof(Assistant)}: {this.Id} has been deleted.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class AssistantPluginImpl : AssistantPlugin
|
||||
{
|
||||
public KernelFunction FunctionAsk { get; }
|
||||
|
||||
internal override Assistant Assistant { get; }
|
||||
|
||||
public override int FunctionCount => 1;
|
||||
|
||||
private static readonly string s_functionName = nameof(Assistant.AskAsync).Substring(0, nameof(Assistant.AskAsync).Length - 5);
|
||||
|
||||
public AssistantPluginImpl(Assistant assistant, KernelFunction functionAsk)
|
||||
: base(s_removeInvalidCharsRegex.Replace(assistant.Name ?? assistant.Id, string.Empty),
|
||||
assistant.Description ?? assistant.Instructions)
|
||||
{
|
||||
this.Assistant = assistant;
|
||||
this.FunctionAsk = functionAsk;
|
||||
}
|
||||
|
||||
public override IEnumerator<KernelFunction> GetEnumerator()
|
||||
{
|
||||
yield return this.FunctionAsk;
|
||||
}
|
||||
|
||||
public override bool TryGetFunction(string name, [NotNullWhen(true)] out KernelFunction? function)
|
||||
{
|
||||
function = null;
|
||||
|
||||
if (s_functionName.Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
function = this.FunctionAsk;
|
||||
}
|
||||
|
||||
return function != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a message that is part of an assistant thread.
|
||||
/// </summary>
|
||||
internal sealed class ChatMessage : IChatMessage
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Id { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? AssistantId { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Content { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Role { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ReadOnlyDictionary<string, object> Properties { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatMessage"/> class.
|
||||
/// </summary>
|
||||
internal ChatMessage(ThreadMessageModel model)
|
||||
{
|
||||
var content = (IEnumerable<ThreadMessageModel.ContentModel>)model.Content;
|
||||
var text = content.First().Text?.Value ?? string.Empty;
|
||||
|
||||
this.Id = model.Id;
|
||||
this.AssistantId = string.IsNullOrWhiteSpace(model.AssistantId) ? null : model.AssistantId;
|
||||
this.Role = model.Role;
|
||||
this.Content = text;
|
||||
this.Properties = new ReadOnlyDictionary<string, object>(model.Metadata);
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an execution run on a thread.
|
||||
/// </summary>
|
||||
internal sealed class ChatRun
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Id => this._model.Id;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string AssistantId => this._model.AssistantId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string ThreadId => this._model.ThreadId;
|
||||
|
||||
private const string ActionState = "requires_action";
|
||||
private const string FailedState = "failed";
|
||||
private const string CompletedState = "completed";
|
||||
private static readonly TimeSpan s_pollingInterval = TimeSpan.FromMilliseconds(500);
|
||||
private static readonly TimeSpan s_pollingBackoff = TimeSpan.FromSeconds(1);
|
||||
|
||||
private static readonly HashSet<string> s_pollingStates =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"queued",
|
||||
"in_progress",
|
||||
};
|
||||
|
||||
private readonly OpenAIRestContext _restContext;
|
||||
private readonly Kernel _kernel;
|
||||
|
||||
private ThreadRunModel _model;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IList<string>> GetResultAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Poll until actionable
|
||||
await PollRunStatus().ConfigureAwait(false);
|
||||
|
||||
// Retrieve steps
|
||||
var steps = await this._restContext.GetRunStepsAsync(this.ThreadId, this.Id, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
do
|
||||
{
|
||||
// Is tool action required?
|
||||
if (ActionState.Equals(this._model.Status, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Execute functions in parallel and post results at once.
|
||||
var tasks = steps.Data.SelectMany(step => this.ExecuteStep(step, cancellationToken)).ToArray();
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
var results = tasks.Select(t => t.Result).ToArray();
|
||||
await this._restContext.AddToolOutputsAsync(this.ThreadId, this.Id, results, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Refresh run as it goes back into pending state after posting function results.
|
||||
await PollRunStatus(force: true).ConfigureAwait(false);
|
||||
|
||||
// Refresh steps to retrieve additional messages.
|
||||
steps = await this._restContext.GetRunStepsAsync(this.ThreadId, this.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Did fail?
|
||||
if (FailedState.Equals(this._model.Status, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new AssistantException($"Unexpected failure processing run: {this.Id}: {this._model.LastError?.Message ?? "Unknown"}");
|
||||
}
|
||||
}
|
||||
while (!CompletedState.Equals(this._model.Status, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var messageIds =
|
||||
steps.Data
|
||||
.Where(s => s.StepDetails.MessageCreation != null)
|
||||
.Select(s => s.StepDetails.MessageCreation!.MessageId)
|
||||
.ToArray();
|
||||
|
||||
return messageIds;
|
||||
|
||||
async Task PollRunStatus(bool force = false)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
// Ignore model status when forced.
|
||||
while (force || s_pollingStates.Contains(this._model.Status))
|
||||
{
|
||||
if (!force)
|
||||
{
|
||||
// Reduce polling frequency after a couple attempts
|
||||
await Task.Delay(count >= 2 ? s_pollingInterval : s_pollingBackoff, cancellationToken).ConfigureAwait(false);
|
||||
++count;
|
||||
}
|
||||
|
||||
force = false;
|
||||
|
||||
try
|
||||
{
|
||||
this._model = await this._restContext.GetRunAsync(this.ThreadId, this.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception) when (!exception.IsCriticalException())
|
||||
{
|
||||
// Retry anyway..
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatRun"/> class.
|
||||
/// </summary>
|
||||
internal ChatRun(
|
||||
ThreadRunModel model,
|
||||
Kernel kernel,
|
||||
OpenAIRestContext restContext)
|
||||
{
|
||||
this._model = model;
|
||||
this._kernel = kernel;
|
||||
this._restContext = restContext;
|
||||
}
|
||||
|
||||
private IEnumerable<Task<ToolResultModel>> ExecuteStep(ThreadRunStepModel step, CancellationToken cancellationToken)
|
||||
{
|
||||
// Process all of the steps that require action
|
||||
if (step.Status == "in_progress" && step.StepDetails.Type == "tool_calls")
|
||||
{
|
||||
foreach (var toolCall in step.StepDetails.ToolCalls)
|
||||
{
|
||||
// Run function
|
||||
yield return this.ProcessFunctionStepAsync(toolCall.Id, toolCall.Function, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ToolResultModel> ProcessFunctionStepAsync(string callId, ThreadRunStepModel.FunctionDetailsModel functionDetails, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await InvokeFunctionCallAsync().ConfigureAwait(false);
|
||||
var toolResult = result as string;
|
||||
if (toolResult == null)
|
||||
{
|
||||
toolResult = JsonSerializer.Serialize(result);
|
||||
}
|
||||
|
||||
return
|
||||
new ToolResultModel
|
||||
{
|
||||
CallId = callId,
|
||||
Output = toolResult!,
|
||||
};
|
||||
|
||||
async Task<object> InvokeFunctionCallAsync()
|
||||
{
|
||||
var function = this._kernel.GetAssistantTool(functionDetails.Name);
|
||||
|
||||
var functionArguments = new KernelArguments();
|
||||
if (!string.IsNullOrWhiteSpace(functionDetails.Arguments))
|
||||
{
|
||||
var arguments = JsonSerializer.Deserialize<Dictionary<string, object>>(functionDetails.Arguments)!;
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
functionArguments[argument.Key] = argument.Value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
var result = await function.InvokeAsync(this._kernel, functionArguments, cancellationToken).ConfigureAwait(false);
|
||||
if (result.ValueType == typeof(AssistantResponse))
|
||||
{
|
||||
return result.GetValue<AssistantResponse>()!;
|
||||
}
|
||||
|
||||
return result.GetValue<string>() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Exceptions;
|
||||
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a thread that contains messages.
|
||||
/// </summary>
|
||||
internal sealed class ChatThread : IChatThread
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Id { get; private set; }
|
||||
|
||||
private readonly OpenAIRestContext _restContext;
|
||||
private bool _isDeleted;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new thread.
|
||||
/// </summary>
|
||||
/// <param name="restContext">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>An initialized <see cref="ChatThread"> instance.</see></returns>
|
||||
public static async Task<IChatThread> CreateAsync(OpenAIRestContext restContext, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Common case is for failure exception to be raised by REST invocation. Null result is a logical possibility, but unlikely edge case.
|
||||
var threadModel = await restContext.CreateThreadModelAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new ChatThread(threadModel, messageListModel: null, restContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve an existing thread.
|
||||
/// </summary>
|
||||
/// <param name="restContext">A context for accessing OpenAI REST endpoint</param>
|
||||
/// <param name="threadId">The thread identifier</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>An initialized <see cref="ChatThread"> instance.</see></returns>
|
||||
public static async Task<IChatThread> GetAsync(OpenAIRestContext restContext, string threadId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var threadModel = await restContext.GetThreadModelAsync(threadId, cancellationToken).ConfigureAwait(false);
|
||||
var messageListModel = await restContext.GetMessagesAsync(threadId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new ChatThread(threadModel, messageListModel, restContext);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IChatMessage> AddUserMessageAsync(string message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.ThrowIfDeleted();
|
||||
|
||||
var messageModel =
|
||||
await this._restContext.CreateUserTextMessageAsync(
|
||||
this.Id,
|
||||
message,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new ChatMessage(messageModel);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IAsyncEnumerable<IChatMessage> InvokeAsync(IAssistant assistant, CancellationToken cancellationToken)
|
||||
{
|
||||
return this.InvokeAsync(assistant, string.Empty, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<IChatMessage> InvokeAsync(IAssistant assistant, string userMessage, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
this.ThrowIfDeleted();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(userMessage))
|
||||
{
|
||||
yield return await this.AddUserMessageAsync(userMessage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var tools = assistant.Plugins.SelectMany(p => p.Select(f => f.ToToolModel(p.Name)));
|
||||
var runModel = await this._restContext.CreateRunAsync(this.Id, assistant.Id, assistant.Instructions, tools, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var run = new ChatRun(runModel, assistant.Kernel, this._restContext);
|
||||
var results = await run.GetResultAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var messages = await this._restContext.GetMessagesAsync(this.Id, results, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var message in messages)
|
||||
{
|
||||
yield return new ChatMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete an existing thread.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
public async Task DeleteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (this._isDeleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await this._restContext.DeleteThreadModelAsync(this.Id, cancellationToken).ConfigureAwait(false);
|
||||
this._isDeleted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatThread"/> class.
|
||||
/// </summary>
|
||||
private ChatThread(
|
||||
ThreadModel threadModel,
|
||||
ThreadMessageListModel? messageListModel,
|
||||
OpenAIRestContext restContext)
|
||||
{
|
||||
this.Id = threadModel.Id;
|
||||
this._restContext = restContext;
|
||||
}
|
||||
|
||||
private void ThrowIfDeleted()
|
||||
{
|
||||
if (this._isDeleted)
|
||||
{
|
||||
throw new AssistantException($"{nameof(ChatThread)}: {this.Id} has been deleted.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder context.
|
||||
/// </summary>
|
||||
internal sealed class OpenAIRestContext
|
||||
{
|
||||
private static readonly HttpClient s_defaultOpenAIClient = new();
|
||||
/// <inheritdoc/>
|
||||
public string ApiKey { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public HttpClient GetHttpClient() => this._clientFactory.Invoke();
|
||||
|
||||
private readonly Func<HttpClient> _clientFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OpenAIRestContext"/> class.
|
||||
/// </summary>
|
||||
public OpenAIRestContext(string apiKey, Func<HttpClient>? clientFactory = null)
|
||||
{
|
||||
this._clientFactory = clientFactory ??= () => s_defaultOpenAIClient;
|
||||
|
||||
this.ApiKey = apiKey;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
#pragma warning disable CA1812
|
||||
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a yaml configuration file for an assistant.
|
||||
/// </summary>
|
||||
internal sealed class AssistantConfigurationModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The assistant name
|
||||
/// </summary>
|
||||
[YamlMember(Alias = "name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The assistant description
|
||||
/// </summary>
|
||||
[YamlMember(Alias = "description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The assistant instructions template
|
||||
/// </summary>
|
||||
[YamlMember(Alias = "instructions")]
|
||||
public string Instructions { get; set; } = string.Empty;
|
||||
|
||||
///// <summary>
|
||||
///// The assistant instructions template
|
||||
///// </summary>
|
||||
//[YamlMember(Alias = "template")]
|
||||
//public string Template { get; set; } = string.Empty;
|
||||
|
||||
///// <summary>
|
||||
///// The assistant instruction template format.
|
||||
///// </summary>
|
||||
//[YamlMember(Alias = "template_format")]
|
||||
//public string TemplateFormat { get; set; } = string.Empty;
|
||||
|
||||
///// <summary>
|
||||
///// Describes the input variables for the template.
|
||||
///// </summary>
|
||||
//[YamlMember(Alias = "input_variables")]
|
||||
//public List<VariableViewModel> InputVariables { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// Describes known valid models.
|
||||
///// </summary>
|
||||
//[YamlMember(Alias = "execution_settings")]
|
||||
//public List<ExecutionSettingsModel> ExecutionSettings { get; set; }
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
#pragma warning disable CA1812
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
/// <summary>
|
||||
/// list of run steps belonging to a run.
|
||||
/// </summary>
|
||||
internal sealed class AssistantListModel : OpenAIListModel<AssistantModel>
|
||||
{
|
||||
// No specialization
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Model of Assistant data returned from OpenAI
|
||||
/// </summary>
|
||||
internal sealed record AssistantModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier, which can be referenced in API endpoints
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Always "assistant"
|
||||
/// </summary>
|
||||
[JsonPropertyName("object")]
|
||||
#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema
|
||||
public string Object { get; init; } = "assistant";
|
||||
#pragma warning restore CA1720 // Identifier contains type name
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the assistant was created
|
||||
/// </summary>
|
||||
[JsonPropertyName("created_at")]
|
||||
public long CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the assistant
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The description of the assistant
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID of the model to use
|
||||
/// </summary>
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The system instructions that the assistant uses
|
||||
/// </summary>
|
||||
[JsonPropertyName("instructions")]
|
||||
public string Instructions { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// A list of tool enabled on the assistant
|
||||
/// There can be a maximum of 128 tools per assistant.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tools")]
|
||||
public List<ToolModel> Tools { get; init; } = new List<ToolModel>();
|
||||
|
||||
/// <summary>
|
||||
/// A list of file IDs attached to this assistant.
|
||||
/// There can be a maximum of 20 files attached to the assistant.
|
||||
/// </summary>
|
||||
[JsonPropertyName("file_ids")]
|
||||
public List<string> FileIds { get; init; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Set of 16 key-value pairs that can be attached to an object.
|
||||
/// This can be useful for storing additional information about the
|
||||
/// object in a structured format.
|
||||
/// Keys can be a maximum of 64 characters long and values can be a
|
||||
/// maximum of 512 characters long.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public Dictionary<string, object> Metadata { get; init; } = new Dictionary<string, object>();
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
#pragma warning disable CA1812
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
/// <summary>
|
||||
/// list of run steps belonging to a run.
|
||||
/// </summary>
|
||||
internal abstract class OpenAIListModel<TModel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Always "list"
|
||||
/// </summary>
|
||||
[JsonPropertyName("object")]
|
||||
#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema
|
||||
public string Object { get; set; } = "list";
|
||||
#pragma warning restore CA1720 // Identifier contains type name
|
||||
|
||||
/// <summary>
|
||||
/// List of steps.
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
public List<TModel> Data { get; set; } = new List<TModel>();
|
||||
|
||||
/// <summary>
|
||||
/// The identifier of the first data record.
|
||||
/// </summary>
|
||||
[JsonPropertyName("first_id")]
|
||||
public string FirstId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The identifier of the last data record.
|
||||
/// </summary>
|
||||
[JsonPropertyName("last_id")]
|
||||
public string LastId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates of more pages of data exist.
|
||||
/// </summary>
|
||||
[JsonPropertyName("has_more")]
|
||||
public bool HasMore { get; set; }
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
#pragma warning disable CA1812
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for parameter map.
|
||||
/// </summary>
|
||||
internal sealed class OpenAIParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// Empty parameter set.
|
||||
/// </summary>
|
||||
public static readonly OpenAIParameters Empty = new();
|
||||
|
||||
/// <summary>
|
||||
/// Always "object"
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "object";
|
||||
|
||||
/// <summary>
|
||||
/// Set of parameters.
|
||||
/// </summary>
|
||||
[JsonPropertyName("properties")]
|
||||
public Dictionary<string, OpenAIParameter> Properties { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Set of parameters.
|
||||
/// </summary>
|
||||
[JsonPropertyName("required")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public List<string>? Required { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for parameter definition.
|
||||
/// </summary>
|
||||
internal sealed class OpenAIParameter
|
||||
{
|
||||
/// <summary>
|
||||
/// The parameter type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "object";
|
||||
|
||||
/// <summary>
|
||||
/// The parameter description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
#pragma warning disable CA1812
|
||||
#pragma warning disable CA1852
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
/// <summary>
|
||||
/// list of run steps belonging to a run.
|
||||
/// </summary>
|
||||
internal sealed class ThreadMessageListModel : OpenAIListModel<ThreadMessageModel>
|
||||
{
|
||||
// No specialization
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a message within a thread.
|
||||
/// </summary>
|
||||
internal sealed class ThreadMessageModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier, which can be referenced in API endpoints.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Always "thread.message"
|
||||
/// </summary>
|
||||
[JsonPropertyName("object")]
|
||||
#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema
|
||||
public string Object { get; set; } = "thread.message";
|
||||
#pragma warning restore CA1720 // Identifier contains type name
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the message was created.
|
||||
/// </summary>
|
||||
[JsonPropertyName("created_at")]
|
||||
public long CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The thread ID that this message belongs to.
|
||||
/// </summary>
|
||||
[JsonPropertyName("thread_id")]
|
||||
public string ThreadId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The entity that produced the message. One of "user" or "assistant".
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
public string Role { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The content of the message in array of text and/or images.
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
public List<ContentModel> Content { get; set; } = new List<ContentModel>();
|
||||
|
||||
/// <summary>
|
||||
/// A list of file IDs that the assistant should use.
|
||||
/// </summary>
|
||||
[JsonPropertyName("file_ids")]
|
||||
public List<string> FileIds { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// If applicable, the ID of the assistant that authored this message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("assistant_id")]
|
||||
public string AssistantId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// If applicable, the ID of the run associated with the authoring of this message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("run_id")]
|
||||
public string RunId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Set of 16 key-value pairs that can be attached to an object.
|
||||
/// This can be useful for storing additional information about the
|
||||
/// object in a structured format. Keys can be a maximum of 64
|
||||
/// characters long and values can be a maximum of 512 characters long.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public Dictionary<string, object> Metadata { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Representa contents within a message.
|
||||
/// </summary>
|
||||
public sealed class ContentModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of content.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Text context.
|
||||
/// </summary>
|
||||
[JsonPropertyName("text")]
|
||||
public TextContentModel? Text { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text content.
|
||||
/// </summary>
|
||||
public sealed class TextContentModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The text itself.
|
||||
/// </summary>
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Any annotations on the text.
|
||||
/// </summary>
|
||||
[JsonPropertyName("annotations")]
|
||||
public List<object> Annotations { get; set; } = new List<object>();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
#pragma warning disable CA1812
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Model of Thread data returned from OpenAI
|
||||
/// </summary>
|
||||
internal sealed class ThreadModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier, which can be referenced in API endpoints.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Always "thread"
|
||||
/// </summary>
|
||||
[JsonPropertyName("object")]
|
||||
#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema
|
||||
public string Object { get; set; } = "thread";
|
||||
#pragma warning restore CA1720 // Identifier contains type name
|
||||
|
||||
/// <summary>
|
||||
/// The Unix timestamp (in seconds) for when the thread was created.
|
||||
/// </summary>
|
||||
[JsonPropertyName("created_at")]
|
||||
public int CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set of 16 key-value pairs that can be attached to an object.
|
||||
/// This can be useful for storing additional information about the
|
||||
/// object in a structured format. Keys can be a maximum of 64
|
||||
/// characters long and values can be a maximum of 512 characters long.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public Dictionary<string, object> Metadata { get; set; } = new Dictionary<string, object>();
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
#pragma warning disable CA1812
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an execution run on a thread.
|
||||
/// </summary>
|
||||
internal sealed class ThreadRunModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier, which can be referenced in API endpoints.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Always "thread.run"
|
||||
/// </summary>
|
||||
[JsonPropertyName("object")]
|
||||
#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema
|
||||
public string Object { get; set; } = "thread.run";
|
||||
#pragma warning restore CA1720 // Identifier contains type name
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the run was created.
|
||||
/// </summary>
|
||||
[JsonPropertyName("created_at")]
|
||||
public long CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID of the assistant used for execution of this run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("assistant_id")]
|
||||
public string AssistantId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// ID of the thread that was executed on as a part of this run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("thread_id")]
|
||||
public string ThreadId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The status of the run, which can be one of:
|
||||
/// queued, in_progress, requires_action, cancelling, cancelled, failed, completed, or expired.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the run was started.
|
||||
/// </summary>
|
||||
[JsonPropertyName("started_at")]
|
||||
public long? StartedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the run will expire.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expires_at")]
|
||||
public long? ExpiresAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the run was cancelled.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cancelled_at")]
|
||||
public long? CancelledAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the run failed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("failed_at")]
|
||||
public long? FailedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the run was completed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("completed_at")]
|
||||
public long? CompletedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The last error associated with this run. Will be null if there are no errors.
|
||||
/// </summary>
|
||||
[JsonPropertyName("last_error")]
|
||||
public ErrorModel? LastError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The model that the assistant used for this run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The instructions that the assistant used for this run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("instructions")]
|
||||
public string Instructions { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The list of tools that the assistant used for this run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tools")]
|
||||
public List<ToolModel> Tools { get; set; } = new List<ToolModel>();
|
||||
|
||||
/// <summary>
|
||||
/// The list of File IDs the assistant used for this run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("file_ids")]
|
||||
public List<string> FileIds { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Set of 16 key-value pairs that can be attached to an object.
|
||||
/// This can be useful for storing additional information about the
|
||||
/// object in a structured format. Keys can be a maximum of 64
|
||||
/// characters long and values can be a maximum of 512 characters long.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public Dictionary<string, object> Metadata { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Run error information.
|
||||
/// </summary>
|
||||
public sealed class ErrorModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Error code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("code")]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Error message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
#pragma warning disable CA1812
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
/// <summary>
|
||||
/// list of run steps belonging to a run.
|
||||
/// </summary>
|
||||
internal sealed class ThreadRunStepListModel : OpenAIListModel<ThreadRunStepModel>
|
||||
{
|
||||
// No specialization
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Step in a run on a thread.
|
||||
/// </summary>
|
||||
internal sealed class ThreadRunStepModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier of the run step, which can be referenced in API endpoints.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Always "thread.run.step"
|
||||
/// </summary>
|
||||
[JsonPropertyName("object")]
|
||||
#pragma warning disable CA1720 // Identifier contains type name - We don't control the schema
|
||||
public string Object { get; set; } = "thread.run.step";
|
||||
#pragma warning restore CA1720 // Identifier contains type name
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the run step was created.
|
||||
/// </summary>
|
||||
[JsonPropertyName("created_at")]
|
||||
public long CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the run to which the run step belongs.
|
||||
/// </summary>
|
||||
[JsonPropertyName("run_id")]
|
||||
public string RunId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// ID of the assistant associated with the run step.
|
||||
/// </summary>
|
||||
[JsonPropertyName("assistant_id")]
|
||||
public string AssistantId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the thread to which the run and run step belongs.
|
||||
/// </summary>
|
||||
[JsonPropertyName("thread_id")]
|
||||
public string ThreadId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The type of run step, which can be either message_creation or tool_calls.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The status of the run step, which can be one of:
|
||||
/// in_progress, cancelled, failed, completed, or expired.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the run step was cancelled.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cancelled_at")]
|
||||
public long? CancelledAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the run step completed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("completed_at")]
|
||||
public long? CompletedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the run step expired.
|
||||
/// A step is considered expired if the parent run is expired.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expired_at")]
|
||||
public long? ExpiredAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unix timestamp (in seconds) for when the run step failed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("failed_at")]
|
||||
public long? FailedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The last error associated with this run step. Will be null if there are no errors.
|
||||
/// </summary>
|
||||
[JsonPropertyName("last_error")]
|
||||
public string LastError { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The details of the run step.
|
||||
/// </summary>
|
||||
[JsonPropertyName("step_details")]
|
||||
public StepDetailsModel StepDetails { get; set; } = StepDetailsModel.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Details of a run step.
|
||||
/// </summary>
|
||||
public sealed class StepDetailsModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Empty definition
|
||||
/// </summary>
|
||||
public static StepDetailsModel Empty = new();
|
||||
|
||||
/// <summary>
|
||||
/// Type of detail.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Details of the message creation by the run step.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message_creation")]
|
||||
public MessageCreationDetailsModel? MessageCreation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Details of tool calls.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tool_calls")]
|
||||
public ToolCallsDetailsModel[] ToolCalls { get; set; } = Array.Empty<ToolCallsDetailsModel>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message creation details.
|
||||
/// </summary>
|
||||
public sealed class MessageCreationDetailsModel
|
||||
{
|
||||
/// <summary>
|
||||
/// ID of the message that was created by this run step.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message_id")]
|
||||
public string MessageId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tool call details.
|
||||
/// </summary>
|
||||
public sealed class ToolCallsDetailsModel
|
||||
{
|
||||
/// <summary>
|
||||
/// ID of the tool call.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The type of tool call.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The definition of the function that was called.
|
||||
/// </summary>
|
||||
[JsonPropertyName("function")]
|
||||
public FunctionDetailsModel Function { get; set; } = FunctionDetailsModel.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Function call details.
|
||||
/// </summary>
|
||||
public sealed class FunctionDetailsModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Empty definition
|
||||
/// </summary>
|
||||
public static FunctionDetailsModel Empty = new();
|
||||
|
||||
/// <summary>
|
||||
/// The name of the function.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The arguments passed to the function.
|
||||
/// </summary>
|
||||
[JsonPropertyName("arguments")]
|
||||
public string Arguments { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The output of the function.
|
||||
/// This will be null if the outputs have not been submitted yet.
|
||||
/// </summary>
|
||||
[JsonPropertyName("output")]
|
||||
public string Output { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Tool entry
|
||||
/// </summary>
|
||||
internal sealed record ToolModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of tool to have at assistant's disposition
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The function definition for Type = 'function'.
|
||||
/// </summary>
|
||||
[JsonPropertyName("function")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public FunctionModel? Function { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the function when ToolModel.Type == 'function'.
|
||||
/// </summary>
|
||||
public sealed record FunctionModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The function name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The function description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The function description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("parameters")]
|
||||
public OpenAIParameters Parameters { get; init; } = OpenAIParameters.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
#pragma warning disable CA1812
|
||||
#pragma warning disable CA1852
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Experimental.Assistants.Models;
|
||||
|
||||
internal sealed class ToolResultModel
|
||||
{
|
||||
private static readonly object s_placeholder = new();
|
||||
|
||||
/// <summary>
|
||||
/// The tool call identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tool_call_id")]
|
||||
public string CallId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The tool output
|
||||
/// </summary>
|
||||
[JsonPropertyName("output")]
|
||||
public object Output { get; set; } = s_placeholder;
|
||||
}
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
// This assembly is currently experimental.
|
||||
[assembly: Experimental("SKEXP0101")]
|
||||
[assembly: Experimental("SKEXP0043")]
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.OpenApi.ApiManifest;
|
||||
using Microsoft.OpenApi.Readers;
|
||||
using Microsoft.OpenApi.Services;
|
||||
using Microsoft.SemanticKernel.Http;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Plugins.OpenApi.Extensions;
|
||||
/// <summary>
|
||||
/// Provides extension methods for the <see cref="Kernel"/> class related to OpenAPI functionality.
|
||||
/// </summary>
|
||||
public static class ApiManifestKernelExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Imports a plugin from an API manifest asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The kernel instance.</param>
|
||||
/// <param name="pluginName">The name of the plugin.</param>
|
||||
/// <param name="filePath">The file path of the API manifest.</param>
|
||||
/// <param name="executionParameters">Optional execution parameters for the plugin.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token.</param>
|
||||
/// <returns>The imported plugin.</returns>
|
||||
public static async Task<KernelPlugin> ImportPluginFromApiManifestAsync(
|
||||
this Kernel kernel,
|
||||
string pluginName,
|
||||
string filePath,
|
||||
OpenApiFunctionExecutionParameters? executionParameters = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
KernelPlugin plugin = await kernel.CreatePluginFromApiManifestAsync(pluginName, filePath, executionParameters, cancellationToken).ConfigureAwait(false);
|
||||
kernel.Plugins.Add(plugin);
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a kernel plugin from an API manifest file asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The kernel instance.</param>
|
||||
/// <param name="pluginName">The name of the plugin.</param>
|
||||
/// <param name="filePath">The file path of the API manifest.</param>
|
||||
/// <param name="executionParameters">Optional execution parameters for the API functions.</param>
|
||||
/// <param name="cancellationToken">Optional cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the created kernel plugin.</returns>
|
||||
public static async Task<KernelPlugin> CreatePluginFromApiManifestAsync(
|
||||
this Kernel kernel,
|
||||
string pluginName,
|
||||
string filePath,
|
||||
OpenApiFunctionExecutionParameters? executionParameters = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Verify.NotNull(kernel);
|
||||
Verify.ValidPluginName(pluginName, kernel.Plugins);
|
||||
|
||||
#pragma warning disable CA2000 // Dispose objects before losing scope. No need to dispose the Http client here. It can either be an internal client using NonDisposableHttpClientHandler or an external client managed by the calling code, which should handle its disposal.
|
||||
var httpClient = HttpClientProvider.GetHttpClient(executionParameters?.HttpClient ?? kernel.Services.GetService<HttpClient>());
|
||||
#pragma warning restore CA2000
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"ApiManifest file not found: {filePath}");
|
||||
}
|
||||
|
||||
var loggerFactory = kernel.LoggerFactory;
|
||||
var logger = loggerFactory.CreateLogger(typeof(ApiManifestKernelExtensions)) ?? NullLogger.Instance;
|
||||
string apiManifestFileJsonContents = await DocumentLoader.LoadDocumentFromFilePathAsync(filePath,
|
||||
logger,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
JsonDocument jsonDocument = JsonDocument.Parse(apiManifestFileJsonContents);
|
||||
|
||||
ApiManifestDocument document = ApiManifestDocument.Load(jsonDocument.RootElement);
|
||||
|
||||
var functions = new List<KernelFunction>();
|
||||
foreach (var apiDependency in document.ApiDependencies)
|
||||
{
|
||||
var apiName = apiDependency.Key;
|
||||
var apiDependencyDetails = apiDependency.Value;
|
||||
|
||||
var apiDescriptionUrl = apiDependencyDetails.ApiDescriptionUrl;
|
||||
|
||||
var openApiDocumentString = await DocumentLoader.LoadDocumentFromUriAsync(new Uri(apiDescriptionUrl),
|
||||
logger,
|
||||
httpClient,
|
||||
authCallback: null,
|
||||
executionParameters?.UserAgent,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
OpenApiDiagnostic diagnostic = new();
|
||||
var openApiDocument = new OpenApiStringReader(new()
|
||||
{
|
||||
BaseUrl = new(apiDescriptionUrl)
|
||||
}
|
||||
).Read(openApiDocumentString, out diagnostic);
|
||||
|
||||
var requestUrls = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
var pathMethodPairs = apiDependencyDetails.Requests.Select(request => (request.UriTemplate, request.Method?.ToUpperInvariant()));
|
||||
foreach (var (UriTemplate, Method) in pathMethodPairs)
|
||||
{
|
||||
if (UriTemplate is null || Method is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (requestUrls.TryGetValue(UriTemplate, out List<string>? value))
|
||||
{
|
||||
value.Add(Method);
|
||||
continue;
|
||||
}
|
||||
|
||||
requestUrls.Add(UriTemplate, new List<string>() { Method });
|
||||
}
|
||||
|
||||
var predicate = OpenApiFilterService.CreatePredicate(null, null, requestUrls, openApiDocument);
|
||||
var filteredOpenApiDocument = OpenApiFilterService.CreateFilteredDocument(openApiDocument, predicate);
|
||||
|
||||
var serverUrl = filteredOpenApiDocument.Servers.FirstOrDefault()?.Url;
|
||||
|
||||
var runner = new RestApiOperationRunner(
|
||||
httpClient,
|
||||
executionParameters?.AuthCallback,
|
||||
executionParameters?.UserAgent,
|
||||
executionParameters?.EnableDynamicPayload ?? true,
|
||||
executionParameters?.EnablePayloadNamespacing ?? false);
|
||||
|
||||
foreach (var path in filteredOpenApiDocument.Paths)
|
||||
{
|
||||
var operations = OpenApiDocumentParser.CreateRestApiOperations(serverUrl, path.Key, path.Value);
|
||||
foreach (RestApiOperation operation in operations)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogTrace("Registering Rest function {0}.{1}", pluginName, operation.Id);
|
||||
functions.Add(OpenApiKernelExtensions.CreateRestApiFunction(pluginName, runner, operation, executionParameters, new Uri(serverUrl), loggerFactory));
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsCriticalException())
|
||||
{
|
||||
//Logging the exception and keep registering other Rest functions
|
||||
logger.LogWarning(ex, "Something went wrong while rendering the Rest function. Function: {0}.{1}. Error: {2}",
|
||||
pluginName, operation.Id, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return KernelPluginFactory.CreateFromFunctions(pluginName, null, functions);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -5,16 +5,20 @@
|
||||
<RootNamespace>$(AssemblyName)</RootNamespace>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
<NoWarn>SKEXP0042</NoWarn>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
<Import Project="$(RepoRoot)/dotnet/src/InternalUtilities/src/InternalUtilities.props" />
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Semantic Kernel - OpenAPI Plugin Extensions</Title>
|
||||
<Description>Semantic Kernel OpenAPI Plugin Extensions</Description>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.OpenApi.ApiManifest" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SemanticKernel.Core\SemanticKernel.Core.csproj" />
|
||||
<ProjectReference Include="..\Functions.OpenApi\Functions.OpenApi.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user