diff --git a/framework/Volo.Abp.slnx b/framework/Volo.Abp.slnx index ab63be61ea..1289991520 100644 --- a/framework/Volo.Abp.slnx +++ b/framework/Volo.Abp.slnx @@ -1,5 +1,7 @@ + + @@ -164,12 +166,11 @@ - - + diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIModule.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIModule.cs index 81d3866199..82a8148679 100644 --- a/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIModule.cs +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/AbpAIModule.cs @@ -34,6 +34,7 @@ public class AbpAIModule : AbpModule } context.Services.TryAddTransient(typeof(IChatClient<>), typeof(TypedChatClient<>)); + context.Services.TryAddTransient(typeof(IChatClientAccessor<>), typeof(ChatClientAccessor<>)); context.Services.TryAddTransient(typeof(IKernelAccessor<>), typeof(KernelAccessor<>)); } @@ -99,5 +100,19 @@ public class AbpAIModule : AbpModule sp => sp.GetRequiredKeyedService(serviceName) ); } + + if (workspaceConfig.Kernel.Builder is null) + { + context.Services.AddKeyedTransient( + AbpAIWorkspaceOptions.GetKernelServiceKeyName(workspaceConfig.Name), + (sp, _) => + { + var chatClient = sp.GetRequiredKeyedService(serviceName); + var builder = Kernel.CreateBuilder(); + builder.Services.AddSingleton(chatClient); + return builder.Build(); + } + ); + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs index 1a852838fc..247e6e354e 100644 --- a/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs @@ -5,9 +5,9 @@ using Volo.Abp.DependencyInjection; namespace Volo.Abp.AI; -[Dependency(ReplaceServices = true, TryRegister = true)] +[Dependency(ReplaceServices = true)] [ExposeServices(typeof(IChatClientAccessor))] -public class ChatClientAccessor : IChatClientAccessor +public class ChatClientAccessor : IChatClientAccessor, ITransientDependency { public IChatClient? ChatClient { get; } @@ -19,8 +19,6 @@ public class ChatClientAccessor : IChatClientAccessor } } -[Dependency(ReplaceServices = true, TryRegister = true)] -[ExposeServices(typeof(IChatClientAccessor))] public class ChatClientAccessor : IChatClientAccessor where TWorkSpace : class { diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/DefaultKernelAccessor.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/DefaultKernelAccessor.cs index 5370a41dd1..11e8587045 100644 --- a/framework/src/Volo.Abp.AI/Volo/Abp/AI/DefaultKernelAccessor.cs +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/DefaultKernelAccessor.cs @@ -13,6 +13,8 @@ public class DefaultKernelAccessor : IKernelAccessor, ITransientDependency public DefaultKernelAccessor(IServiceProvider serviceProvider) { Kernel = serviceProvider.GetKeyedService( - AbpAIModule.DefaultWorkspaceName); + AbpAIWorkspaceOptions.GetKernelServiceKeyName( + AbpAIModule.DefaultWorkspaceName + )); } } diff --git a/framework/test/Volo.Abp.AI.Tests/Volo.Abp.AI.Tests.abppkg b/framework/test/Volo.Abp.AI.Tests/Volo.Abp.AI.Tests.abppkg new file mode 100644 index 0000000000..a686451fbc --- /dev/null +++ b/framework/test/Volo.Abp.AI.Tests/Volo.Abp.AI.Tests.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.test" +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AI.Tests/Volo.Abp.AI.Tests.csproj b/framework/test/Volo.Abp.AI.Tests/Volo.Abp.AI.Tests.csproj new file mode 100644 index 0000000000..c67a9b8a0d --- /dev/null +++ b/framework/test/Volo.Abp.AI.Tests/Volo.Abp.AI.Tests.csproj @@ -0,0 +1,18 @@ + + + + + + net10.0 + Volo.Abp.AI.Tests + Volo.Abp.AI.Tests + + + + + + + + + + diff --git a/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/AbpAITestModule.cs b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/AbpAITestModule.cs new file mode 100644 index 0000000000..2de265db92 --- /dev/null +++ b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/AbpAITestModule.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.AI; +using Volo.Abp.AI; +using Volo.Abp.AI.Mocks; +using Volo.Abp.AI.Tests.Workspaces; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AutoMapper; + +[DependsOn( + typeof(AbpTestBaseModule), + typeof(AbpAIModule) +)] +public class AbpAITestModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(options => + { + options.Workspaces.ConfigureDefault(options => + { + options.ConfigureChatClient(clientOptions => + { + clientOptions.Builder = new ChatClientBuilder(new MockDefaultChatClient()); + }); + }); + + options.Workspaces.Configure(workspaceOptions => + { + workspaceOptions.ConfigureChatClient(clientOptions => + { + clientOptions.Builder = new ChatClientBuilder(new MockChatClient()); + }); + }); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + } +} diff --git a/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/ChatClientAccessor_Tests.cs b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/ChatClientAccessor_Tests.cs new file mode 100644 index 0000000000..6cf352318a --- /dev/null +++ b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/ChatClientAccessor_Tests.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Shouldly; +using Volo.Abp.AI.Tests.Workspaces; +using Volo.Abp.AutoMapper; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.AI; +public class ChatClientAccessor_Tests : AbpIntegratedTest +{ + [Fact] + public void Should_Resolve_DefaultChatClientAccessor() + { + // Arrange & Act + var chatClientAccessor = GetRequiredService(); + // Assert + chatClientAccessor.ShouldNotBeNull(); + chatClientAccessor.ChatClient.ShouldNotBeNull(); + } + + [Fact] + public void Should_Resolve_ChatClientAccessor_For_Workspace() + { + // Arrange & Act + var chatClientAccessor = GetRequiredService>(); + // Assert + chatClientAccessor.ShouldNotBeNull(); + chatClientAccessor.ChatClient.ShouldNotBeNull(); + } + + [Fact] + public void Should_Resolve_ChatClientAccessor_For_NonConfigured_Workspace() + { + // Arrange & Act + var chatClientAccessor = GetRequiredService>(); + + // Assert + chatClientAccessor.ShouldNotBeNull(); + chatClientAccessor.ChatClient.ShouldBeNull(); + } + + public class NonConfiguredWorkspace + { + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/ChatClient_Tests.cs b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/ChatClient_Tests.cs new file mode 100644 index 0000000000..6e5c0e02ee --- /dev/null +++ b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/ChatClient_Tests.cs @@ -0,0 +1,89 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Shouldly; +using Volo.Abp.AI.Mocks; +using Volo.Abp.AI.Tests.Workspaces; +using Volo.Abp.AutoMapper; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.AI.Tests; + +public class ChatClient_Tests : AbpIntegratedTest +{ + [Fact] + public void Should_Resolve_ChatClient_For_Workspace() + { + // Arrange & Act + var chatClient = GetRequiredService>(); + + // Assert + chatClient.ShouldNotBeNull(); + chatClient.ShouldNotBeOfType(); + } + + [Fact] + public void Should_Resolve_Keyed_ChatClient_For_Workspace() + { + // Arrange + var workspaceName = WorkspaceNameAttribute.GetWorkspaceName(); + var serviceName = AbpAIWorkspaceOptions.GetChatClientServiceKeyName(workspaceName); + + // Act + var chatClient = GetRequiredKeyedService( + serviceName + ); + + // Assert + chatClient.ShouldNotBeNull(); + } + + [Fact] + public void Should_Resolve_Default_ChatClient() + { + // Arrange & Act + var chatClient = GetRequiredService(); + + // Assert + chatClient.ShouldNotBeNull(); + chatClient.ShouldBeOfType(); + } + + [Fact] + public async Task Should_Get_Response_For_Workspace() + { + // Arrange + var chatClient = GetRequiredService>(); + + // Act + var response = await chatClient.GetResponseAsync(new[] + { + new ChatMessage(ChatRole.User, "Hello, how are you?") + }); + + // Assert + response.ShouldNotBeNull(); + response.Messages.ShouldNotBeEmpty(); + } + + [Fact] + public async Task Should_Get_Streaming_Response_For_Workspace() + { + // Arrange + var chatClient = GetRequiredService>(); + var messagesInput = new[] + { + new ChatMessage(ChatRole.User, "Hello, how are you?") + }; + + // Act + var responseParts = 0; + await foreach (var response in chatClient.GetStreamingResponseAsync(messagesInput)) + { + responseParts++; + } + + // Assert + responseParts.ShouldBe(MockChatClient.StreamingResponseParts); + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/KernelAccessor_Tests.cs b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/KernelAccessor_Tests.cs new file mode 100644 index 0000000000..b5c54e056a --- /dev/null +++ b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/KernelAccessor_Tests.cs @@ -0,0 +1,62 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Shouldly; +using Volo.Abp.AI.Mocks; +using Volo.Abp.AI.Tests.Workspaces; +using Volo.Abp.AutoMapper; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.AI; +public class KernelAccessor_Tests : AbpIntegratedTest +{ + [Fact] + public void Should_Resolve_DefaultKernelAccessor() + { + // Arrange & Act + var kernelAccessor = GetRequiredService(); + // Assert + kernelAccessor.ShouldNotBeNull(); + kernelAccessor.Kernel.ShouldNotBeNull(); + } + + [Fact] + public async Task Should_Get_Response_From_DefaultKernel() + { + // Arrange + var kernelAccessor = GetRequiredService(); + var kernel = kernelAccessor.Kernel; + // Act + var result = await kernel.GetRequiredService() + .GetResponseAsync("Hello, World!"); + // Assert + result.ShouldNotBeNull(); + result.RawRepresentation.ShouldBe(MockChatClient.MockResponse); + } + + [Fact] + public void Should_Resolve_KernelAccessor_For_Workspace() + { + // Arrange & Act + var kernelAccessor = GetRequiredService>(); + // Assert + kernelAccessor.ShouldNotBeNull(); + kernelAccessor.Kernel.ShouldNotBeNull(); + } + + [Fact] + public async Task Should_Get_Response_From_Kernel_For_Workspace() + { + // Arrange + var kernelAccessor = GetRequiredService>(); + var kernel = kernelAccessor.Kernel; + + // Act + var result = await kernel.GetRequiredService() + .GetResponseAsync("Hello, World!"); + + // Assert + result.ShouldNotBeNull(); + result.RawRepresentation.ShouldBe(MockChatClient.MockResponse); + } +} diff --git a/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/Mocks/MockChatClient.cs b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/Mocks/MockChatClient.cs new file mode 100644 index 0000000000..64294e1a38 --- /dev/null +++ b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/Mocks/MockChatClient.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.AI; + +namespace Volo.Abp.AI.Mocks; + +public class MockChatClient : IChatClient +{ + public const int StreamingResponseParts = 4; + + public const string MockResponse = "This is a mock response."; + public void Dispose() + { + + } + + public Task GetResponseAsync( + IEnumerable messages, + ChatOptions options = null, + CancellationToken cancellationToken = default) + { + var responseMessages = messages.ToList(); + responseMessages.Add(new ChatMessage(ChatRole.Assistant, MockResponse)); + return Task.FromResult(new ChatResponse + { + Messages = responseMessages, + RawRepresentation = MockResponse + }); + } + + public object GetService(Type serviceType, object serviceKey = null) + { + return null; + } + + public async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, + ChatOptions options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + for (var i = 0; i < StreamingResponseParts; i++) + { + await Task.Delay(25, cancellationToken); + + if (cancellationToken.IsCancellationRequested) + { + break; + } + + yield return new ChatResponseUpdate + { + Role = ChatRole.Assistant, + RawRepresentation = MockResponse + " " + (i + 1), + }; + } + } +} diff --git a/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/Mocks/MockDefaultChatClient.cs b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/Mocks/MockDefaultChatClient.cs new file mode 100644 index 0000000000..42ff57fd7e --- /dev/null +++ b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/Mocks/MockDefaultChatClient.cs @@ -0,0 +1,4 @@ +namespace Volo.Abp.AI.Mocks; +public class MockDefaultChatClient : MockChatClient +{ +} diff --git a/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/Workspaces/WordCounter.cs b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/Workspaces/WordCounter.cs new file mode 100644 index 0000000000..0e0a4c81b1 --- /dev/null +++ b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/Workspaces/WordCounter.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.AI.Tests.Workspaces; + +[WorkspaceName("WordCounter")] +public class WordCounter +{ + +} \ No newline at end of file