diff --git a/BotSharp.sln b/BotSharp.sln index 29e7cb8e..42ac29f8 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -63,6 +63,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.MongoStorag EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.GoogleAI", "src\Plugins\BotSharp.Plugin.GoogleAI\BotSharp.Plugin.GoogleAI.csproj", "{8BC29F8A-78D6-422C-B522-10687ADC38ED}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.SemanticKernel", "src\Plugins\BotSharp.Plugin.SemanticKernel\BotSharp.Plugin.SemanticKernel.csproj", "{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.SemanticKernel.UnitTests", "tests\BotSharp.Plugin.SemanticKernel.UnitTests\BotSharp.Plugin.SemanticKernel.UnitTests.csproj", "{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.Twilio", "src\Plugins\BotSharp.Plugin.Twilio\BotSharp.Plugin.Twilio.csproj", "{E627F1E3-BE03-443A-83A2-86A855A278EB}" EndProject Global @@ -225,6 +229,22 @@ Global {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|Any CPU.Build.0 = Release|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|x64.ActiveCfg = Release|Any CPU {8BC29F8A-78D6-422C-B522-10687ADC38ED}.Release|x64.Build.0 = Release|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Debug|x64.Build.0 = Debug|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|Any CPU.Build.0 = Release|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x64.ActiveCfg = Release|Any CPU + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x64.Build.0 = Release|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x64.ActiveCfg = Debug|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x64.Build.0 = Debug|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|Any CPU.Build.0 = Release|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|x64.ActiveCfg = Release|Any CPU + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Release|x64.Build.0 = Release|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {E627F1E3-BE03-443A-83A2-86A855A278EB}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -264,6 +284,8 @@ Global {5CD330E1-9E5A-4112-8346-6E31CA98EF78} = {2635EC9B-2E5F-4313-AC21-0B847F31F36C} {DB3DE37B-1208-4ED3-9615-A52AD0AAD69C} = {5CD330E1-9E5A-4112-8346-6E31CA98EF78} {8BC29F8A-78D6-422C-B522-10687ADC38ED} = {D5293208-2BEF-42FC-A64C-5954F61720BA} + {73EE2CD0-3B27-4F02-A67B-762CBDD740D0} = {D5293208-2BEF-42FC-A64C-5954F61720BA} + {BC57D428-A1A4-4D38-A2D0-AC6CA943F247} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} {E627F1E3-BE03-443A-83A2-86A855A278EB} = {64264688-0F5C-4AB0-8F2B-B59B717CCE00} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/Plugins/BotSharp.Plugin.SemanticKernel/BotSharp.Plugin.SemanticKernel.csproj b/src/Plugins/BotSharp.Plugin.SemanticKernel/BotSharp.Plugin.SemanticKernel.csproj new file mode 100644 index 00000000..9ff34130 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SemanticKernel/BotSharp.Plugin.SemanticKernel.csproj @@ -0,0 +1,25 @@ + + + + netstandard2.1 + enable + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + True + + + + + + + + + + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelChatCompletionProvider.cs new file mode 100644 index 00000000..1b5298fc --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelChatCompletionProvider.cs @@ -0,0 +1,112 @@ +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.MLTasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BotSharp.Plugin.SemanticKernel +{ + /// + /// Use Semantic Kernel as chat completion provider + /// + public class SemanticKernelChatCompletionProvider : IChatCompletion + { + private IKernel _kernel; + private IServiceProvider _services; + private ITokenStatistics _tokenStatistics; + private string? _model = null; + + /// + public string Provider => "semantic-kernel"; + + /// + /// Create a new instance of + /// + /// + /// + /// + public SemanticKernelChatCompletionProvider(IKernel kernel, + IServiceProvider services, + ITokenStatistics tokenStatistics) + { + this._kernel = kernel; + this._services = services; + this._tokenStatistics = tokenStatistics; + } + /// + public RoleDialogModel GetChatCompletions(Agent agent, List conversations) + { + var hooks = _services.GetServices().ToList(); + + // Before chat completion hook + Task.WaitAll(hooks.Select(hook => + hook.BeforeGenerating(agent, conversations)).ToArray()); + + var completion = _kernel.GetService(_model); + + var agentService = _services.GetRequiredService(); + var instruction = agentService.RenderedInstruction(agent); + + var chatHistory = completion.CreateNewChat(instruction); + + foreach (var message in conversations) + { + if (message.Role == AgentRole.User) + { + chatHistory.AddUserMessage(message.Content); + } + else + { + chatHistory.AddAssistantMessage(message.Content); + } + } + + var response = completion.GetChatCompletionsAsync(chatHistory) + .ContinueWith(async t => + { + var result = await t; + var message = await result.First().GetChatMessageAsync(); + return message.Content; + }).ConfigureAwait(false).GetAwaiter().GetResult() + .ConfigureAwait(false).GetAwaiter().GetResult(); + + var msg = new RoleDialogModel(AgentRole.Assistant, response) + { + CurrentAgentId = agent.Id + }; + + // After chat completion hook + Task.WaitAll(hooks.Select(hook => + hook.AfterGenerated(msg, new TokenStatsModel + { + Model = _model ?? "default" + })).ToArray()); + + return msg; + } + /// + public Task GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived, Func onFunctionExecuting) + { + throw new NotImplementedException(); + } + /// + public Task GetChatCompletionsStreamingAsync(Agent agent, List conversations, Func onMessageReceived) + { + throw new NotImplementedException(); + } + + /// + public void SetModelName(string model) + { + if (!string.IsNullOrWhiteSpace(model)) + this._model = model; + } + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelPlugin.cs b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelPlugin.cs new file mode 100644 index 00000000..1494bf56 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelPlugin.cs @@ -0,0 +1,19 @@ +using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.Plugins; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace BotSharp.Plugin.SemanticKernel +{ + public class SemanticKernelPlugin : IBotSharpPlugin + { + public string Name => "Semantic Kernel"; + public string Description => "Semantic Kernel Service"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(); + services.AddScoped(); + } + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelTextCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelTextCompletionProvider.cs new file mode 100644 index 00000000..d222e850 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SemanticKernel/SemanticKernelTextCompletionProvider.cs @@ -0,0 +1,86 @@ +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.MLTasks; +using Microsoft; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI.TextCompletion; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BotSharp.Plugin.SemanticKernel +{ + /// + /// User Semantic Kernel as text completion provider + /// + public class SemanticKernelTextCompletionProvider : Abstraction.MLTasks.ITextCompletion + { + private readonly IKernel _kernel; + private readonly IServiceProvider _services; + private readonly ITokenStatistics _tokenStatistics; + private string? _model = null; + + /// + public string Provider => "semantic-kernel"; + + /// + /// Create a new instance of + /// + /// + /// + /// + public SemanticKernelTextCompletionProvider(IKernel kernel, + IServiceProvider services, + ITokenStatistics tokenStatistics) + { + Requires.NotNull(kernel, nameof(IKernel)); + + this._kernel = kernel; + this._services = services; + this._tokenStatistics = tokenStatistics; + } + + /// + public async Task GetCompletion(string text, string agentId, string messageId) + { + var hooks = _services.GetServices().ToList(); + + // Before chat completion hook + var agent = new Agent() + { + Id = agentId + }; + var userMessage = new RoleDialogModel(AgentRole.User, text) + { + MessageId = messageId + }; + Task.WaitAll(hooks.Select(hook => + hook.BeforeGenerating(agent, new List { userMessage })).ToArray()); + + var completion = _kernel.GetService(_model); + _tokenStatistics.StartTimer(); + var result = await completion.CompleteAsync(text); + _tokenStatistics.StopTimer(); + + // After chat completion hook + Task.WaitAll(hooks.Select(hook => + hook.AfterGenerated(new RoleDialogModel(AgentRole.Assistant, result), new TokenStatsModel + { + Model = _model ?? "default" + })).ToArray()); + + return result; + } + + /// + public void SetModelName(string model) + { + if (!string.IsNullOrWhiteSpace(model)) + this._model = model; + } + } +} \ No newline at end of file diff --git a/tests/BotSharp.Plugin.SemanticKernel.UnitTests/BotSharp.Plugin.SemanticKernel.UnitTests.csproj b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/BotSharp.Plugin.SemanticKernel.UnitTests.csproj new file mode 100644 index 00000000..057c117c --- /dev/null +++ b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/BotSharp.Plugin.SemanticKernel.UnitTests.csproj @@ -0,0 +1,32 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/BotSharp.Plugin.SemanticKernel.UnitTests/Helpers/ResultHelper.cs b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/Helpers/ResultHelper.cs new file mode 100644 index 00000000..84d9bbcc --- /dev/null +++ b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/Helpers/ResultHelper.cs @@ -0,0 +1,35 @@ +using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.SemanticKernel.AI.TextCompletion; +using Microsoft.SemanticKernel.Orchestration; + +namespace BotSharp.Plugin.SemanticKernel.UnitTests.Helpers +{ + public class ResultHelper : IChatResult, ITextResult + { + public ModelResult ModelResult { get; set; } + private string _response; + + public ResultHelper(string response) + { + ModelResult = new ModelResult(response); + _response = response; + } + + public async Task GetChatMessageAsync(CancellationToken cancellationToken = default) + { + return await Task.FromResult(new MockModelResult(_response)); + } + + public Task GetCompletionAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(_response); + } + + public class MockModelResult : ChatMessageBase + { + public MockModelResult(string content) : base(AuthorRole.Assistant, content, null) + { + } + } + } +} \ No newline at end of file diff --git a/tests/BotSharp.Plugin.SemanticKernel.UnitTests/Helpers/SemanticKernelHelper.cs b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/Helpers/SemanticKernelHelper.cs new file mode 100644 index 00000000..ef775f76 --- /dev/null +++ b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/Helpers/SemanticKernelHelper.cs @@ -0,0 +1,47 @@ +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.SemanticKernel.AI.TextCompletion; +using Microsoft.SemanticKernel.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BotSharp.Plugin.SemanticKernel.UnitTests.Helpers +{ + internal class SemanticKernelHelper : IChatCompletion, ITextCompletion, IAIService + { + private readonly string _excepted; + + public SemanticKernelHelper(string excepted) + { + this._excepted = excepted; + } + + public ChatHistory CreateNewChat(string? instructions = null) + { + return new ChatHistory(); + } + + public Task> GetChatCompletionsAsync(ChatHistory chat, AIRequestSettings? requestSettings = null, CancellationToken cancellationToken = default) + { + return Task.FromResult>( new List { new ResultHelper(_excepted) }); + } + + public Task> GetCompletionsAsync(string text, AIRequestSettings? requestSettings = null, CancellationToken cancellationToken = default) + { + return Task.FromResult>(new List { new ResultHelper(_excepted) }); + } + + public IAsyncEnumerable GetStreamingChatCompletionsAsync(ChatHistory chat, AIRequestSettings? requestSettings = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable GetStreamingCompletionsAsync(string text, AIRequestSettings? requestSettings = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelChatCompletionProviderTests.cs b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelChatCompletionProviderTests.cs new file mode 100644 index 00000000..5be823fa --- /dev/null +++ b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelChatCompletionProviderTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.MLTasks; +using BotSharp.Plugin.SemanticKernel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Moq; +using Xunit; +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Agents.Models; +using System.Linq; +using System.Runtime; +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Models; +using Microsoft.SemanticKernel.AI.ChatCompletion; +using Microsoft.SemanticKernel.AI; +using Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; +using BotSharp.Plugin.SemanticKernel.UnitTests.Helpers; + +namespace BotSharp.Plugin.SemanticKernel.Tests +{ + public class SemanticKernelChatCompletionProviderTests + { + private readonly Mock _kernelMock; + private readonly Mock _servicesMock; + private readonly Mock _tokenStatisticsMock; + private readonly SemanticKernelChatCompletionProvider _provider; + + public SemanticKernelChatCompletionProviderTests() + { + _kernelMock = new Mock(); + _servicesMock = new Mock(); + _tokenStatisticsMock = new Mock(); + _provider = new SemanticKernelChatCompletionProvider(_kernelMock.Object, _servicesMock.Object, _tokenStatisticsMock.Object); + } + + [Fact] + public void GetChatCompletions_Returns_RoleDialogModel() + { + // Arrange + var agent = new Agent(); + var conversations = new List + { + new RoleDialogModel(AgentRole.User, "Hello") + }; + + _servicesMock.Setup(x => x.GetService(typeof(IEnumerable))) + .Returns(new List()); + var agentService = new Mock(); + agentService.Setup(x => x.RenderedInstruction(agent)).Returns(""); + _servicesMock.Setup(x => x.GetService(typeof(IAgentService))) + .Returns(agentService.Object); + + var chatHistoryMock = new Mock(); + var chatCompletionMock = new Mock(); + chatCompletionMock.Setup(x => x.CreateNewChat(It.IsAny())).Returns(chatHistoryMock.Object); + chatCompletionMock.Setup(x => x.GetChatCompletionsAsync(chatHistoryMock.Object, It.IsAny(), It.IsAny())) + .ReturnsAsync(new List + { + new ResultHelper("How can I help you?") + }); + + _kernelMock.Setup(x => x.GetService(null)).Returns(chatCompletionMock.Object); + + // Act + var result = _provider.GetChatCompletions(agent, conversations); + + // Assert + Assert.IsType(result); + } + } + +} diff --git a/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelPluginTests.cs b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelPluginTests.cs new file mode 100644 index 00000000..a1ba7214 --- /dev/null +++ b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelPluginTests.cs @@ -0,0 +1,35 @@ +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.MLTasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Moq; + +namespace BotSharp.Plugin.SemanticKernel.Tests +{ + public class SemanticKernelPluginTests + { + [Fact] + public void TestRegisterDI() + { + var services = new ServiceCollection(); + var config = new ConfigurationBuilder().Build(); + var plugin = new SemanticKernelPlugin(); + services.AddScoped(x => + { + return new KernelBuilder() + .WithAzureOpenAIChatCompletionService("test", "test", "test") + .Build(); + }); + services.AddScoped(x=> Mock.Of()); + + + plugin.RegisterDI(services, config); + + var provider = services.BuildServiceProvider(); + + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); + } + } +} \ No newline at end of file diff --git a/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelTextCompletionProviderTests.cs b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelTextCompletionProviderTests.cs new file mode 100644 index 00000000..1e463cdf --- /dev/null +++ b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/SemanticKernelTextCompletionProviderTests.cs @@ -0,0 +1,53 @@ +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Plugin.SemanticKernel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AI.TextCompletion; +using Moq; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Conversations; +using System; +using System.Linq; +using Microsoft; +using Microsoft.SemanticKernel.AI; +using BotSharp.Plugin.SemanticKernel.UnitTests.Helpers; + +namespace BotSharp.Plugin.SemanticKernel.Tests +{ + public class SemanticKernelTextCompletionProviderTests + { + private readonly IServiceProvider _services; + private readonly ITokenStatistics _tokenStatistics; + + public SemanticKernelTextCompletionProviderTests() + { + + _services = new ServiceCollection().BuildServiceProvider(); + _tokenStatistics = Mock.Of(); + } + + [Fact] + public async Task GetCompletion_ReturnsExpectedResult() + { + // Arrange + + var text = "Hello"; + var expected = "Hello, world!"; + var _kernel = new KernelBuilder() + .WithAIService("", new SemanticKernelHelper(expected)) + .Build(); + var provider = new SemanticKernelTextCompletionProvider(_kernel, _services, _tokenStatistics); + + // Act + var result = await provider.GetCompletion(text, "agent1", "message1"); + + // Assert + Assert.Equal(expected, result); + + } + } +} diff --git a/tests/BotSharp.Plugin.SemanticKernel.UnitTests/Usings.cs b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/Usings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/tests/BotSharp.Plugin.SemanticKernel.UnitTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file