From 9d182b7effc017899daba55be2d7045d817e0700 Mon Sep 17 00:00:00 2001 From: Kevin BEAUGRAND Date: Sun, 10 Dec 2023 13:41:02 +0100 Subject: [PATCH] Initial code commit --- .github/CODEOWNERS | 6 + .github/dependabot.yaml | 17 ++ .github/pull_request_template.md | 17 ++ .github/workflows/build_tests.yml | 30 +++ .github/workflows/publish.yml | 33 +++ .gitignore | 2 + README.md | 91 ++++++- SemanticKernel.Assistants.sln | 53 ++++ nuget/NUGET.md | 23 ++ nuget/nuget-package.props | 49 ++++ .../01-mathematician/01-mathematician.csproj | 41 +++ .../01-mathematician/Assistants/Butler.yaml | 11 + .../Assistants/Mathematician.yaml | 16 ++ samples/01-mathematician/Program.cs | 48 ++++ samples/01-mathematician/appsettings.json | 5 + src/Assistants.Tests/Assistants/Auditor.yaml | 13 + src/Assistants.Tests/Assistants/Butler.yaml | 10 + .../Assistants/Mathematician.yaml | 16 ++ src/Assistants.Tests/HarnessTests.cs | 216 +++++++++++++++ .../SemanticKernel.Assistants.Tests.csproj | 58 ++++ src/Assistants.Tests/TestConfig.cs | 35 +++ src/Assistants.Tests/Usings.cs | 3 + src/Assistants.Tests/XunitLoggerProvider.cs | 91 +++++++ src/Assistants.Tests/testsettings.json | 5 + src/Assistants/Assistant.cs | 102 +++++++ src/Assistants/AssistantBuilder.cs | 253 ++++++++++++++++++ .../Extensions/KernelArgumentsExtensions.cs | 28 ++ src/Assistants/Extensions/KernelExtensions.cs | 55 ++++ src/Assistants/IAssistant.cs | 69 +++++ src/Assistants/IThread.cs | 30 +++ .../Models/AssistantExecutionSettings.cs | 17 ++ .../Models/AssistantInputParameters.cs | 14 + src/Assistants/Models/AssistantModel.cs | 24 ++ src/Assistants/RoomThread/IRoomThread.cs | 24 ++ .../RoomMeetingInstructions.handlebars | 13 + src/Assistants/RoomThread/RoomThread.cs | 96 +++++++ src/Assistants/RoomThread/SpectatorAgent.yaml | 10 + .../SemanticKernel.Assistants.csproj | 45 ++++ src/Assistants/Thread.cs | 216 +++++++++++++++ 39 files changed, 1883 insertions(+), 2 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yaml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/build_tests.yml create mode 100644 .github/workflows/publish.yml create mode 100644 SemanticKernel.Assistants.sln create mode 100644 nuget/NUGET.md create mode 100644 nuget/nuget-package.props create mode 100644 samples/01-mathematician/01-mathematician.csproj create mode 100644 samples/01-mathematician/Assistants/Butler.yaml create mode 100644 samples/01-mathematician/Assistants/Mathematician.yaml create mode 100644 samples/01-mathematician/Program.cs create mode 100644 samples/01-mathematician/appsettings.json create mode 100644 src/Assistants.Tests/Assistants/Auditor.yaml create mode 100644 src/Assistants.Tests/Assistants/Butler.yaml create mode 100644 src/Assistants.Tests/Assistants/Mathematician.yaml create mode 100644 src/Assistants.Tests/HarnessTests.cs create mode 100644 src/Assistants.Tests/SemanticKernel.Assistants.Tests.csproj create mode 100644 src/Assistants.Tests/TestConfig.cs create mode 100644 src/Assistants.Tests/Usings.cs create mode 100644 src/Assistants.Tests/XunitLoggerProvider.cs create mode 100644 src/Assistants.Tests/testsettings.json create mode 100644 src/Assistants/Assistant.cs create mode 100644 src/Assistants/AssistantBuilder.cs create mode 100644 src/Assistants/Extensions/KernelArgumentsExtensions.cs create mode 100644 src/Assistants/Extensions/KernelExtensions.cs create mode 100644 src/Assistants/IAssistant.cs create mode 100644 src/Assistants/IThread.cs create mode 100644 src/Assistants/Models/AssistantExecutionSettings.cs create mode 100644 src/Assistants/Models/AssistantInputParameters.cs create mode 100644 src/Assistants/Models/AssistantModel.cs create mode 100644 src/Assistants/RoomThread/IRoomThread.cs create mode 100644 src/Assistants/RoomThread/RoomMeetingInstructions.handlebars create mode 100644 src/Assistants/RoomThread/RoomThread.cs create mode 100644 src/Assistants/RoomThread/SpectatorAgent.yaml create mode 100644 src/Assistants/SemanticKernel.Assistants.csproj create mode 100644 src/Assistants/Thread.cs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..d9946d5 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence. + +* @kbeaugrand \ No newline at end of file diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..c57d324 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,17 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + # Maintain dependencies for NuGet packages + - package-ecosystem: "nuget" + directory: "/src/" + schedule: + interval: "daily" + ignore: + - dependency-name: "System.*" + update-types: ["version-update:semver-major"] + - dependency-name: "Microsoft.Extensions.*" + update-types: ["version-update:semver-major"] \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..2d8150d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,17 @@ +## Description + +What's new? + +- + +## What kind of change does this PR introduce? + +- [ ] Bugfix +- [ ] Feature +- [ ] Code style update (formatting, local variables) +- [ ] Refactoring (no functional changes, no api changes) +- [ ] Build related changes +- [ ] CI related changes +- [ ] Documentation content changes +- [ ] Tests +- [ ] Other \ No newline at end of file diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml new file mode 100644 index 0000000..e2ad85a --- /dev/null +++ b/.github/workflows/build_tests.yml @@ -0,0 +1,30 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: Build & Test + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + - name: Restore dependencies + run: dotnet restore + working-directory: ./src/Assistants.Tests/ + - name: Build + run: dotnet build --no-restore + working-directory: ./src/Assistants.Tests/ + - name: Test + run: dotnet test --no-build --verbosity normal + working-directory: ./src/Assistants.Tests/ \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..987b558 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: Create Release + +on: + release: + types: [published] + +jobs: + Publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + - name: Restore dependencies + run: dotnet restore + working-directory: ./src/Assistants/ + - name: Build + run: dotnet build --no-restore --configuration Release + working-directory: ./src/Assistants/ + - name: Pack + run: dotnet pack --configuration Release /p:Version=${{ github.event.release.tag_name }} + working-directory: ./src/Assistants/ + - name: Push to NuGet + run: | + dotnet nuget push **/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate + working-directory: ./src/Assistants/ + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8a30d25..7700772 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,5 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +/src/Assistants.Tests/testsettings.development.json +/samples/01-mathematician/appsettings.development.json diff --git a/README.md b/README.md index b6a7708..18ffd7e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,89 @@ -# SemanticKernel.Assistants -Microsoft Semantic Kernel Assistants +# Semantic Kernel - Assistants + +[![Build & Test](https://github.com/kbeaugrand/SemanticKernel.Assistants/actions/workflows/build_tests.yml/badge.svg)](https://github.com/kbeaugrand/SemanticKernel.Assistants/actions/workflows/build_test.yml) +[![Create Release](https://github.com/kbeaugrand/SemanticKernel.Assistants/actions/workflows/publish.yml/badge.svg)](https://github.com/kbeaugrand/SemanticKernel.Assistants/actions/workflows/publish.yml) +[![Version](https://img.shields.io/github/v/release/kbeaugrand/SemanticKernel.Assistants)](https://img.shields.io/github/v/release/kbeaugrand/SemanticKernel.Assistants) +[![License](https://img.shields.io/github/license/kbeaugrand/SemanticKernel.Assistants)](https://img.shields.io/github/v/release/kbeaugrand/SemanticKernel.Assistants) + +This is assistant proposal for the [Semantic Kernel](https://aka.ms/semantic-kernel). + +This enables the usage of assistants for the Semantic Kernel. + +It provides different scenarios for the usage of assistants such as: +- **Assistant with Semantic Kernel plugins** +- **Multi-Assistant conversation** + +## About Semantic Kernel + +**Semantic Kernel (SK)** is a lightweight SDK enabling integration of AI Large +Language Models (LLMs) with conventional programming languages. The SK +extensible programming model combines natural language **semantic functions**, +traditional code **native functions**, and **embeddings-based memory** unlocking +new potential and adding value to applications with AI. + +Semantic Kernel incorporates cutting-edge design patterns from the latest in AI +research. This enables developers to augment their applications with advanced +capabilities, such as prompt engineering, prompt chaining, retrieval-augmented +generation, contextual and long-term vectorized memory, embeddings, +summarization, zero or few-shot learning, semantic indexing, recursive +reasoning, intelligent planning, and access to external knowledge stores and +proprietary data. + +### Getting Started with Semantic Kernelâš¡ + +- Learn more at the [documentation site](https://aka.ms/SK-Docs). +- Join the [Discord community](https://aka.ms/SKDiscord). +- Follow the team on [Semantic Kernel blog](https://aka.ms/sk/blog). +- Check out the [GitHub repository](https://github.com/microsoft/semantic-kernel) for the latest updates. + +## Installation + +To install this memory store, you need to add the required nuget package to your project: + +```dotnetcli +dotnet add package SemanticKernel.Assistants --version 1.0.0-rc3 +``` + +## Usage + +1. Create you agent description file in yaml: + ```yaml + name: Mathematician + description: A mathematician that resolves given maths problems. + instructions: | + You are a mathematician. + Given a math problem, you must answer it with the best calculation formula. + No need to show your work, just give the answer to the math problem. + Use calculation results. + input_parameter: + default_value: "" + description: | + The word mathematics problem to solve in 2-3 sentences. + Make sure to include all the input variables needed along with their values and units otherwise the math function will not be able to solve it. + execution_settings: + planner: Handlebars + model: gpt-3.5-turbo + deployment_name: gpt-35-turbo-1106 + ``` +2. Instanciate your assistant in your code: + ```csharp + string azureOpenAIEndpoint = configuration["AzureOpenAIEndpoint"]!; + string azureOpenAIKey = configuration["AzureOpenAIAPIKey"]!; + + var mathematician = AssistantBuilder.FromTemplate("./Assistants/Mathematician.yaml", + azureOpenAIEndpoint, + azureOpenAIKey, + plugins: new List() + { + KernelPluginFactory.CreateFromObject(new MathPlugin(), "math") + }); + ``` +3. Create a new conversation thread with your assistant. + ```csharp + var thread = agent.CreateThread(); + await thread.InvokeAsync("Your ask to the assistant."); + ``` + +## License + +This project is licensed under the [MIT License](LICENSE). \ No newline at end of file diff --git a/SemanticKernel.Assistants.sln b/SemanticKernel.Assistants.sln new file mode 100644 index 0000000..fc1155e --- /dev/null +++ b/SemanticKernel.Assistants.sln @@ -0,0 +1,53 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34330.188 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel.Assistants", "src\Assistants\SemanticKernel.Assistants.csproj", "{6D530B82-6217-4A67-B22D-E5ACFAE4A511}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel.Assistants.Tests", "src\Assistants.Tests\SemanticKernel.Assistants.Tests.csproj", "{03C21161-E835-4857-A81A-C1727140E920}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{96B59E8F-BF38-4918-8312-63DA3363B20B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{803BA424-8745-4689-9C1D-72CA4384E6AC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "01-mathematician", "samples\01-mathematician\01-mathematician.csproj", "{BBC6C36F-DC43-4FD3-9706-ECA4738F8F57}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution files", "Solution files", "{324300B5-4DBA-4DF0-957C-75458CCF93CE}" + ProjectSection(SolutionItems) = preProject + nuget\nuget-package.props = nuget\nuget-package.props + nuget\NUGET.md = nuget\NUGET.md + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6D530B82-6217-4A67-B22D-E5ACFAE4A511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D530B82-6217-4A67-B22D-E5ACFAE4A511}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D530B82-6217-4A67-B22D-E5ACFAE4A511}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D530B82-6217-4A67-B22D-E5ACFAE4A511}.Release|Any CPU.Build.0 = Release|Any CPU + {03C21161-E835-4857-A81A-C1727140E920}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03C21161-E835-4857-A81A-C1727140E920}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03C21161-E835-4857-A81A-C1727140E920}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03C21161-E835-4857-A81A-C1727140E920}.Release|Any CPU.Build.0 = Release|Any CPU + {BBC6C36F-DC43-4FD3-9706-ECA4738F8F57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BBC6C36F-DC43-4FD3-9706-ECA4738F8F57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BBC6C36F-DC43-4FD3-9706-ECA4738F8F57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BBC6C36F-DC43-4FD3-9706-ECA4738F8F57}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6D530B82-6217-4A67-B22D-E5ACFAE4A511} = {96B59E8F-BF38-4918-8312-63DA3363B20B} + {03C21161-E835-4857-A81A-C1727140E920} = {96B59E8F-BF38-4918-8312-63DA3363B20B} + {BBC6C36F-DC43-4FD3-9706-ECA4738F8F57} = {803BA424-8745-4689-9C1D-72CA4384E6AC} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3252A8D5-644E-45F0-B096-AC8C2F0A15B4} + EndGlobalSection +EndGlobal diff --git a/nuget/NUGET.md b/nuget/NUGET.md new file mode 100644 index 0000000..07e84f2 --- /dev/null +++ b/nuget/NUGET.md @@ -0,0 +1,23 @@ +# Assistants for Semantic Kernel + +This enables the usage of assistants for the Semantic Kernel. + +It provides different scenarios for the usage of assistants such as: +- **Assistant with Semantic Kernel plugins** +- **Multi-Assistant conversation** + +## About Semantic Kernel + +**Semantic Kernel (SK)** is a lightweight SDK enabling integration of AI Large +Language Models (LLMs) with conventional programming languages. The SK +extensible programming model combines natural language **semantic functions**, +traditional code **native functions**, and **embeddings-based memory** unlocking +new potential and adding value to applications with AI. + +Semantic Kernel incorporates cutting-edge design patterns from the latest in AI +research. This enables developers to augment their applications with advanced +capabilities, such as prompt engineering, prompt chaining, retrieval-augmented +generation, contextual and long-term vectorized memory, embeddings, +summarization, zero or few-shot learning, semantic indexing, recursive +reasoning, intelligent planning, and access to external knowledge stores and +proprietary data. \ No newline at end of file diff --git a/nuget/nuget-package.props b/nuget/nuget-package.props new file mode 100644 index 0000000..cfca53a --- /dev/null +++ b/nuget/nuget-package.props @@ -0,0 +1,49 @@ + + + + 0.0.1-alpha + + Debug;Release;Publish + true + + + Kevin BEAUGRAND + + Semantic Kernel + Empowers app owners to integrate cutting-edge LLM technology quickly and easily into their apps. + AI, Artificial Intelligence, SDK + $(AssemblyName) + + + MIT + © Kevin BEAUGRAND. All rights reserved. + https://github.com/kbeaugrand/SemanticKernel.Connectors.Memory.SqlServer + https://github.com/kbeaugrand/SemanticKernel.Connectors.Memory.SqlServer + true + + + NUGET.md + + + true + snupkg + + + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + + + + + + + + + + + + + + true + + \ No newline at end of file diff --git a/samples/01-mathematician/01-mathematician.csproj b/samples/01-mathematician/01-mathematician.csproj new file mode 100644 index 0000000..afb33c3 --- /dev/null +++ b/samples/01-mathematician/01-mathematician.csproj @@ -0,0 +1,41 @@ + + + + Exe + net6.0 + _01_mathematician + enable + enable + SKEXP0050 + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/samples/01-mathematician/Assistants/Butler.yaml b/samples/01-mathematician/Assistants/Butler.yaml new file mode 100644 index 0000000..13924d1 --- /dev/null +++ b/samples/01-mathematician/Assistants/Butler.yaml @@ -0,0 +1,11 @@ +name: butler +description: A butler that helps humans. +instructions: | + You are a butler. + No need to explain further the internal process. + Be concise when answering. + Speak like Jarvis from Iron man. +execution_settings: + planner: Stepwise + model: gpt-3.5-turbo + deployment_name: gpt-35-turbo-1106 diff --git a/samples/01-mathematician/Assistants/Mathematician.yaml b/samples/01-mathematician/Assistants/Mathematician.yaml new file mode 100644 index 0000000..6f47b94 --- /dev/null +++ b/samples/01-mathematician/Assistants/Mathematician.yaml @@ -0,0 +1,16 @@ +name: Mathematician +description: A mathematician that resolves given maths problems. +instructions: | + You are a mathematician. + Given a math problem, you must answer it with the best calculation formula. + No need to show your work, just give the answer to the math problem. + Use calculation results. +input_parameter: + default_value: "" + description: | + The word mathematics problem to solve in 2-3 sentences. + Make sure to include all the input variables needed along with their values and units otherwise the math function will not be able to solve it. +execution_settings: + planner: Handlebars + model: gpt-3.5-turbo + deployment_name: gpt-35-turbo-1106 \ No newline at end of file diff --git a/samples/01-mathematician/Program.cs b/samples/01-mathematician/Program.cs new file mode 100644 index 0000000..1b28586 --- /dev/null +++ b/samples/01-mathematician/Program.cs @@ -0,0 +1,48 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Plugins.Core; +using SemanticKernel.Assistants; + +var configuration = new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddJsonFile("appsettings.json") + .AddJsonFile("appsettings.development.json", optional: true) + .Build(); + +using var loggerFactory = LoggerFactory.Create(logging => +{ + logging + .AddConsole(opts => + { + opts.FormatterName = "simple"; + }) + .ClearProviders() + .AddConfiguration(configuration.GetSection("Logging")); +}); + +string azureOpenAIEndpoint = configuration["AzureOpenAIEndpoint"]!; +string azureOpenAIKey = configuration["AzureOpenAIAPIKey"]!; + +var mathematician = AssistantBuilder.FromTemplate("./Assistants/Mathematician.yaml", + azureOpenAIEndpoint, + azureOpenAIKey, + plugins: new List() + { + KernelPluginFactory.CreateFromObject(new MathPlugin(), "math") + }); + +var agent = AssistantBuilder.FromTemplate("./Assistants/Butler.yaml", + azureOpenAIEndpoint, + azureOpenAIKey, + assistants: mathematician); + +var thread = agent.CreateThread(); + +while (true) +{ + Console.Write("User > "); + await thread.InvokeAsync(Console.ReadLine()); +} diff --git a/samples/01-mathematician/appsettings.json b/samples/01-mathematician/appsettings.json new file mode 100644 index 0000000..51d2e1b --- /dev/null +++ b/samples/01-mathematician/appsettings.json @@ -0,0 +1,5 @@ +{ + "AzureOpenAIEndpoint": "", + "AzureOpenAIAPIKey": "", + "AzureOpenAIDeploymentName": "gpt-35-turbo-1106" +} diff --git a/src/Assistants.Tests/Assistants/Auditor.yaml b/src/Assistants.Tests/Assistants/Auditor.yaml new file mode 100644 index 0000000..1bb5e76 --- /dev/null +++ b/src/Assistants.Tests/Assistants/Auditor.yaml @@ -0,0 +1,13 @@ +name: Auditor +description: An assistant that is in charge to verify the results of the agent. +instructions: | + You are an assistant that compare whether two agents give the same results + You have two inputs that represent an answer to the same question. + Taking the result of the question from each sentence. + Then compare the two results. + If the results are equal, return only "True", otherwise simply return "False". + DO NOT EXECUTE ANYTHING JUST COMPARE THEIR ANSWERS. +execution_settings: + planner: Handlebars + model: gpt-4 + deployment_name: gpt-4-1106-preview diff --git a/src/Assistants.Tests/Assistants/Butler.yaml b/src/Assistants.Tests/Assistants/Butler.yaml new file mode 100644 index 0000000..a198eca --- /dev/null +++ b/src/Assistants.Tests/Assistants/Butler.yaml @@ -0,0 +1,10 @@ +name: butler +description: A butler that helps humans. +instructions: | + Act as a butler. + No need to explain further the internal process. + Be concise when answering. +execution_settings: + planner: Stepwise + model: gpt-3.5-turbo + deployment_name: gpt-35-turbo-1106 diff --git a/src/Assistants.Tests/Assistants/Mathematician.yaml b/src/Assistants.Tests/Assistants/Mathematician.yaml new file mode 100644 index 0000000..7f9575b --- /dev/null +++ b/src/Assistants.Tests/Assistants/Mathematician.yaml @@ -0,0 +1,16 @@ +name: Mathematician +description: An assistant that answers math problems with a given user prompt. +instructions: | + You are a mathematician. + Given a math problem, you must answer it with the best calculation formula. + No need to show your work, just give the answer to the math problem. + Use calculation results. +input_parameter: + default_value: "" + description: | + The word mathematics problem to solve in 2-3 sentences. + Make sure to include all the input variables needed along with their values and units otherwise the math function will not be able to solve it. +execution_settings: + planner: Handlebars + model: gpt-3.5-turbo + deployment_name: gpt-35-turbo-1106 diff --git a/src/Assistants.Tests/HarnessTests.cs b/src/Assistants.Tests/HarnessTests.cs new file mode 100644 index 0000000..6719406 --- /dev/null +++ b/src/Assistants.Tests/HarnessTests.cs @@ -0,0 +1,216 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Plugins.Core; +using SemanticKernel.Assistants; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace SemanticKernel.Experimental.Agents.Tests; + + +public class HarnessTests +{ +#if DISABLEHOST + private const string SkipReason = "Harness only for local/dev environment"; +#else + private const string SkipReason = null; +#endif + + private readonly ITestOutputHelper _output; + + private readonly ILoggerFactory _loggerFactory; + + public HarnessTests(ITestOutputHelper output) + { + this._output = output; + + this._loggerFactory = LoggerFactory.Create(logging => + { + logging + .AddProvider(new XunitLoggerProvider(output)) + .AddConfiguration(TestConfig.Configuration.GetSection("Logging")); + }); + } + + [Fact(Skip = SkipReason)] + public async Task PoetTestAsync() + { + string azureOpenAIKey = TestConfig.AzureOpenAIAPIKey; + string azureOpenAIEndpoint = TestConfig.AzureOpenAIEndpoint; + string azureOpenAIChatCompletionDeployment = TestConfig.AzureOpenAIDeploymentName; + + var assistant = new AssistantBuilder() + .WithName("poet") + .WithDescription("An assistant that create sonnet poems.") + .WithInstructions("You are a poet that composes poems based on user input.\nCompose a sonnet inspired by the user input.") + .WithAzureOpenAIChatCompletion(azureOpenAIChatCompletionDeployment, azureOpenAIChatCompletionDeployment, azureOpenAIEndpoint, azureOpenAIKey) + .WithLoggerFactory(this._loggerFactory) + .Build(); + + var thread = assistant.CreateThread(); + + var response = await thread.InvokeAsync("Eggs are yummy and beautiful geometric gems.").ConfigureAwait(true); + } + + [Theory(Skip = SkipReason)] + [InlineData("What is the square root of 4?")] + [InlineData("If you start with $25,000 in the stock market and leave it to grow for 20 years at a 5% interest rate, how much would you have?")] + public async Task MathCalculationTestsAsync(string prompt) + { + string azureOpenAIKey = TestConfig.AzureOpenAIAPIKey; + string azureOpenAIEndpoint = TestConfig.AzureOpenAIEndpoint; + string azureOpenAIChatCompletionDeployment = TestConfig.AzureOpenAIDeploymentName; + + var mathematician = new AssistantBuilder() + .WithName("mathematician") + .WithDescription("An assistant that answers math problems.") + .WithInstructions("You are a mathematician.\n" + + "No need to show your work, just give the answer to the math problem.\n" + + "Use calculation results.") + .WithAzureOpenAIChatCompletion(azureOpenAIChatCompletionDeployment, azureOpenAIChatCompletionDeployment, azureOpenAIEndpoint, azureOpenAIKey) + .WithPlugin(KernelPluginFactory.CreateFromObject(new MathPlugin(), "math")) + .WithLoggerFactory(this._loggerFactory) + .Build(); + + var thread = mathematician.CreateThread(); + + var response = await thread.InvokeAsync(prompt).ConfigureAwait(true); + } + + [Fact(Skip = SkipReason)] + public async Task ButlerTestAsync() + { + string azureOpenAIKey = TestConfig.AzureOpenAIAPIKey; + string azureOpenAIEndpoint = TestConfig.AzureOpenAIEndpoint; + string azureOpenAIChatCompletionDeployment = TestConfig.AzureOpenAIDeploymentName; + + var mathematician = new AssistantBuilder() + .WithName("mathematician") + .WithDescription("An assistant that answers math problems with a given user prompt.") + .WithInstructions("You are a mathematician.\n" + + "No need to show your work, just give the answer to the math problem.\n" + + "Use calculation results.") + .WithAzureOpenAIChatCompletion(azureOpenAIChatCompletionDeployment, azureOpenAIChatCompletionDeployment, azureOpenAIEndpoint, azureOpenAIKey) + .WithPlugin(KernelPluginFactory.CreateFromObject(new MathPlugin(), "math")) + .WithInputParameter("The word mathematics problem to solve in 2-3 sentences.\n" + + "Make sure to include all the input variables needed along with their values and units otherwise the math function will not be able to solve it.") + .WithLoggerFactory(this._loggerFactory) + .Build(); + + var butler = new AssistantBuilder() + .WithName("alfred") + .WithDescription("An AI butler that helps humans.") + .WithInstructions("Act as a butler.\nNo need to explain further the internal process.\nBe concise when answering.") + .WithAzureOpenAIChatCompletion(azureOpenAIChatCompletionDeployment, azureOpenAIChatCompletionDeployment, azureOpenAIEndpoint, azureOpenAIKey) + .WithLoggerFactory(this._loggerFactory) + .WithAssistant(mathematician) + .Build(); + + var thread = butler.CreateThread(); + + var response = await thread.InvokeAsync("If I start with $25,000 in the stock market and leave it to grow for 20 years at a 5% interest rate, how much would I have?").ConfigureAwait(true); + } + + [Fact(Skip = SkipReason)] + public async Task FinancialAdvisorFromTemplateTestsAsync() + { + string azureOpenAIKey = TestConfig.AzureOpenAIAPIKey; + string azureOpenAIEndpoint = TestConfig.AzureOpenAIEndpoint; + string azureOpenAIChatCompletionDeployment = TestConfig.AzureOpenAIDeploymentName; + + var mathematician = AssistantBuilder.FromTemplate("./Assistants/Mathematician.yaml", + azureOpenAIEndpoint, + azureOpenAIKey, + new[] { + KernelPluginFactory.CreateFromObject(new MathPlugin(), "math") + }, + loggerFactory: this._loggerFactory); + + var butler = AssistantBuilder.FromTemplate("./Assistants/Butler.yaml", + azureOpenAIEndpoint, + azureOpenAIKey, + assistants: new[] { + mathematician + }, + loggerFactory: this._loggerFactory); + + var thread = butler.CreateThread(); + var question = "If I start with $25,000 in the stock market and leave it to grow for 20 years at a 5% interest rate, how much would I have?"; + + var result = await thread.InvokeAsync(question) + .ConfigureAwait(true); + + await this.AuditorTestsAsync(question, result, "If you start with $25,000 in the stock market and leave it to grow for 20 years at a 5% interest rate, the future value of the investment would be approximately $66,332.44.", true).ConfigureAwait(true); + + result = await thread.InvokeAsync("What if the rate is about 3.6%?").ConfigureAwait(true); + await this.AuditorTestsAsync(question + "\nWhat if the rate is about 3.6%?", result, "If you start with $25,000 in the stock market and leave it to grow for 20 years at a 3.6% interest rate, the future value of the investment would be approximately $50,714.85.", true).ConfigureAwait(true); + } + + [Theory(Skip = SkipReason)] + [InlineData("What is the square root of 4?", "square result is 2", "2 is the square of 4.", true)] + [InlineData("If I start with $25,000 in the stock market and leave it to grow for 20 years at a 5% interest rate, how much would I have?", "The future value of $25,000 invested at a 5% interest rate for 20 years would be approximately $66,332.44.", "If you start with $25,000 in the stock market and leave it to grow for 20 years at a 5% interest rate, the future value of the investment would be approximately $66,332.44.", true)] + [InlineData("If I start with $25,000 in the stock market and leave it to grow for 20 years at a 5% interest rate, how much would I have?", "If the interest rate is 3.6%, the future value of the $25,000 investment over 20 years would be approximately $47,688.04.", "If you start with $25,000 in the stock market and leave it to grow for 20 years at a 5% interest rate, the future value of the investment would be approximately $66,332.44.", false)] + public async Task AuditorTestsAsync( + string question, + string answer1, + string answer2, + bool equality) + { + string azureOpenAIKey = TestConfig.AzureOpenAIAPIKey; + string azureOpenAIEndpoint = TestConfig.AzureOpenAIEndpoint; + string azureOpenAIChatCompletionDeployment = TestConfig.AzureOpenAIDeploymentName; + + var verifier = AssistantBuilder.FromTemplate("./Assistants/Auditor.yaml", + azureOpenAIEndpoint, + azureOpenAIKey, + loggerFactory: this._loggerFactory); + + Assert.Equal(equality, bool.Parse(await verifier.CreateThread() + .InvokeAsync( + $"Question: {question}\n" + + $"Answer 1: {answer1}\n" + + $"Answer 2: {answer2}") + .ConfigureAwait(true))); + } + + + [Theory(Skip = SkipReason)] + [InlineData("What is the square of 16?")] + [InlineData("What is the square root of 16?")] + [InlineData("Help me to find the how I will have in my account in 2 years.")] + + + public async Task RoomMeetingSampleTestAsync(string prompt) + { + string azureOpenAIKey = TestConfig.AzureOpenAIAPIKey; + string azureOpenAIEndpoint = TestConfig.AzureOpenAIEndpoint; + + var mathematician = AssistantBuilder.FromTemplate("./Assistants/Mathematician.yaml", + azureOpenAIEndpoint, + azureOpenAIKey, + new[] { + KernelPluginFactory.CreateFromObject(new MathPlugin(), "math") + }); + + var butler = AssistantBuilder.FromTemplate("./Assistants/Butler.yaml", + azureOpenAIEndpoint, + azureOpenAIKey); + + var logger = this._loggerFactory.CreateLogger("Tests"); + + var thread = AssistantBuilder.CreateRoomThread(butler, mathematician); + + thread.OnMessageReceived += (object? sender, string message) => + { + var agent = sender as IAssistant; + this._output.WriteLine($"{agent.Name} > {message}"); + }; + + this._output.WriteLine($"User > {prompt}"); + + await thread.AddUserMessageAsync(prompt) + .ConfigureAwait(true); + } +} diff --git a/src/Assistants.Tests/SemanticKernel.Assistants.Tests.csproj b/src/Assistants.Tests/SemanticKernel.Assistants.Tests.csproj new file mode 100644 index 0000000..c8dacbe --- /dev/null +++ b/src/Assistants.Tests/SemanticKernel.Assistants.Tests.csproj @@ -0,0 +1,58 @@ + + + + SemanticKernel.Assistants.Tests + SemanticKernel.Assistants.Tests + net6.0 + LatestMajor + true + enable + disable + false + CS1591;SKEXP0001;SKEXP0050;SKEXP0060;SKEXP0061 + + + + + + + + + + + + + + + + + all + + + all + + + + + + + + + + PreserveNewest + + + Always + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/src/Assistants.Tests/TestConfig.cs b/src/Assistants.Tests/TestConfig.cs new file mode 100644 index 0000000..5de8962 --- /dev/null +++ b/src/Assistants.Tests/TestConfig.cs @@ -0,0 +1,35 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Xunit.Sdk; + +namespace SemanticKernel.Experimental.Agents.Tests; + +internal static class TestConfig +{ + public static IConfiguration Configuration { get; } = CreateConfiguration(); + + public static string AzureOpenAIEndpoint => + Configuration.GetValue("AzureOpenAIEndpoint") ?? + throw new TestClassException("Missing Azure OpenAI Endpoint."); + + public static string AzureOpenAIAPIKey => + Configuration.GetValue("AzureOpenAIAPIKey") ?? + throw new TestClassException("Missing Azure OpenAI API Key."); + + public static string AzureOpenAIDeploymentName => + Configuration.GetValue("AzureOpenAIDeploymentName") ?? + throw new TestClassException("Missing Azure OpenAI deployment name."); + + private static IConfiguration CreateConfiguration() + { + return + new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddJsonFile("testsettings.json") + .AddJsonFile("testsettings.development.json", optional: true) + .AddUserSecrets(Assembly.GetExecutingAssembly()) + .Build(); + } +} diff --git a/src/Assistants.Tests/Usings.cs b/src/Assistants.Tests/Usings.cs new file mode 100644 index 0000000..56b4d6d --- /dev/null +++ b/src/Assistants.Tests/Usings.cs @@ -0,0 +1,3 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +global using Xunit; \ No newline at end of file diff --git a/src/Assistants.Tests/XunitLoggerProvider.cs b/src/Assistants.Tests/XunitLoggerProvider.cs new file mode 100644 index 0000000..50b228d --- /dev/null +++ b/src/Assistants.Tests/XunitLoggerProvider.cs @@ -0,0 +1,91 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using System; +using System.Linq; +using System.Text; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace SemanticKernel.Experimental.Agents.Tests; + +/// +/// A logger that writes to the Xunit test output +/// +internal class XunitLoggerProvider : ILoggerProvider +{ + private readonly ITestOutputHelper _output; + private readonly LogLevel _minLevel; + private readonly DateTimeOffset? _logStart; + + public XunitLoggerProvider(ITestOutputHelper output) + : this(output, LogLevel.Trace) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel) + : this(output, minLevel, null) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) + { + _output = output; + _minLevel = minLevel; + _logStart = logStart; + } + + public ILogger CreateLogger(string categoryName) + { + return new XunitLogger(_output, categoryName, _minLevel, _logStart); + } + + public void Dispose() + { + } +} + +internal class XunitLogger : ILogger +{ + private static readonly string[] s_newLineChars = new[] { Environment.NewLine }; + private readonly string _category; + private readonly LogLevel _minLogLevel; + private readonly ITestOutputHelper _output; + private DateTimeOffset? _logStart; + + public XunitLogger(ITestOutputHelper output, string category, LogLevel minLogLevel, DateTimeOffset? logStart) + { + _minLogLevel = minLogLevel; + _category = category; + _output = output; + _logStart = logStart; + } + + public void Log( + LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + try + { + _output.WriteLine(formatter(state, exception)); + } + catch (Exception) + { + // We could fail because we're on a background thread and our captured ITestOutputHelper is + // busted (if the test "completed" before the background thread fired). + // So, ignore this. There isn't really anything we can do but hope the + // caller has additional loggers registered + } + } + + public bool IsEnabled(LogLevel logLevel) + => logLevel >= _minLogLevel; + + public IDisposable BeginScope(TState state) + => new NullScope(); + + private class NullScope : IDisposable + { + public void Dispose() + { + } + } +} diff --git a/src/Assistants.Tests/testsettings.json b/src/Assistants.Tests/testsettings.json new file mode 100644 index 0000000..51d2e1b --- /dev/null +++ b/src/Assistants.Tests/testsettings.json @@ -0,0 +1,5 @@ +{ + "AzureOpenAIEndpoint": "", + "AzureOpenAIAPIKey": "", + "AzureOpenAIDeploymentName": "gpt-35-turbo-1106" +} diff --git a/src/Assistants/Assistant.cs b/src/Assistants/Assistant.cs new file mode 100644 index 0000000..f382f05 --- /dev/null +++ b/src/Assistants/Assistant.cs @@ -0,0 +1,102 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI.ChatCompletion; +using SemanticKernel.Assistants.Models; +using System.Collections.Generic; + +namespace SemanticKernel.Assistants; + +/// +/// Represents an agent. +/// +internal class Assistant : IAssistant +{ + /// + /// The agent model. + /// + private readonly AssistantModel _model; + + /// + /// The kernel. + /// + private readonly Kernel _kernel; + + /// + /// Gets the agent's name. + /// + public string? Name => this._model.Name; + + /// + /// Gets the agent's description. + /// + public string? Description => this._model.Description; + + /// + /// Gets the agent's instructions. + /// + public string Instructions => this._model.Instructions; + + /// + /// Gets the tools defined for run execution. + /// + public KernelPluginCollection Plugins => this._kernel.Plugins; + + /// + /// Gets the kernel. + /// + Kernel IAssistant.Kernel => this._kernel; + + /// + /// Gets the chat completion service. + /// + IChatCompletionService IAssistant.ChatCompletion => this._kernel.Services.GetService()!; + + /// + /// Gets the assistant threads. + /// + Dictionary IAssistant.AssistantThreads { get; } = new Dictionary(); + + /// + /// Gets the assistant model. + /// + AssistantModel IAssistant.AssistantModel => this._model; + + /// + /// Gets the planner. + /// + public string Planner => this._model.ExecutionSettings.Planner!; + + /// + /// Initializes a new instance of the class. + /// + /// The model + /// The kernel + public Assistant(AssistantModel model, + Kernel kernel) + { + this._model = model; + this._kernel = kernel; + } + + /// + /// Create a new conversable thread. + /// + /// + public IThread CreateThread() + { + return new Thread(this); + } + + /// + /// Create a new conversable thread using actual kernel arguments. + /// + /// The agent that is creating a thread with this agent. + /// The actual kernel parameters. + /// + IThread IAssistant.CreateThread(IAssistant initatedAgent, Dictionary arguments) + { + return new Thread(this, initatedAgent.Name!, arguments); + } +} diff --git a/src/Assistants/AssistantBuilder.cs b/src/Assistants/AssistantBuilder.cs new file mode 100644 index 0000000..0c46ef9 --- /dev/null +++ b/src/Assistants/AssistantBuilder.cs @@ -0,0 +1,253 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel; +using SemanticKernel.Assistants.Extensions; +using SemanticKernel.Assistants.Models; +using SemanticKernel.Assistants.RoomThread; +using System.Collections.Generic; +using System.IO; +using YamlDotNet.Serialization; + +namespace SemanticKernel.Assistants; + +/// +/// Fluent builder for initializing an instance. +/// +public partial class AssistantBuilder +{ + /// + /// The agent model. + /// + private readonly AssistantModel _model; + + /// + /// The agent's assistants. + /// + private readonly List _assistants; + + /// + /// The agent's plugins. + /// + private readonly List _plugins; + + /// + /// The logger factory. + /// + private ILoggerFactory _loggerFactory = NullLoggerFactory.Instance; + + /// + /// The kernel builder. + /// + private readonly KernelBuilder _kernelBuilder; + + /// + /// Initializes a new instance of the class. + /// + public AssistantBuilder() + { + this._model = new AssistantModel(); + this._assistants = new List(); + this._kernelBuilder = new KernelBuilder(); + this._plugins = new List(); + } + + /// + /// Builds the agent. + /// + /// + /// + public IAssistant Build() + { + var kernel = this._kernelBuilder.Build(); + + var agent = new Assistant(this._model, kernel); + + foreach (var item in this._plugins) + { + kernel.Plugins.Add(item); + } + + foreach (var item in this._assistants) + { + kernel.ImportPluginFromAgent(agent, item); + } + + return agent; + } + + /// + /// Defines the agent's name. + /// + /// The agent's name. + /// + public AssistantBuilder WithName(string name) + { + this._model.Name = name; + return this; + } + + /// + /// Defines the agent's description. + /// + /// The description. + /// + public AssistantBuilder WithDescription(string description) + { + this._model.Description = description; + return this; + } + + /// + /// Defines the agent's instructions. + /// + /// The instructions. + /// + public AssistantBuilder WithInstructions(string instructions) + { + this._model.Instructions = instructions; + return this; + } + + /// + /// Define the Azure OpenAI chat completion service (required). + /// + /// instance for fluid expression. + public AssistantBuilder WithAzureOpenAIChatCompletion(string deploymentName, string model, string endpoint, string apiKey) + { + this._model.ExecutionSettings.DeploymentName = deploymentName; + this._model.ExecutionSettings.Model = model; + + this._kernelBuilder.AddAzureOpenAIChatCompletion(deploymentName, model, endpoint, apiKey); + this._kernelBuilder.AddAzureOpenAITextGeneration(deploymentName, model, endpoint, apiKey); + return this; + } + + /// + /// Adds a plugin to the agent. + /// + /// + /// + public AssistantBuilder WithPlugin(IKernelPlugin plugin) + { + this._plugins.Add(plugin); + return this; + } + + /// + /// Adds the agent's collaborative assistant. + /// + /// The assistant. + /// + public AssistantBuilder WithAssistant(IAssistant assistant) + { + this._assistants.Add(assistant); + + return this; + } + + /// + /// Defines the agent's planner. + /// + /// The agent's planner name. + /// + public AssistantBuilder WithPlanner(string plannerName) + { + this._model.ExecutionSettings.Planner = plannerName; + return this; + } + + /// + /// Sets the logger factory to use. + /// + /// The logger factory. + public AssistantBuilder WithLoggerFactory(ILoggerFactory loggerFactory) + { + this._loggerFactory = loggerFactory; + this._kernelBuilder.Services.AddSingleton(loggerFactory); + + return this; + } + + /// + /// Defines the agent's input parameter. + /// + /// The input parameter. + /// + public AssistantBuilder WithInputParameter(string description, string defaultValue = "") + { + this._model.Input = new AssistantInputParameter + { + DefaultValue = defaultValue, + Description = description, + }; + return this; + } + + /// + /// Creates a new agent from a yaml template. + /// + /// The yaml definition file path. + /// The Azure OpenAI endpoint. + /// The Azure OpenAI key. + /// The plugins. + /// The assistants. + /// The logger factory instance. + /// + public static IAssistant FromTemplate( + string definitionPath, + string azureOpenAIEndpoint, + string azureOpenAIKey, + IEnumerable? plugins = null, + ILoggerFactory? loggerFactory = null, + params IAssistant[] assistants) + { + var deserializer = new DeserializerBuilder().Build(); + var yamlContent = File.ReadAllText(definitionPath); + + var agentModel = deserializer.Deserialize(yamlContent); + + var agentBuilder = new AssistantBuilder() + .WithName(agentModel.Name!.Trim()) + .WithDescription(agentModel.Description!.Trim()) + .WithInstructions(agentModel.Instructions.Trim()) + .WithPlanner(agentModel.ExecutionSettings.Planner?.Trim()) + .WithInputParameter(agentModel.Input.Description?.Trim()!, agentModel.Input.DefaultValue?.Trim()!) + .WithAzureOpenAIChatCompletion(agentModel.ExecutionSettings.DeploymentName!, agentModel.ExecutionSettings.Model!, azureOpenAIEndpoint, azureOpenAIKey); + + if (plugins is not null) + { + foreach (var plugin in plugins) + { + agentBuilder.WithPlugin(plugin); + } + } + + if (assistants is not null) + { + foreach (var assistant in assistants) + { + agentBuilder.WithAssistant(assistant); + } + } + + if (loggerFactory is not null) + { + agentBuilder.WithLoggerFactory(loggerFactory); + } + + return agentBuilder.Build(); + } + + /// + /// Creates a new room thread for collaborative agents. + /// + /// The collaborative agents. + /// + public static IRoomThread CreateRoomThread(params IAssistant[] agents) + { + return new RoomThread.RoomThread(agents); + } +} diff --git a/src/Assistants/Extensions/KernelArgumentsExtensions.cs b/src/Assistants/Extensions/KernelArgumentsExtensions.cs new file mode 100644 index 0000000..fc115d2 --- /dev/null +++ b/src/Assistants/Extensions/KernelArgumentsExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using Microsoft.SemanticKernel; +using System.Collections.Generic; + +namespace SemanticKernel.Assistants.Extensions; + +/// +/// Extensions for . +/// +internal static class KernelArgumentsExtensions +{ + /// + /// Converts the to a dictionary. + /// + /// The Kernel arguments to convert. + /// + internal static Dictionary ToDictionary(this KernelArguments args) + { + var dictionary = new Dictionary(); + foreach (var arg in args) + { + dictionary.Add(arg.Key, arg.Value); + } + + return dictionary; + } +} diff --git a/src/Assistants/Extensions/KernelExtensions.cs b/src/Assistants/Extensions/KernelExtensions.cs new file mode 100644 index 0000000..2a094d3 --- /dev/null +++ b/src/Assistants/Extensions/KernelExtensions.cs @@ -0,0 +1,55 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using Microsoft.SemanticKernel; +using System; +using System.Linq; + +namespace SemanticKernel.Assistants.Extensions; + +/// +/// Extensions for . +/// +internal static class KernelExtensions +{ + /// + /// Imports the agent's plugin into the kernel. + /// + /// The Kernel instance. + /// The Agent to import. + /// The instance. + public static void ImportPluginFromAgent(this Kernel kernel, IAssistant agent, IAssistant otherAssistant) + { + var agentConversationPlugin = new KernelPlugin(otherAssistant.Name!, otherAssistant.Description!); + + agentConversationPlugin.AddFunctionFromMethod(async (string input, KernelArguments args) => + { + if (!agent.AssistantThreads.TryGetValue(otherAssistant, out var thread)) + { + thread = otherAssistant.CreateThread(); + agent.AssistantThreads.Add(otherAssistant, thread); + } + + return await thread.InvokeAsync(input).ConfigureAwait(false); + }, + functionName: "Ask", + description: otherAssistant.Description, + parameters: new[] + { + new KernelParameterMetadata("input") + + { + IsRequired = true, + ParameterType = typeof(string), + DefaultValue = otherAssistant.AssistantModel.Input.DefaultValue, + Description = otherAssistant.AssistantModel.Input.Description + } + }, returnParameter: new() + { + ParameterType = typeof(string), + Description = "The response from the assistant." + }, + loggerFactory: kernel.LoggerFactory); + + kernel.Plugins.Add(agentConversationPlugin); + } +} diff --git a/src/Assistants/IAssistant.cs b/src/Assistants/IAssistant.cs new file mode 100644 index 0000000..4effd07 --- /dev/null +++ b/src/Assistants/IAssistant.cs @@ -0,0 +1,69 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using System.Collections.Generic; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI.ChatCompletion; +using SemanticKernel.Assistants.Models; + +namespace SemanticKernel.Assistants; + +/// +/// Interface for an agent that can call the model and use tools. +/// +public interface IAssistant +{ + /// + /// Gets the agent's name. + /// + public string? Name { get; } + + /// + /// Gets the agent's description. + /// + public string? Description { get; } + + /// + /// Gets the agent's instructions. + /// + public string Instructions { get; } + + /// + /// Gets the planner. + /// + public string Planner { get; } + + /// + /// Tools defined for run execution. + /// + public KernelPluginCollection Plugins { get; } + + /// + /// A semantic-kernel instance associated with the assistant. + /// + internal Kernel Kernel { get; } + + /// + /// The chat completion service. + /// + internal IChatCompletionService ChatCompletion { get; } + + /// + /// Gets the agent threads. + /// + internal Dictionary AssistantThreads { get; } + + /// + /// Gets the assistant model. + /// + internal AssistantModel AssistantModel { get; } + + /// + /// Create a new conversable thread. + /// + public IThread CreateThread(); + + /// + /// Create a new conversable thread using actual kernel arguments. + /// + internal IThread CreateThread(IAssistant initatedAgent, Dictionary arguments); +} diff --git a/src/Assistants/IThread.cs b/src/Assistants/IThread.cs new file mode 100644 index 0000000..1c886a9 --- /dev/null +++ b/src/Assistants/IThread.cs @@ -0,0 +1,30 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using Microsoft.SemanticKernel.AI.ChatCompletion; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SemanticKernel.Assistants; + +/// +/// Interface for a conversable thread. +/// +public interface IThread +{ + /// + /// Invokes the thread. + /// + /// + Task InvokeAsync(string userMessage); + + /// + /// Adds a system message. + /// + /// The message to add. + void AddSystemMessage(string message); + + /// + /// Gets the chat messages. + /// + IReadOnlyList ChatMessages { get; } +} diff --git a/src/Assistants/Models/AssistantExecutionSettings.cs b/src/Assistants/Models/AssistantExecutionSettings.cs new file mode 100644 index 0000000..bdcc185 --- /dev/null +++ b/src/Assistants/Models/AssistantExecutionSettings.cs @@ -0,0 +1,17 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using YamlDotNet.Serialization; + +namespace SemanticKernel.Assistants.Models; + +internal class AssistantExecutionSettings +{ + [YamlMember(Alias = "planner")] + public string? Planner { get; set; } + + [YamlMember(Alias = "model")] + public string? Model { get; set; } + + [YamlMember(Alias = "deployment_name")] + public string? DeploymentName { get; set; } +} diff --git a/src/Assistants/Models/AssistantInputParameters.cs b/src/Assistants/Models/AssistantInputParameters.cs new file mode 100644 index 0000000..0fc03d2 --- /dev/null +++ b/src/Assistants/Models/AssistantInputParameters.cs @@ -0,0 +1,14 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using YamlDotNet.Serialization; + +namespace SemanticKernel.Assistants.Models; + +internal class AssistantInputParameter +{ + [YamlMember(Alias = "description")] + public string? Description { get; set; } + + [YamlMember(Alias = "default_value")] + public string? DefaultValue { get; set; } +} diff --git a/src/Assistants/Models/AssistantModel.cs b/src/Assistants/Models/AssistantModel.cs new file mode 100644 index 0000000..479f3e5 --- /dev/null +++ b/src/Assistants/Models/AssistantModel.cs @@ -0,0 +1,24 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using SemanticKernel.Assistants.Models; +using YamlDotNet.Serialization; + +namespace SemanticKernel.Assistants.Models; + +internal class AssistantModel +{ + [YamlMember(Alias = "name")] + public string? Name { get; set; } + + [YamlMember(Alias = "description")] + public string? Description { get; set; } + + [YamlMember(Alias = "instructions")] + public string Instructions { get; set; } = string.Empty; + + [YamlMember(Alias = "execution_settings")] + public AssistantExecutionSettings ExecutionSettings { get; set; } = new(); + + [YamlMember(Alias = "input_parameter")] + public AssistantInputParameter Input { get; set; } = new(); +} diff --git a/src/Assistants/RoomThread/IRoomThread.cs b/src/Assistants/RoomThread/IRoomThread.cs new file mode 100644 index 0000000..ab871c2 --- /dev/null +++ b/src/Assistants/RoomThread/IRoomThread.cs @@ -0,0 +1,24 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using System; +using System.Threading.Tasks; + +namespace SemanticKernel.Assistants.RoomThread; + +/// +/// Interface representing a room thread. +/// +public interface IRoomThread +{ + /// + /// Adds the user message to the discussion. + /// + /// The user message. + /// + Task AddUserMessageAsync(string message); + + /// + /// Event produced when an agent sends a message. + /// + event EventHandler? OnMessageReceived; +} diff --git a/src/Assistants/RoomThread/RoomMeetingInstructions.handlebars b/src/Assistants/RoomThread/RoomMeetingInstructions.handlebars new file mode 100644 index 0000000..c99b2f7 --- /dev/null +++ b/src/Assistants/RoomThread/RoomMeetingInstructions.handlebars @@ -0,0 +1,13 @@ +## Participants +Here are each participants of this discussion: +{{#each participants}} +- {{Name}}: {{Description}} +{{/each}} + +Note that a human user is also a participant of this discussion. +You act as {{agent.Name}}. + +## Instructions + +You are an AI assistant that can collaborate with other AI assistant and user to solve a problem. +If it seems that the last message was not addressed to you or you cannot assist, send [silence] as a reply. \ No newline at end of file diff --git a/src/Assistants/RoomThread/RoomThread.cs b/src/Assistants/RoomThread/RoomThread.cs new file mode 100644 index 0000000..40b32d6 --- /dev/null +++ b/src/Assistants/RoomThread/RoomThread.cs @@ -0,0 +1,96 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using HandlebarsDotNet; +using Microsoft.SemanticKernel.AI.ChatCompletion; + +namespace SemanticKernel.Assistants.RoomThread; + +internal class RoomThread : IRoomThread +{ + public IReadOnlyList ChatMessages => throw new NotImplementedException(); + + private readonly Dictionary _assistantThreads = new(); + + public event EventHandler? OnMessageReceived; + + internal RoomThread(IEnumerable agents) + { + this._assistantThreads = agents.ToDictionary(agent => agent, agent => + { + var thread = agent.CreateThread(); + thread.AddSystemMessage(this.GetRoomInstructions()(new + { + agent, + participants = agents + })); + + return thread; + }); + + this.OnMessageReceived += (sender, message) => + { + var agent = sender as IAssistant; + + this.DispatchMessageRecievedAsync(agent!.Name!, message); // TODO fix to run it synchronously + }; + } + + public async Task AddUserMessageAsync(string message) + { + await this.DispatchMessageRecievedAsync("User", message).ConfigureAwait(false); + } + + /// + /// Dispatches the message to all recipients. + /// + /// The sender of the message (can be the agent name or "User"). + /// The message.. + /// + private async Task DispatchMessageRecievedAsync(string sender, string message) + { + await Task.WhenAll(this._assistantThreads + .Where(c => c.Key.Name != sender) + .Select(async assistantThread => + { + var response = await assistantThread.Value.InvokeAsync($"{sender} > {message}") + .ConfigureAwait(false); + + if (response.Equals("[silence]", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + this.OnMessageReceived!(assistantThread.Key, response); + })).ConfigureAwait(false); + } + + private HandlebarsTemplate GetRoomInstructions() + { + var roomInstructionTemplate = this.ReadManifestResource("RoomMeetingInstructions.handlebars"); + + IHandlebars handlebarsInstance = Handlebars.Create( + new HandlebarsConfiguration + { + NoEscape = true + }); + + var template = handlebarsInstance.Compile(roomInstructionTemplate); + + return template; + } + + private string ReadManifestResource(string resourceName) + { + var promptStream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(RoomThread).Namespace}.{resourceName}")!; + + using var reader = new StreamReader(promptStream); + + return reader.ReadToEnd(); + } +} diff --git a/src/Assistants/RoomThread/SpectatorAgent.yaml b/src/Assistants/RoomThread/SpectatorAgent.yaml new file mode 100644 index 0000000..0b72b98 --- /dev/null +++ b/src/Assistants/RoomThread/SpectatorAgent.yaml @@ -0,0 +1,10 @@ +name: Auditor +description: A spectator of this meeting. +instructions: | + You are a spectator of a conversation between agents. + You have the access to all the conversation. + Regarding this conversation, you have to decide if this conversation is ended or not. + If the conversation is supposed to end, return only "True", otherwise simply return "False". +execution_settings: + model: gpt-4 + deployment_name: gpt-4-1106-preview diff --git a/src/Assistants/SemanticKernel.Assistants.csproj b/src/Assistants/SemanticKernel.Assistants.csproj new file mode 100644 index 0000000..fd4bcbf --- /dev/null +++ b/src/Assistants/SemanticKernel.Assistants.csproj @@ -0,0 +1,45 @@ + + + + + SemanticKernel.Assistants + SemanticKernel.Assistants + netstandard2.0 + Latest + 1701;1702;SKEXP0060;SKEXP0061 + + + + + + + + + + + + + + + + + + + + + + + + + + Semantic Kernel Assistants + + This enables the usage of assistants for the Semantic Kernel. + + It provides different scenarios for the usage of assistants such as: + - **Assistant with Semantic Kernel plugins** + - **Multi-Assistant conversation** + + + + diff --git a/src/Assistants/Thread.cs b/src/Assistants/Thread.cs new file mode 100644 index 0000000..946f4c4 --- /dev/null +++ b/src/Assistants/Thread.cs @@ -0,0 +1,216 @@ +// Copyright (c) Kevin BEAUGRAND. All rights reserved. + +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.SemanticKernel.Planning; +using Microsoft.SemanticKernel.Planning.Handlebars; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SemanticKernel.Assistants; + +/// +/// Represents a thread of conversation with an agent. +/// +public class Thread : IThread +{ + /// + /// The agent. + /// + private readonly IAssistant _agent; + + /// + /// The chat history of this thread. + /// + private readonly ChatHistory _chatHistory; + + /// + /// The prompt to use for extracting the user intent. + /// + private const string SystemIntentExtractionPrompt = + "Rewrite the last messages to reflect the user's intents, taking into consideration the provided chat history. " + + "The output should be rewritten sentences that describes the user's intent and is understandable outside of the context of the chat history, in a way that will be useful for executing an action. " + + "Do not try to find an answer, just extract the user intent and all needed information to execute the goal."; + + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// The arguments to pass to the agent. + /// + private readonly Dictionary _arguments; + + /// + /// The name of the caller. + /// + private readonly string _callerName; + + /// + /// Gets the chat messages. + /// + public IReadOnlyList ChatMessages => this._chatHistory; + + /// + /// Initializes a new instance of the class. + /// + /// The agent. + /// The caller name. + /// The arguments to pass. + internal Thread(IAssistant agent, + string callerName = "User", + Dictionary arguments = null) + { + this._logger = agent.Kernel.LoggerFactory.CreateLogger(); + this._agent = agent; + this._callerName = callerName; + this._arguments = arguments ?? new Dictionary(); + this._chatHistory = new ChatHistory(this._agent.Description!); + + this._chatHistory.AddSystemMessage(this._agent.Instructions); + } + + /// + /// Invoke the agent completion. + /// + /// + public async Task InvokeAsync(string userMessage) + { + this._logger.LogInformation($"{this._callerName} > {userMessage}"); + + await this.ExecutePlannerIfNeededAsync(userMessage).ConfigureAwait(false); + + this._chatHistory.AddUserMessage(userMessage); + + var agentAnswer = await this._agent.ChatCompletion.GetChatMessageContentAsync(this._chatHistory) + .ConfigureAwait(false); + + this._chatHistory.Add(agentAnswer); + this._logger.LogInformation(message: $"{this._agent.Name!} > {agentAnswer.Content}"); + + return agentAnswer.Content; + } + + /// + /// Adds the system message to the chat history. + /// + /// The message to add. + public void AddSystemMessage(string message) + { + this._chatHistory.AddSystemMessage(message); + } + + /// + /// Extracts the user intent from the chat history. + /// + /// The user message. + /// + private async Task ExtractUserIntentAsync(string userMessage) + { + var chat = new ChatHistory(SystemIntentExtractionPrompt); + + foreach (var item in this._chatHistory) + { + if (item.Role == AuthorRole.User) + { + chat.AddUserMessage(item.Content); + } + else if (item.Role == AuthorRole.Assistant) + { + chat.AddAssistantMessage(item.Content); + } + } + + chat.AddUserMessage(userMessage); + + var chatResult = await this._agent.ChatCompletion.GetChatMessageContentAsync(chat).ConfigureAwait(false); + + return chatResult.Content; + } + + private async Task ExecutePlannerIfNeededAsync(string userMessage) + { + if (string.IsNullOrEmpty(this._agent.Planner)) + { + return; + } + + var userIntent = await this.ExtractUserIntentAsync(userMessage) + .ConfigureAwait(false); + + var result = await this.ExecutePlannerAsync(userIntent).ConfigureAwait(false); + + this._chatHistory.AddFunctionMessage(result!.Trim(), this._agent.Name!); + } + + private async Task ExecutePlannerAsync(string userIntent) + { + var goal = $"{this._agent.Instructions}\n" + + $"Given the following context, accomplish the user intent.\n" + + $"{userIntent}"; + + switch (this._agent.Planner) + { + case "Handlebars": + return await this.ExecuteHandleBarsPlannerAsync(goal).ConfigureAwait(false); + case "Stepwise": + return await this.ExecuteStepwisePlannerAsync(goal).ConfigureAwait(false); + default: + throw new NotImplementedException($"Planner {this._agent.Planner} is not implemented."); + } + } + + private async Task ExecuteHandleBarsPlannerAsync(string goal, int maxTries = 3) + { + HandlebarsPlan? lastPlan = null; + Exception? lastError = null; + + while (maxTries > 0) + { + try + { + var planner = new HandlebarsPlanner(new() + { + LastPlan = lastPlan, // Pass in the last plan in case we want to try again + LastError = lastError?.Message // Pass in the last error to avoid trying the same thing again + }); + + var plan = await planner.CreatePlanAsync(this._agent.Kernel, goal).ConfigureAwait(false); + lastPlan = plan; + + var result = plan.Invoke(this._agent.Kernel, new KernelArguments(this._arguments)); + + return result; + } + catch (Exception e) + { + // If we get an error, try again + lastError = e; + this._logger.LogWarning(e.Message); + } + maxTries--; + } + + this._logger.LogError(lastError!, lastError!.Message); + this._logger.LogError(lastPlan!.ToString()); + + throw lastError; + } + + private async Task ExecuteStepwisePlannerAsync(string goal) + { + var config = new FunctionCallingStepwisePlannerConfig + { + MaxIterations = 15, + MaxTokens = 4000, + }; + var planner = new FunctionCallingStepwisePlanner(config); + + var result = await planner.ExecuteAsync(this._agent.Kernel, goal).ConfigureAwait(false); + + return result.FinalAnswer; + } +}