.Net: Experimental Feature - Assistants API (#3446)

### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->
This is an experimental feature that integrates Open AI assistants into
Semantic Kernel:


https://devblogs.microsoft.com/semantic-kernel/assistants-the-future-of-semantic-kernel/

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

This change introduces an assistant framework that integrates with
semantic-kernel patterns, particularly around function calling.

https://platform.openai.com/docs/api-reference/assistants

Assistant usage is showcased in two `KernelSyntaxExamples`.

This initial version does not support:

- Files
- Retrieval
- Code Interpreter



<img width="308" alt="image"
src="https://github.com/microsoft/semantic-kernel/assets/66376200/fc221f53-1717-4767-828c-4f90fd5d0129">

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄

---------

Co-authored-by: Gil LaHaye <[email protected]>
This commit is contained in:
Chris
2023-11-15 23:03:53 +00:00
committed by GitHub
co-authored by Gil LaHaye
parent e721b85d25
commit 47b879435d
57 changed files with 3718 additions and 37 deletions
+1
View File
@@ -18,6 +18,7 @@ extend-exclude = [
[default.extend-words]
ACI = "ACI" # Azure Container Instance
exercize = "exercize" #test typos
[default.extend-identifiers]
ags = "ags" # Azure Graph Service
+36 -16
View File
@@ -159,8 +159,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.Milvus",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugins.Memory", "src\Plugins\Plugins.Memory\Plugins.Memory.csproj", "{E91365A1-8B01-4AB8-825F-67E3515EADCD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.MongoDB", "src\Connectors\Connectors.Memory.MongoDB\Connectors.Memory.MongoDB.csproj", "{CD24F70C-224D-4125-AED4-AFFE6F68C997}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemplateEngine.Handlebars", "src\Extensions\TemplateEngine.Handlebars\TemplateEngine.Handlebars.csproj", "{4CA6B046-DFE1-417C-A4A0-43BFD262C681}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "experimental", "experimental", "{A2357CF8-3BB9-45A1-93F1-B366C9B63658}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Orchestration.Flow", "src\Experimental\Orchestration.Flow\Experimental.Orchestration.Flow.csproj", "{C8BFFC74-3050-4F63-9E9D-C4F600427DE9}"
@@ -169,6 +167,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Orchestration.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Orchestration.Flow.IntegrationTests", "src\Experimental\Orchestration.Flow.IntegrationTests\Experimental.Orchestration.Flow.IntegrationTests.csproj", "{C17DD7E1-E588-46AF-A87E-DF8829A021DC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Assistants", "src\Experimental\Assistants\Experimental.Assistants.csproj", "{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Assistants.UnitTests", "src\Experimental\Assistants.UnitTests\Experimental.Assistants.UnitTests.csproj", "{815E113D-6AAC-4AE8-8E14-5381E01E49C7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.MongoDB", "src\Connectors\Connectors.Memory.MongoDB\Connectors.Memory.MongoDB.csproj", "{6009CC87-32F1-4282-88BB-8E5A7BA12925}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemplateEngine.Handlebars", "src\Extensions\TemplateEngine.Handlebars\TemplateEngine.Handlebars.csproj", "{B0646036-0C50-4F66-B479-ADA9C1166816}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -389,24 +395,12 @@ Global
{8B754E80-7A97-4585-8D7E-1D588FA5F727}.Publish|Any CPU.Build.0 = Debug|Any CPU
{8B754E80-7A97-4585-8D7E-1D588FA5F727}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B754E80-7A97-4585-8D7E-1D588FA5F727}.Release|Any CPU.Build.0 = Release|Any CPU
{CD24F70C-224D-4125-AED4-AFFE6F68C997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD24F70C-224D-4125-AED4-AFFE6F68C997}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD24F70C-224D-4125-AED4-AFFE6F68C997}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
{CD24F70C-224D-4125-AED4-AFFE6F68C997}.Publish|Any CPU.Build.0 = Publish|Any CPU
{CD24F70C-224D-4125-AED4-AFFE6F68C997}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD24F70C-224D-4125-AED4-AFFE6F68C997}.Release|Any CPU.Build.0 = Release|Any CPU
{E91365A1-8B01-4AB8-825F-67E3515EADCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E91365A1-8B01-4AB8-825F-67E3515EADCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E91365A1-8B01-4AB8-825F-67E3515EADCD}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
{E91365A1-8B01-4AB8-825F-67E3515EADCD}.Publish|Any CPU.Build.0 = Publish|Any CPU
{E91365A1-8B01-4AB8-825F-67E3515EADCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E91365A1-8B01-4AB8-825F-67E3515EADCD}.Release|Any CPU.Build.0 = Release|Any CPU
{4CA6B046-DFE1-417C-A4A0-43BFD262C681}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4CA6B046-DFE1-417C-A4A0-43BFD262C681}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4CA6B046-DFE1-417C-A4A0-43BFD262C681}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
{4CA6B046-DFE1-417C-A4A0-43BFD262C681}.Publish|Any CPU.Build.0 = Publish|Any CPU
{4CA6B046-DFE1-417C-A4A0-43BFD262C681}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4CA6B046-DFE1-417C-A4A0-43BFD262C681}.Release|Any CPU.Build.0 = Release|Any CPU
{C8BFFC74-3050-4F63-9E9D-C4F600427DE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8BFFC74-3050-4F63-9E9D-C4F600427DE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8BFFC74-3050-4F63-9E9D-C4F600427DE9}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
@@ -425,6 +419,30 @@ Global
{C17DD7E1-E588-46AF-A87E-DF8829A021DC}.Publish|Any CPU.Build.0 = Debug|Any CPU
{C17DD7E1-E588-46AF-A87E-DF8829A021DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C17DD7E1-E588-46AF-A87E-DF8829A021DC}.Release|Any CPU.Build.0 = Release|Any CPU
{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA}.Publish|Any CPU.Build.0 = Publish|Any CPU
{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA}.Release|Any CPU.Build.0 = Release|Any CPU
{815E113D-6AAC-4AE8-8E14-5381E01E49C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{815E113D-6AAC-4AE8-8E14-5381E01E49C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{815E113D-6AAC-4AE8-8E14-5381E01E49C7}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
{815E113D-6AAC-4AE8-8E14-5381E01E49C7}.Publish|Any CPU.Build.0 = Debug|Any CPU
{815E113D-6AAC-4AE8-8E14-5381E01E49C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{815E113D-6AAC-4AE8-8E14-5381E01E49C7}.Release|Any CPU.Build.0 = Release|Any CPU
{6009CC87-32F1-4282-88BB-8E5A7BA12925}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6009CC87-32F1-4282-88BB-8E5A7BA12925}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6009CC87-32F1-4282-88BB-8E5A7BA12925}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
{6009CC87-32F1-4282-88BB-8E5A7BA12925}.Publish|Any CPU.Build.0 = Publish|Any CPU
{6009CC87-32F1-4282-88BB-8E5A7BA12925}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6009CC87-32F1-4282-88BB-8E5A7BA12925}.Release|Any CPU.Build.0 = Release|Any CPU
{B0646036-0C50-4F66-B479-ADA9C1166816}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B0646036-0C50-4F66-B479-ADA9C1166816}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B0646036-0C50-4F66-B479-ADA9C1166816}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
{B0646036-0C50-4F66-B479-ADA9C1166816}.Publish|Any CPU.Build.0 = Publish|Any CPU
{B0646036-0C50-4F66-B479-ADA9C1166816}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B0646036-0C50-4F66-B479-ADA9C1166816}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -480,13 +498,15 @@ Global
{F224B869-FA0E-4EEE-A6BF-C2D61FF8E731} = {A21FAC7C-0C09-4EAD-843B-926ACEF73C80}
{CC77DCFA-A419-4202-A98A-868CDF457792} = {A21FAC7C-0C09-4EAD-843B-926ACEF73C80}
{8B754E80-7A97-4585-8D7E-1D588FA5F727} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{CD24F70C-224D-4125-AED4-AFFE6F68C997} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{E91365A1-8B01-4AB8-825F-67E3515EADCD} = {D6D598DF-C17C-46F4-B2B9-CDE82E2DE132}
{4CA6B046-DFE1-417C-A4A0-43BFD262C681} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
{A2357CF8-3BB9-45A1-93F1-B366C9B63658} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0}
{C8BFFC74-3050-4F63-9E9D-C4F600427DE9} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658}
{CB6E74CD-3A25-459F-A578-DEF25A414335} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658}
{C17DD7E1-E588-46AF-A87E-DF8829A021DC} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658}
{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658}
{815E113D-6AAC-4AE8-8E14-5381E01E49C7} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658}
{6009CC87-32F1-4282-88BB-8E5A7BA12925} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{B0646036-0C50-4F66-B479-ADA9C1166816} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
@@ -32,10 +32,10 @@ public static class Example59_OpenAIFunctionCalling
// Include all functions registered with the kernel.
// Alternatively, you can provide your own list of OpenAIFunctions to include.
Functions = kernel.Functions.GetFunctionViews().Select(f => f.ToOpenAIFunction()).ToList(),
FunctionCall = "TimePlugin-Date",
};
// Set FunctionCall to the name of a specific function to force the model to use that function.
requestSettings.FunctionCall = "TimePlugin-Date";
await CompleteChatWithFunctionsAsync("What day is today?", chatHistory, chatCompletion, kernel, requestSettings);
// Uncomment the samples and run them one at a time
@@ -0,0 +1,170 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Experimental.Assistants;
using Microsoft.SemanticKernel.Orchestration;
using Plugins;
using Resources;
// ReSharper disable once InconsistentNaming
/// <summary>
/// Showcase Open AI Assistant integration with semantic kernel.
/// </summary>
public static class Example70_Assistant
{
private const string OpenAIFunctionEnabledModel = "gpt-3.5-turbo-1106";
/// <summary>
/// Show how to define an use a single assistant using multiple patterns.
/// </summary>
public static async Task RunAsync()
{
Console.WriteLine("======== Example70_Assistant ========");
if (TestConfiguration.OpenAI.ApiKey == null)
{
Console.WriteLine("OpenAI apiKey not found. Skipping example.");
return;
}
await RunSimpleChatAsync();
await RunWithNativeFunctionsAsync();
await RunWithSemanticFunctionsAsync();
await RunAsFunctionAsync();
}
private static async Task RunSimpleChatAsync()
{
Console.WriteLine("======== Run:SimpleChat ========");
await ChatAsync(
"Assistants.ParrotAssistant.yaml",
"Fortune favors the bold.",
"I came, I saw, I conquered.",
"Practice makes perfect.");
}
private static async Task RunWithNativeFunctionsAsync()
{
Console.WriteLine("======== Run:WithNativeFunctions ========");
IKernel kernel = new KernelBuilder().Build();
var functions = kernel.ImportFunctions(new MenuPlugin(), nameof(MenuPlugin));
await ChatAsync(
"Assistants.ToolAssistant.yaml",
functions.Values,
"Hello",
"What is the special soup?",
"What is the special drink?",
"Thank you!");
}
private static async Task RunWithSemanticFunctionsAsync()
{
Console.WriteLine("======== Run:WithSemanticFunctions ========");
IKernel kernel = new KernelBuilder().Build();
var functions =
new[]
{
kernel.CreateSemanticFunction(
"Correct any misspelling or gramatical errors provided in input: {{$input}}",
functionName: "spellChecker",
pluginName: "test",
description: "Correct the spelling for the user input."),
};
await ChatAsync(
"Assistants.ToolAssistant.yaml",
functions,
"Hello",
"Is this spelled correctly: exercize",
"What is the special soup?",
"Thank you!");
}
private static async Task RunAsFunctionAsync()
{
Console.WriteLine("======== Run:AsFunction ========");
var assistant =
await AssistantBuilder.FromDefinitionAsync(
TestConfiguration.OpenAI.ApiKey,
OpenAIFunctionEnabledModel,
EmbeddedResource.Read("Assistants.ParrotAssistant.yaml"));
IKernel kernel = new KernelBuilder().Build();
var assistants = kernel.ImportFunctions(assistant, assistant.Id);
var variables = new ContextVariables
{
["input"] = "Practice makes perfect."
};
var result = await kernel.RunAsync(assistants.Single().Value, variables);
var resultValue = result.GetValue<string>();
var response = JsonSerializer.Deserialize<AssistantResponse>(resultValue ?? string.Empty);
Console.WriteLine(
response?.Response ??
$"No response from assistant: {assistant.Id}");
}
private static Task ChatAsync(
string resourcePath,
params string[] messages)
{
return ChatAsync(resourcePath, null, messages);
}
private static async Task ChatAsync(
string resourcePath,
IEnumerable<ISKFunction>? functions,
params string[] messages)
{
var definition = EmbeddedResource.Read(resourcePath);
var assistant =
await AssistantBuilder.FromDefinitionAsync(
TestConfiguration.OpenAI.ApiKey,
OpenAIFunctionEnabledModel,
definition,
functions);
Console.WriteLine($"[{assistant.Id}]");
var thread = await assistant.NewThreadAsync();
foreach (var message in messages)
{
var messageUser = await thread.AddUserMessageAsync(message).ConfigureAwait(true);
DisplayMessage(messageUser);
var assistantMessages = await thread.InvokeAsync(assistant).ConfigureAwait(true);
DisplayMessages(assistantMessages);
}
}
private static void DisplayMessages(IEnumerable<IChatMessage> messages)
{
foreach (var message in messages)
{
DisplayMessage(message);
}
}
private static void DisplayMessage(IChatMessage message)
{
Console.WriteLine($"[{message.Id}]");
Console.WriteLine($"# {message.Role}: {message.Content}");
}
}
@@ -0,0 +1,103 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Experimental.Assistants;
using Plugins;
using Resources;
// ReSharper disable once InconsistentNaming
/// <summary>
/// Showcase complex Open AI Assistant interactions using semantic kernel.
/// </summary>
public static class Example71_AssistantDelegation
{
private const string OpenAIFunctionEnabledModel = "gpt-3.5-turbo-1106";
/// <summary>
/// Show how to combine coordinate multiple assistants.
/// </summary>
public static async Task RunAsync()
{
Console.WriteLine("======== Example71_AssistantDelegation ========");
if (TestConfiguration.OpenAI.ApiKey == null)
{
Console.WriteLine("OpenAI apiKey not found. Skipping example.");
return;
}
IKernel kernel = new KernelBuilder().Build();
var functions = kernel.ImportFunctions(new MenuPlugin(), nameof(MenuPlugin));
var menuAssistant =
await AssistantBuilder.FromDefinitionAsync(
TestConfiguration.OpenAI.ApiKey,
model: OpenAIFunctionEnabledModel,
template: EmbeddedResource.Read("Assistants.ToolAssistant.yaml"),
functions: functions.Values);
var parrotAssistant =
await AssistantBuilder.FromDefinitionAsync(
TestConfiguration.OpenAI.ApiKey,
model: OpenAIFunctionEnabledModel,
template: EmbeddedResource.Read("Assistants.ParrotAssistant.yaml"));
var helperAssistants = Import(menuAssistant, parrotAssistant).ToArray();
var toolAssistant =
await AssistantBuilder.FromDefinitionAsync(
TestConfiguration.OpenAI.ApiKey,
model: OpenAIFunctionEnabledModel,
template: EmbeddedResource.Read("Assistants.ToolAssistant.yaml"),
functions: helperAssistants);
await ChatAsync(
toolAssistant,
"What's on the menu?",
"Can you talk like pirate?",
"Thank you");
IEnumerable<ISKFunction> Import(params IAssistant[] assistants)
{
foreach (var assistant in assistants)
{
var functions = kernel.ImportFunctions(assistant, assistant.Id);
foreach (var function in functions.Values)
{
yield return function;
}
}
}
}
private static async Task ChatAsync(IAssistant assistant, params string[] messages)
{
var thread = await assistant.NewThreadAsync();
foreach (var message in messages)
{
var messageUser = await thread.AddUserMessageAsync(message).ConfigureAwait(true);
DisplayMessage(messageUser);
var assistantMessages = await thread.InvokeAsync(assistant).ConfigureAwait(true);
DisplayMessages(assistantMessages);
}
}
private static void DisplayMessages(IEnumerable<IChatMessage> messages)
{
foreach (var message in messages)
{
DisplayMessage(message);
}
}
private static void DisplayMessage(IChatMessage message)
{
Console.WriteLine($"[{message.Id}]");
Console.WriteLine($"# {message.Role}: {message.Content}");
}
}
@@ -11,9 +11,6 @@
<!-- Suppress: "Declare types in namespaces", "Require ConfigureAwait" -->
<NoWarn>CA1050;CA1707;CA2007;VSTHRD111;CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<None Remove="appsettings.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Configuration" />
@@ -45,6 +42,7 @@
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Weaviate\Connectors.Memory.Weaviate.csproj" />
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Redis\Connectors.Memory.Redis.csproj" />
<ProjectReference Include="..\..\src\Connectors\Connectors.Memory.Pinecone\Connectors.Memory.Pinecone.csproj" />
<ProjectReference Include="..\..\src\Experimental\Assistants\Experimental.Assistants.csproj" />
<ProjectReference Include="..\..\src\Experimental\Orchestration.Flow\Experimental.Orchestration.Flow.csproj" />
<ProjectReference Include="..\..\src\Extensions\Reliability.Basic\Reliability.Basic.csproj" />
<ProjectReference Include="..\..\src\Extensions\TemplateEngine.Basic\TemplateEngine.Basic.csproj" />
@@ -68,6 +66,12 @@
<EmbeddedResource Include="Resources\30-user-prompt.txt" />
<EmbeddedResource Include="Resources\30-system-prompt.txt" />
<EmbeddedResource Include="Resources\30-user-context.txt" />
<EmbeddedResource Include="Resources\Assistants\ParrotAssistant.yaml">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\Assistants\ToolAssistant.yaml">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\EnglishRoberta\dict.txt" />
<EmbeddedResource Include="Resources\EnglishRoberta\encoder.json" />
<EmbeddedResource Include="Resources\EnglishRoberta\vocab.bpe" />
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.SemanticKernel;
namespace Plugins;
public sealed class MenuPlugin
{
[SKFunction, Description("Provides a list of specials from the menu.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")]
public string GetSpecials()
{
return @"
Special Soup: Clam Chowder
Special Salad: Cobb Chowder
Special Drink: Chai Tea
";
}
[SKFunction, Description("Provides the price of the requested menu item.")]
public string GetItemPrice(
[Description("The name of the menu item.")]
string menuItem)
{
return "$9.99";
}
}
@@ -0,0 +1,4 @@
name: Parrot
instructions: |
Repeat the user message in the voice of a pirate and then end with a parrot sound.
description: A fun chat bot.
@@ -0,0 +1,5 @@
name: ToolRunner
instructions: |
Respond to the user using a tool. If no tool is appropriate, let the user know you are able to provide responses using tools.
When reporting a tool result, start with, "The tool I used informed me that"
description: Determines if a tool can be utilized to accomplish a result.
@@ -473,7 +473,7 @@ public static class OpenAIKernelBuilderExtensions
OpenAIChatCompletion Factory(ILoggerFactory loggerFactory)
{
return new(deploymentName, openAIClient, loggerFactory);
};
}
builder.WithAIService<IChatCompletion>(serviceId, Factory, setAsDefault);
@@ -567,10 +567,12 @@ public static class OpenAIKernelBuilderExtensions
private static OpenAIClientOptions CreateOpenAIClientOptions(ILoggerFactory loggerFactory, IDelegatingHandlerFactory httpHandlerFactory, HttpClient? httpClient)
{
OpenAIClientOptions options = new();
OpenAIClientOptions options = new()
{
#pragma warning disable CA2000 // Dispose objects before losing scope
options.Transport = new HttpClientTransport(HttpClientProvider.GetHttpClient(httpHandlerFactory, httpClient, loggerFactory));
options.RetryPolicy = new RetryPolicy(maxRetries: 0); //Disabling Azure SDK retry policy to use the one provided by the delegating handler factory or the HTTP client.
Transport = new HttpClientTransport(HttpClientProvider.GetHttpClient(httpHandlerFactory, httpClient, loggerFactory)),
RetryPolicy = new RetryPolicy(maxRetries: 0) //Disabling Azure SDK retry policy to use the one provided by the delegating handler factory or the HTTP client.
};
#pragma warning restore CA2000 // Dispose objects before losing scope
return options;
@@ -0,0 +1,49 @@
<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</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>
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
using Moq;
using Xunit;
namespace SemanticKernel.Experimental.Assistants.UnitTests.Extensions;
[Trait("Category", "Unit Tests")]
[Trait("Feature", "Assistant")]
public sealed class KernelExtensionTests
{
private const string SinglePartToolName = "Bogus";
private const string TwoPartToolName = "Fake-Bogus";
[Fact]
public static void InvokeSinglePartTool()
{
var mockFunctionCollection = new Mock<IReadOnlyFunctionCollection>();
var kernel = new Mock<IKernel>();
kernel.SetupGet(k => k.Functions).Returns(mockFunctionCollection.Object);
kernel.Object.GetAssistantTool(SinglePartToolName);
kernel.Verify(x => x.Functions.GetFunction(SinglePartToolName), Times.Once());
kernel.Verify(x => x.Functions.GetFunction(It.IsAny<string>(), SinglePartToolName), Times.Never());
}
[Fact]
public static void InvokeTwoPartTool()
{
var mockFunctionCollection = new Mock<IReadOnlyFunctionCollection>();
var kernel = new Mock<IKernel>();
kernel.SetupGet(k => k.Functions).Returns(mockFunctionCollection.Object);
kernel.Object.GetAssistantTool(TwoPartToolName);
kernel.Verify(x => x.Functions.GetFunction(SinglePartToolName), Times.Never());
kernel.Verify(x => x.Functions.GetFunction("Fake", SinglePartToolName), Times.Once());
}
[Fact]
public static void InvokeInvalidThreePartTool()
{
var mockFunctionCollection = new Mock<IReadOnlyFunctionCollection>();
var kernel = new Mock<IKernel>();
kernel.SetupGet(k => k.Functions).Returns(mockFunctionCollection.Object);
Assert.Throws<SKException>(() => kernel.Object.GetAssistantTool("i-am-not-valid"));
kernel.Verify(x => x.Functions.GetFunction(SinglePartToolName), Times.Never());
kernel.Verify(x => x.Functions.GetFunction("Fake", SinglePartToolName), Times.Never());
}
}
@@ -0,0 +1,67 @@
// 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.Extensions;
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
using Moq;
using Moq.Protected;
using Xunit;
namespace SemanticKernel.Experimental.Assistants.UnitTests.Extensions;
[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));
}
}
@@ -0,0 +1,69 @@
// 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.Extensions;
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
using Moq;
using Moq.Protected;
using Xunit;
namespace SemanticKernel.Experimental.Assistants.UnitTests.Extensions;
[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);
}
}
@@ -0,0 +1,71 @@
// 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.Extensions;
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
using Moq;
using Moq.Protected;
using Xunit;
namespace SemanticKernel.Experimental.Assistants.UnitTests.Extensions;
[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));
}
}
@@ -0,0 +1,57 @@
// 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.Extensions;
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
using Moq;
using Moq.Protected;
using Xunit;
namespace SemanticKernel.Experimental.Assistants.UnitTests.Extensions;
[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));
}
}
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
using Moq;
using Xunit;
namespace SemanticKernel.Experimental.Assistants.UnitTests.Extensions;
[Trait("Category", "Unit Tests")]
[Trait("Feature", "Assistant")]
public sealed class SKFunctionExtensionTests
{
private const string ToolName = "Bogus";
private const string PluginName = "Fake";
[Fact]
public static void GetSinglePartName()
{
var mockFunction = new Mock<ISKFunction>();
mockFunction.SetupGet(f => f.Name).Returns(ToolName);
string qualifiedName = mockFunction.Object.GetQualifiedName();
Assert.Equal(ToolName, qualifiedName);
}
[Fact]
public static void GetTwoPartName()
{
var mockFunction = new Mock<ISKFunction>();
mockFunction.SetupGet(f => f.Name).Returns(ToolName);
mockFunction.SetupGet(f => f.PluginName).Returns(PluginName);
string qualifiedName = mockFunction.Object.GetQualifiedName();
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 ParameterView("required", IsRequired: true);
var optionalParam = new ParameterView("optional", IsRequired: false);
var parameters = new List<ParameterView> { requiredParam, optionalParam };
var functionView = new FunctionView(ToolName, PluginName, FunctionDescription, parameters);
var mockFunction = new Mock<ISKFunction>();
mockFunction.SetupGet(f => f.Name).Returns(ToolName);
mockFunction.SetupGet(f => f.PluginName).Returns(PluginName);
mockFunction.SetupGet(f => f.Description).Returns(FunctionDescription);
mockFunction.Setup(f => f.Describe()).Returns(functionView);
var toolModel = mockFunction.Object.ToToolModel();
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);
}
}
@@ -0,0 +1,148 @@
// 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.Extensions;
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 AssistantBuilder.FromTemplateAsync(
apiKey: TestConfig.OpenAIApiKey,
model: TestConfig.SupportedGpt35TurboModel,
definitionPath: "Templates/PoetAssistant.yaml").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}");
}
}
@@ -0,0 +1,154 @@
// Copyright (c) Microsoft. All rights reserved.
#define DISABLEHOST // Comment line to enable
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Experimental.Assistants;
using Xunit;
using Xunit.Abstractions;
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 AssistantBuilder.FromTemplateAsync(
apiKey: TestConfig.OpenAIApiKey,
model: TestConfig.SupportedGpt35TurboModel,
definitionPath: "Templates/PoetAssistant.yaml").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 kernel = new KernelBuilder().Build();
var gamePlugin = kernel.ImportFunctions(new GuessingGame(), nameof(GuessingGame));
var assistant =
await AssistantBuilder.FromTemplateAsync(
apiKey: TestConfig.OpenAIApiKey,
model: TestConfig.SupportedGpt35TurboModel,
definitionPath: "Templates/GameAssistant.yaml",
functions: gamePlugin.Values).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).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>
[SKFunction, Description("Get the guessing game question")]
public string GetQuestion() => "What color am I thinking of?";
/// <summary>
/// Get the answer
/// </summary>
[SKFunction, Description("Get the answer to the guessing game question.")]
public string GetAnswer() => "Blue";
}
}
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft. All rights reserved.
#define DISABLEHOST // Comment line to enable
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Experimental.Assistants;
using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
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()
.WithOpenAIChatCompletionService(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<SKException>(() => context.GetThreadModelAsync(thread.Id)).ConfigureAwait(true);
}
}
@@ -0,0 +1,21 @@
// 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>());
}
}
@@ -0,0 +1,4 @@
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.
@@ -0,0 +1,4 @@
name: Poet
instructions: |
Compose a sonnet inspired by the user input.
description: You are a poet that composes poems based on user input.
@@ -0,0 +1,29 @@
// 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();
}
}
@@ -0,0 +1,3 @@
{
"OpenAIApiKey": ""
}
@@ -0,0 +1,121 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.ChatCompletion;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
using YamlDotNet.Serialization;
namespace Microsoft.SemanticKernel.Experimental.Assistants;
/// <summary>
/// Context for interacting with OpenAI REST API.
/// </summary>
public partial class AssistantBuilder
{
/// <summary>
/// Create a new assistant from a yaml formatted string.
/// </summary>
/// <param name="apiKey">The OpenAI API key</param>
/// <param name="model">The LLM name</param>
/// <param name="template">YAML assistant definition.</param>
/// <param name="functions">Functions to associate with the tool.</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>The requested <see cref="IAssistant">.</see></returns>
public static async Task<IAssistant> FromDefinitionAsync(
string apiKey,
string model,
string template,
IEnumerable<ISKFunction>? functions = null,
CancellationToken cancellationToken = default)
{
var deserializer = new DeserializerBuilder().Build();
var assistantKernelModel = deserializer.Deserialize<AssistantConfigurationModel>(template);
return
await new AssistantBuilder()
.WithOpenAIChatCompletionService(model, apiKey)
.WithInstructions(assistantKernelModel.Instructions.Trim())
.WithName(assistantKernelModel.Name.Trim())
.WithDescription(assistantKernelModel.Description.Trim())
.WithFunctions(functions ?? Array.Empty<ISKFunction>())
.BuildAsync(cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
/// Create a new assistant from a yaml template.
/// </summary>
/// <param name="apiKey">The OpenAI API key</param>
/// <param name="model">The LLM name</param>
/// <param name="definitionPath">Path to a configuration file.</param>
/// <param name="functions">Functions to associate with the tool.</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>The requested <see cref="IAssistant">.</see></returns>
public static Task<IAssistant> FromTemplateAsync(
string apiKey,
string model,
string definitionPath,
IEnumerable<ISKFunction>? functions = null,
CancellationToken cancellationToken = default)
{
var yamlContent = File.ReadAllText(definitionPath);
return FromDefinitionAsync(apiKey, model, yamlContent, functions, cancellationToken);
}
/// <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()
.WithOpenAIChatCompletionService(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="functions">Functions 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<ISKFunction>? functions = null,
CancellationToken cancellationToken = default)
{
var restContext = new OpenAIRestContext(apiKey);
var resultModel =
await restContext.GetAssistantModelAsync(assistantId, cancellationToken).ConfigureAwait(false) ??
throw new SKException($"Unexpected failure retrieving assistant: no result. ({assistantId})");
var chatService = new OpenAIChatCompletion(resultModel.Model, apiKey);
return new Assistant(resultModel, chatService, restContext, functions);
}
}
@@ -0,0 +1,167 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.ChatCompletion;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
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 Dictionary<string, ISKFunction> _functions;
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._functions = new(12);
}
/// <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 SKException("Model must be defined for assistant.");
}
if (string.IsNullOrWhiteSpace(this._apiKey))
{
throw new SKException("ApiKey must be provided for assistant.");
}
return
await Assistant.CreateAsync(
new OpenAIRestContext(this._apiKey!, this._httpClientProvider),
new OpenAIChatCompletion(this._model.Model, this._apiKey!),
this._model,
this._functions.Values,
cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Define the OpenAI chat completion service (required).
/// </summary>
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
public AssistantBuilder WithOpenAIChatCompletionService(string model, string apiKey)
{
this._apiKey = apiKey;
this._model.Model = model;
return this;
}
/// <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 WithFunction(ISKFunction tool)
{
this._functions[tool.GetQualifiedName()] = tool;
return this;
}
/// <summary>
/// Define functions associated with assistant instance (optional).
/// </summary>
/// <returns><see cref="AssistantBuilder"/> instance for fluid expression.</returns>
public AssistantBuilder WithFunctions(IEnumerable<ISKFunction> tools)
{
foreach (var tool in tools)
{
this.WithFunction(tool);
}
return this;
}
}
@@ -0,0 +1,29 @@
// 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="ISKFunction"/>.
/// </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 Response { get; set; } = string.Empty;
/// <summary>
/// Instructions from assistant on next steps.
/// </summary>
[JsonPropertyName("system_instructions")]
public string Instructions { get; set; } = string.Empty;
}
@@ -0,0 +1,32 @@
<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>
<LangVersion>Latest</LangVersion>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Semantic Kernel Assistants</Title>
<Description>Semantic Kernel Assistants</Description>
</PropertyGroup>
<ItemGroup>
<None Remove="C:\Users\crickman\source\repos\semantic-kernel2/dotnet/nuget/icon.png" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="SemanticKernel.Experimental.Assistants.UnitTests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="YamlDotNet" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Connectors\Connectors.AI.OpenAI\Connectors.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
<ProjectReference Include="..\..\SemanticKernel.Core\SemanticKernel.Core.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="SemanticKernel.Experimental.Assistants.UnitTests" />
</ItemGroup>
</Project>
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel.Diagnostics;
namespace Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
internal static class IKernelExtensions
{
/// <summary>
/// Retrieve a kernel function based on the tool name.
/// </summary>
public static ISKFunction GetAssistantTool(this IKernel kernel, string toolName)
{
string[] nameParts = toolName.Split('-');
return nameParts.Length switch
{
1 => kernel.Functions.GetFunction(toolName),
2 => kernel.Functions.GetFunction(nameParts[0], nameParts[1]),
_ => throw new SKException($"Unknown tool: {toolName}"),
};
}
}
@@ -0,0 +1,91 @@
// 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.Extensions;
internal static class ISKFunctionExtensions
{
/// <summary>
/// Produce a fully qualified toolname.
/// </summary>
public static string GetQualifiedName(this ISKFunction function)
{
return
string.IsNullOrWhiteSpace(function.PluginName) ?
function.Name :
$"{function.PluginName}-{function.Name}";
}
/// <summary>
/// Convert <see cref="ISKFunction"/> to an OpenAI tool model.
/// </summary>
/// <param name="function">The source function</param>
/// <returns>An OpenAI tool model</returns>
public static ToolModel ToToolModel(this ISKFunction function)
{
var view = function.Describe();
var required = new List<string>(view.Parameters.Count);
var properties =
view.Parameters.ToDictionary(
p => p.Name,
p =>
{
if (p.IsRequired ?? false)
{
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(),
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;
}
}
@@ -0,0 +1,136 @@
// 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.Extensions;
/// <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}";
}
}
@@ -0,0 +1,119 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.AI.ChatCompletion;
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
namespace Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
/// <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}";
}
}
@@ -0,0 +1,137 @@
// 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.Extensions;
/// <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";
}
}
@@ -0,0 +1,69 @@
// 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.Extensions;
/// <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}";
}
}
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Experimental.Assistants.Internal;
namespace Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
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 SKException($"Unexpected failure: {response.StatusCode} [{url}]");
}
string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return
JsonSerializer.Deserialize<TResult>(responseBody) ??
throw new SKException($"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 SKException($"Unexpected failure: {response.StatusCode} [{url}]");
}
string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return
JsonSerializer.Deserialize<TResult>(responseBody) ??
throw new SKException($"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 SKException($"Unexpected failure: {response.StatusCode} [{url}]");
}
}
}
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
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="IKernel"/> instance associated with the assistant.
/// </summary>
internal IKernel Kernel { get; }
/// <summary>
/// Tools defined for run execution.
/// </summary>
internal IList<ISKFunction> Functions { get; }
/// <summary>
/// Creates a new assistant chat thread.
/// </summary>
/// <param name="cancellationToken">A cancellation token</param>
Task<IChatThread> NewThreadAsync(CancellationToken cancellationToken = default);
}
@@ -0,0 +1,36 @@
// 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; }
}
@@ -0,0 +1,40 @@
// 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>
Task<IEnumerable<IChatMessage>> InvokeAsync(IAssistant assistant, CancellationToken cancellationToken = default);
/// <summary>
/// Delete existing thread.
/// </summary>
/// <param name="cancellationToken">A cancellation token</param>
Task DeleteThreadAsync(CancellationToken cancellationToken = default);
}
@@ -0,0 +1,144 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.AI.ChatCompletion;
using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.ChatCompletion;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
using Microsoft.SemanticKernel.Http;
using Microsoft.SemanticKernel.Services;
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 IKernel Kernel { get; }
/// <inheritdoc/>
public IList<ISKFunction> Functions { get; }
/// <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 readonly OpenAIRestContext _restContext;
private readonly AssistantModel _model;
/// <summary>
/// Create a new assistant.
/// </summary>
/// <param name="restContext">A context for accessing OpenAI REST endpoint</param>
/// <param name="chatService">An OpenAI chat service.</param>
/// <param name="assistantModel">The assistant definition</param>
/// <param name="functions">Functions 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,
OpenAIChatCompletion chatService,
AssistantModel assistantModel,
IEnumerable<ISKFunction>? functions = null,
CancellationToken cancellationToken = default)
{
var resultModel =
await restContext.CreateAssistantModelAsync(assistantModel, cancellationToken).ConfigureAwait(false) ??
throw new SKException("Unexpected failure creating assistant: no result.");
return new Assistant(resultModel, chatService, restContext, functions);
}
/// <summary>
/// Initializes a new instance of the <see cref="Assistant"/> class.
/// </summary>
internal Assistant(
AssistantModel model,
OpenAIChatCompletion chatService,
OpenAIRestContext restContext,
IEnumerable<ISKFunction>? functions = null)
{
this._model = model;
this._restContext = restContext;
this.Functions = new List<ISKFunction>(functions ?? Array.Empty<ISKFunction>());
var functionCollection = new FunctionCollection();
foreach (var function in this.Functions)
{
functionCollection.AddFunction(function);
}
var services = new AIServiceCollection();
services.SetService<IChatCompletion>(chatService);
services.SetService<ITextCompletion>(chatService);
this.Kernel =
new Kernel(
functionCollection,
services.Build(),
memory: null!,
NullHttpHandlerFactory.Instance,
loggerFactory: null);
}
/// <inheritdoc/>
public Task<IChatThread> NewThreadAsync(CancellationToken cancellationToken = default)
{
return ChatThread.CreateAsync(this._restContext, cancellationToken);
}
/// <summary>
/// Marshal thread run through <see cref="ISKFunction"/> interface.
/// </summary>
/// <param name="input">The user input</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>An assistant response (<see cref="AssistantResponse"/></returns>
[SKFunction, Description("Provide input to assistant a response")]
public async Task<string> AskAsync(
[Description("The input for the assistant.")]
string input,
CancellationToken cancellationToken = default)
{
var thread = await this.NewThreadAsync(cancellationToken).ConfigureAwait(false);
await thread.AddUserMessageAsync(input, cancellationToken).ConfigureAwait(false);
var message = await thread.InvokeAsync(this, cancellationToken).ConfigureAwait(false);
var response =
new AssistantResponse
{
ThreadId = thread.Id,
Response = string.Concat(message.Select(m => m.Content)),
};
return JsonSerializer.Serialize(response);
}
}
@@ -0,0 +1,44 @@
// 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);
}
}
@@ -0,0 +1,160 @@
// 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.Diagnostics;
using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
using Microsoft.SemanticKernel.Experimental.Assistants.Models;
using Microsoft.SemanticKernel.Orchestration;
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 static readonly TimeSpan s_pollingInterval = TimeSpan.FromMilliseconds(200);
private static readonly HashSet<string> s_pollingStates =
new(StringComparer.OrdinalIgnoreCase)
{
"queued",
"in_progress",
};
private readonly OpenAIRestContext _restContext;
private ThreadRunModel _model;
private readonly IKernel _kernel;
/// <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);
// 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.
this._model = await this._restContext.GetRunAsync(this.ThreadId, this.Id, cancellationToken).ConfigureAwait(false);
await PollRunStatus().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 SKException($"Unexpected failure processing run: {this.Id}: {this._model.LastError?.Message ?? "Unknown"}");
}
var messageIds =
steps.Data
.Where(s => s.StepDetails.MessageCreation != null)
.Select(s => s.StepDetails.MessageCreation!.MessageId)
.ToArray();
return messageIds;
async Task PollRunStatus()
{
while (s_pollingStates.Contains(this._model.Status))
{
await Task.Delay(s_pollingInterval, cancellationToken).ConfigureAwait(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,
IKernel 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);
return
new ToolResultModel
{
CallId = callId,
Output = result,
};
async Task<string> InvokeFunctionCallAsync()
{
var function = this._kernel.GetAssistantTool(functionDetails.Name);
var variables = new ContextVariables();
if (!string.IsNullOrWhiteSpace(functionDetails.Arguments))
{
var arguments = JsonSerializer.Deserialize<Dictionary<string, object>>(functionDetails.Arguments)!;
foreach (var argument in arguments)
{
variables[argument.Key] = argument.Value.ToString();
}
}
var results = await this._kernel.RunAsync(function, variables, cancellationToken).ConfigureAwait(false);
return results.GetValue<string>() ?? string.Empty;
}
}
}
@@ -0,0 +1,116 @@
// 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.Diagnostics;
using Microsoft.SemanticKernel.Experimental.Assistants.Extensions;
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;
/// <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)
{
var threadModel =
await restContext.CreateThreadModelAsync(cancellationToken).ConfigureAwait(false) ??
throw new SKException("Unexpected failure creating thread: no result.");
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) ??
throw new SKException($"Unexpected failure retrieving thread: no result. ({threadId})");
var messageListModel =
await restContext.GetMessagesAsync(threadId, cancellationToken).ConfigureAwait(false) ??
throw new SKException($"Unexpected failure retrieving thread: no result. ({threadId})");
return new ChatThread(threadModel, messageListModel, restContext);
}
/// <inheritdoc/>
public async Task<IChatMessage> AddUserMessageAsync(string message, CancellationToken cancellationToken = default)
{
var messageModel =
await this._restContext.CreateUserTextMessageAsync(
this.Id,
message,
cancellationToken).ConfigureAwait(false);
return new ChatMessage(messageModel);
}
/// <inheritdoc/>
public async Task<IEnumerable<IChatMessage>> InvokeAsync(IAssistant assistant, CancellationToken cancellationToken)
{
var tools = assistant.Functions.Select(f => f.ToToolModel()).ToArray();
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.GetMessagesAsync(results, cancellationToken).ToListAsync(cancellationToken).ConfigureAwait(false);
return messages;
}
/// <summary>
/// Delete an existing thread.
/// </summary>
/// <param name="cancellationToken">A cancellation token</param>
public Task DeleteThreadAsync(CancellationToken cancellationToken)
{
return this._restContext.DeleteThreadModelAsync(this.Id, cancellationToken);
}
private async IAsyncEnumerable<IChatMessage> GetMessagesAsync(
IList<string> messageIds,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var messages = await this._restContext.GetMessagesAsync(this.Id, messageIds, cancellationToken).ConfigureAwait(false);
foreach (var message in messages)
{
yield return new ChatMessage(message);
}
}
/// <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;
}
}
@@ -0,0 +1,31 @@
// 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;
}
}
@@ -0,0 +1,54 @@
// 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; }
}
@@ -0,0 +1,91 @@
// 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>();
}
@@ -0,0 +1,45 @@
// 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; }
}
@@ -0,0 +1,56 @@
// 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; }
}
@@ -0,0 +1,123 @@
// 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 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>();
}
}
@@ -0,0 +1,42 @@
// 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>();
}
@@ -0,0 +1,139 @@
// 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;
}
}
@@ -0,0 +1,204 @@
// 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;
}
}
@@ -0,0 +1,48 @@
// 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;
}
}
@@ -0,0 +1,24 @@
// 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 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;
}
@@ -96,13 +96,7 @@ internal sealed class OpenApiDocumentParser : IOpenApiDocumentParser
/// <returns>OpenAPI document with downgraded document version.</returns>
private async Task<JsonObject> DowngradeDocumentVersionToSupportedOneAsync(Stream stream, CancellationToken cancellationToken)
{
var jsonObject = await ConvertContentToJsonAsync(stream, cancellationToken).ConfigureAwait(false);
if (jsonObject == null)
{
// The document is malformed.
throw new SKException("Parsing of OpenAPI document failed.");
}
var jsonObject = await ConvertContentToJsonAsync(stream, cancellationToken).ConfigureAwait(false) ?? throw new SKException("Parsing of OpenAPI document failed.");
if (!jsonObject.TryGetPropertyValue(OpenApiVersionPropertyName, out var propertyNode))
{
// The document is either malformed or has 2.x version that specifies document version in the 'swagger' property rather than in the 'openapi' one.
@@ -274,12 +268,7 @@ internal sealed class OpenApiDocumentParser : IOpenApiDocumentParser
return null;
}
var mediaType = s_supportedMediaTypes.FirstOrDefault(smt => requestBody.Content.ContainsKey(smt));
if (mediaType == null)
{
throw new SKException($"Neither of the media types of {operationId} is supported.");
}
var mediaType = s_supportedMediaTypes.FirstOrDefault(smt => requestBody.Content.ContainsKey(smt)) ?? throw new SKException($"Neither of the media types of {operationId} is supported.");
var mediaTypeMetadata = requestBody.Content[mediaType];
var payloadProperties = GetPayloadProperties(operationId, mediaTypeMetadata.Schema, mediaTypeMetadata.Schema?.Required ?? new HashSet<string>());
@@ -28,5 +28,6 @@
<InternalsVisibleTo Include="SemanticKernel.Extensions.UnitTests" />
<InternalsVisibleTo Include="Microsoft.SemanticKernel.Planners.Core.UnitTests" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" /> <!-- Moq -->
<InternalsVisibleTo Include="Microsoft.SemanticKernel.Experimental.Assistants" />
</ItemGroup>
</Project>
@@ -34,4 +34,8 @@
<ProjectReference Include="..\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.SemanticKernel.Experimental.Assistants" />
</ItemGroup>
</Project>