diff --git a/Directory.Packages.props b/Directory.Packages.props index 128eb81a82..df44503ee8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,10 +19,10 @@ - - - - + + + + diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 4969fe5430..a18406dd44 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -2343,6 +2343,10 @@ } ] }, + { + "text": "AI Management (Pro)", + "path": "modules/ai-management/index.md" + }, { "text": "Audit Logging", "path": "modules/audit-logging.md" diff --git a/docs/en/modules/ai-management/index.md b/docs/en/modules/ai-management/index.md new file mode 100644 index 0000000000..5baf923e4f --- /dev/null +++ b/docs/en/modules/ai-management/index.md @@ -0,0 +1,663 @@ +# AI Management (Pro) + +> You must have an ABP Team or a higher license to use this module. + +This module implements AI (Artificial Intelligence) management capabilities on top of the [Artificial Intelligence Workspaces](../../framework/infrastructure/artificial-intelligence.md) feature of the ABP Framework and allows to manage workspaces dynamically from the application including UI components and API endpoints. + + +## How to Install + +### New Solutions + +AI Management module is not pre-installed in [the startup templates](../solution-templates/layered-web-application). You can install it using the ABP CLI or ABP Suite. + +**Using ABP CLI:** + +```bash +abp add-module Volo.AIManagement +``` + +**Using ABP Suite:** + +Open ABP Suite, navigate to your solution, and install the AI Management module from the Modules page. + +### Existing Solutions +If you want to add the **AI Management** module to your existing solution, you can use the ABP CLI `add-module` command: + +```bash +abp add-module Volo.AIManagement +``` + +## Packages + +This module follows the [module development best practices guide](../../framework/architecture/best-practices) and consists of several NuGet and NPM packages. See the guide if you want to understand the packages and relations between them. + +You can visit [AI Management module package list page](https://abp.io/packages?moduleName=Volo.AIManagement) to see list of packages related with this module. + +AI Management module packages are designed for various usage scenarios. Packages are grouped by the usage scenario as `Volo.AIManagement.*` and `Volo.AIManagement.Client.*`. This structure helps to separate the use-cases clearly. + +## User Interface + +### Menu Items + +AI Management module adds the following items to the "Main" menu, under the "Administration" menu item: + +* **Workspaces**: Workspace management page. +* **Chat**: AI chat interface for testing workspaces. + +`AIManagementMenus` class has the constants for the menu item names. + +### Pages + +#### Workspace Management + +Workspaces page is used to manage AI workspaces in the system. You can create, edit, duplicate, and delete workspaces. + +![ai-management-workspaces](../../images/ai-management-workspaces.png) + +You can create a new workspace or edit an existing workspace in this page. The workspace configuration includes: + +* **Name**: Unique identifier for the workspace (cannot contain spaces) +* **Provider**: AI provider (OpenAI, Ollama, or custom providers) +* **Model**: AI model name (e.g., gpt-4, mistral) +* **API Key**: Authentication key (if required by provider) +* **API Base URL**: Custom endpoint URL (optional) +* **System Prompt**: Default system instructions +* **Temperature**: Response randomness (0.0-1.0) +* **Application Name**: Associate with specific application +* **Required Permission**: Permission needed to use this workspace + +#### Chat Interface + +The AI Management module includes a built-in chat interface for testing workspaces. You can: + +* Select a workspace from available workspaces +* Send messages and receive AI responses +* Test streaming responses +* Verify workspace configuration before using in production + +> Access the chat interface at: `/AIManagement/Chat` + +## Workspace Configuration + +Workspaces are the core concept of the AI Management module. A workspace represents an AI provider configuration that can be used throughout your application. + +### Workspace Properties + +When creating or managing a workspace, you can configure the following properties: + +| Property | Required | Description | +|----------|----------|-------------| +| `Name` | Yes | Unique workspace identifier (cannot contain spaces) | +| `Provider` | Yes* | AI provider name (e.g., "OpenAI", "Ollama") | +| `ModelName` | Yes* | Model identifier (e.g., "gpt-4", "mistral") | +| `ApiKey` | No | API authentication key (required by some providers) | +| `ApiBaseUrl` | No | Custom endpoint URL (defaults to provider's default) | +| `SystemPrompt` | No | Default system prompt for all conversations | +| `Temperature` | No | Response randomness (0.0-1.0, defaults to provider default) | +| `Description` | No | Workspace description | +| `IsActive` | No | Enable/disable the workspace (default: true) | +| `ApplicationName` | No | Associate workspace with specific application (for multi-application scenarios) | +| `RequiredPermissionName` | No | Permission required to use this workspace | +| `IsSystem` | No | Whether it's a system workspace (read-only) | +| `OverrideSystemConfiguration` | No | Allow database configuration to override code-defined settings | + +**\*Not required for system workspaces** + +### System vs Dynamic Workspaces + +The AI Management module supports two types of workspaces: + +#### System Workspaces + +* **Defined in code** using `PreConfigure` +* **Cannot be deleted** through the UI +* **Read-only by default**, but can be overridden when `OverrideSystemConfiguration` is enabled +* **Useful for** application-critical AI features that must always be available +* **Created automatically** when the application starts + +Example: + +```csharp +PreConfigure(options => +{ + options.Workspaces.Configure(configuration => + { + configuration.ConfigureChatClient(chatClientConfiguration => + { + chatClientConfiguration.Builder = new ChatClientBuilder( + sp => new OpenAIClient(apiKey).AsChatClient("gpt-4") + ); + }); + }); +}); +``` + +#### Dynamic Workspaces + +* **Created through the UI** or programmatically via `IWorkspaceRepository` +* **Fully manageable** - can be created, updated, activated/deactivated, and deleted +* **Stored in database** with all configuration +* **Ideal for** user-customizable AI features and multi-tenant scenarios +* **Supports multi-tenancy** - each tenant has isolated workspaces + +Example (data seeding): + +```csharp +await _workspaceRepository.InsertAsync(new Workspace( + name: "CustomerSupportWorkspace", + provider: "OpenAI", + modelName: "gpt-4", + apiKey: "your-api-key", + systemPrompt: "You are a helpful customer support assistant.", + requiredPermissionName: "MyApp.CustomerSupport" +)); +``` + +### Workspace Naming Rules + +* Workspace names **must be unique** +* Workspace names **cannot contain spaces** (use underscores or camelCase) +* Workspace names are **case-sensitive** + +## Permissions + +The AI Management module defines the following permissions: + +### Workspace Management Permissions + +| Permission | Description | Default Granted To | +|------------|-------------|-------------------| +| `AIManagement.Workspaces` | View workspaces | Admin role | +| `AIManagement.Workspaces.Create` | Create new workspaces | Admin role | +| `AIManagement.Workspaces.Update` | Edit existing workspaces | Admin role | +| `AIManagement.Workspaces.Delete` | Delete workspaces | Admin role | + +### Chat Permissions + +| Permission | Description | Default Granted To | +|------------|-------------|-------------------| +| `AIManagement.Chat` | Access chat interface | Admin role | + +### Workspace-Level Permissions + +In addition to module-level permissions, you can restrict access to individual workspaces by setting the `RequiredPermissionName` property: + +```csharp +var workspace = new Workspace( + name: "PremiumWorkspace", + provider: "OpenAI", + modelName: "gpt-4", + requiredPermissionName: "MyApp.PremiumFeatures" +); +``` + +When a workspace has a required permission: +* Only users with that permission can access the workspace +* Users without the permission will receive an authorization error +* The workspace will not appear in the workspace selection dropdown for unauthorized users + +## Usage Scenarios + +The AI Management module is designed to support various usage patterns, from simple standalone AI integration to complex microservice architectures. The module provides two main package groups to support different scenarios: + +- **`Volo.AIManagement.*`** packages for hosting AI Management with full database and management capabilities +- **`Volo.AIManagement.Client.*`** packages for client applications that consume AI services + +### Scenario 1: No AI Management Dependency + +**Use this when:** You want to use AI in your application without any dependency on the AI Management module. + +In this scenario, you only use the ABP Framework's AI features directly. You configure AI providers (like OpenAI) in your code and don't need any database or management UI. + +**Required Packages:** +- `Volo.Abp.AI` +- Any Microsoft AI extensions (e.g., `Microsoft.Extensions.AI.OpenAI`) + +**Configuration:** + +```csharp +public class YourModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(options => + { + options.Workspaces.ConfigureDefault(configuration => + { + configuration.ConfigureChatClient(chatClientConfiguration => + { + chatClientConfiguration.Builder = new ChatClientBuilder( + sp => new OpenAIClient(apiKey).AsChatClient("gpt-4") + ); + }); + }); + }); + } +} +``` + +**Usage:** + +```csharp +public class MyService +{ + private readonly IChatClient _chatClient; + + public MyService(IChatClient chatClient) + { + _chatClient = chatClient; + } + + public async Task GetResponseAsync(string prompt) + { + var response = await _chatClient.CompleteAsync(prompt); + return response.Message.Text; + } +} +``` + +> See [Artificial Intelligence](../../framework/infrastructure/artificial-intelligence.md) documentation for more details about workspace configuration. + +### Scenario 2: AI Management with Domain Layer Dependency (Local Execution) + +**Use this when:** You want to host the full AI Management module inside your application with database storage and management UI. + +In this scenario, you install the AI Management module with its database layer, which allows you to manage AI workspaces dynamically through the UI or data seeding. + +**Required Packages:** + +**Minimum (backend only):** +- `Volo.AIManagement.EntityFrameworkCore` (or `Volo.AIManagement.MongoDB`) +- `Volo.AIManagement.OpenAI` (or another AI provider package) + +**Full installation (with UI and API):** +- `Volo.AIManagement.EntityFrameworkCore` (or `Volo.AIManagement.MongoDB`) +- `Volo.AIManagement.Application` +- `Volo.AIManagement.HttpApi` +- `Volo.AIManagement.Web` (for management UI) +- `Volo.AIManagement.OpenAI` (or another AI provider package) + +> Note: `Volo.AIManagement.EntityFrameworkCore` transitively includes `Volo.AIManagement.Domain` and `Volo.Abp.AI.AIManagement` packages. + +**Workspace Definition Options:** + +**Option 1 - System Workspace (Code-based):** + +```csharp +public class YourModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(options => + { + options.Workspaces.Configure(configuration => + { + configuration.ConfigureChatClient(chatClientConfiguration => + { + // Configuration will be populated from database + }); + }); + }); + } +} +``` + +**Option 2 - Dynamic Workspace (UI-based):** + +No code configuration needed. Define workspaces through: +- The AI Management UI (navigate to AI Management > Workspaces) +- Data seeding in your `DataSeeder` class + +**Using Chat Client:** + +```csharp +public class MyService +{ + private readonly IChatClient _chatClient; + + public MyService(IChatClient chatClient) + { + _chatClient = chatClient; + } +} +``` + +### Scenario 3: AI Management Client with Remote Execution + +**Use this when:** You want to use AI capabilities without managing AI configuration yourself, and let a dedicated AI Management microservice handle everything. + +In this scenario, your application communicates with a separate AI Management microservice that manages configurations and communicates with AI providers on your behalf. The AI Management service handles all AI provider interactions. + +**Required Packages:** +- `Volo.AIManagement.Client.HttpApi.Client` + +**Configuration:** + +Add the remote service endpoint in your `appsettings.json`: + +```json +{ + "RemoteServices": { + "AIManagementClient": { + "BaseUrl": "https://your-ai-management-service.com/" + } + } +} +``` + +Optionally define workspace in your module: + +```csharp +public class YourModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(options => + { + // Optional: Pre-define workspace type for type safety + options.Workspaces.Configure(configuration => + { + // Configuration will be fetched from remote service + }); + }); + } +} +``` + +**Usage:** + +```csharp +public class MyService +{ + private readonly IChatCompletionClientAppService _chatService; + + public MyService(IChatCompletionClientAppService chatService) + { + _chatService = chatService; + } + + public async Task GetAIResponseAsync(string workspaceName, string prompt) + { + var request = new ChatClientCompletionRequestDto + { + Messages = new List + { + new ChatMessageDto { Role = "user", Content = prompt } + } + }; + + var response = await _chatService.ChatCompletionsAsync(workspaceName, request); + return response.Content; + } + + // For streaming responses + public async IAsyncEnumerable StreamAIResponseAsync(string workspaceName, string prompt) + { + var request = new ChatClientCompletionRequestDto + { + Messages = new List + { + new ChatMessageDto { Role = "user", Content = prompt } + } + }; + + await foreach (var update in _chatService.StreamChatCompletionsAsync(workspaceName, request)) + { + yield return update.Content; + } + } +} +``` + +### Scenario 4: Exposing Client HTTP Endpoints (Proxy Pattern) + +**Use this when:** You want your application to act as a proxy/API gateway, exposing AI capabilities to other services or client applications. + +This scenario builds on Scenario 3, but your application exposes its own HTTP endpoints that other applications can call. Your application then forwards these requests to the AI Management service. + +**Required Packages:** +- `Volo.AIManagement.Client.HttpApi.Client` (to communicate with AI Management service) +- `Volo.AIManagement.Client.Application` (application services) +- `Volo.AIManagement.Client.HttpApi` (to expose HTTP endpoints) +- `Volo.AIManagement.Client.Web` (optional, for UI components) + +**Configuration:** + +Same as Scenario 3, configure the remote AI Management service in `appsettings.json`. + +**Usage:** + +Once configured, other applications can call your application's endpoints: +- `POST /api/ai-management-client/chat-completion` for chat completions +- `POST /api/ai-management-client/stream-chat-completion` for streaming responses + +Your application acts as a proxy, forwarding these requests to the AI Management microservice. + +## Comparison Table + +| Scenario | Database Required | Manages Config | Executes AI | Exposes API | Use Case | +|----------|------------------|----------------|-------------|-------------|----------| +| **1. No AI Management** | No | Code | Local | Optional | Simple apps, no config management needed | +| **2. Full AI Management** | Yes | Database/UI | Local | Optional | Monoliths, services managing their own AI | +| **3. Client Remote** | No | Remote Service | Remote Service | No | Microservices consuming AI centrally | +| **4. Client Proxy** | No | Remote Service | Remote Service | Yes | API Gateway pattern, proxy services | + +## Implementing Custom AI Provider Factories + +While the AI Management module provides built-in support for OpenAI through the `Volo.AIManagement.OpenAI` package, you can easily add support for other AI providers by implementing a custom `IChatClientFactory`. + +### Understanding the Factory Pattern + +The AI Management module uses a factory pattern to create `IChatClient` instances based on the provider configuration stored in the database. Each provider (OpenAI, Ollama, Azure OpenAI, etc.) needs its own factory implementation. + +### Creating a Custom Factory + +Here's how to implement a factory for Ollama as an example: + +#### Step 1: Install the Provider's NuGet Package + +First, install the AI provider's package. For Ollama: + +```bash +dotnet add package OllamaSharp +``` + +#### Step 2: Implement the `IChatClientFactory` Interface + +Create a factory class that implements `IChatClientFactory`: + +```csharp +using Microsoft.Extensions.AI; +using OllamaSharp; +using Volo.AIManagement.Factory; +using Volo.Abp.DependencyInjection; + +namespace YourNamespace; + +public class OllamaChatClientFactory : IChatClientFactory, ITransientDependency +{ + public string Provider => "Ollama"; + + public Task CreateAsync(ChatClientCreationConfiguration configuration) + { + // Create the Ollama client with configuration from database + var client = new OllamaApiClient( + configuration.ApiBaseUrl ?? "http://localhost:11434", + configuration.ModelName + ); + + // Return as IChatClient + return Task.FromResult(client); + } +} +``` + +#### Step 3: Register the Factory + +Register your factory in your module's `ConfigureServices` method: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + options.AddFactory("Ollama"); + }); +} +``` + +> [!TIP] +> For production scenarios, you may want to add validation for the factory configuration. + + +### Available Configuration Properties + +The `ChatClientCreationConfiguration` object provides the following properties from the database: + +| Property | Type | Description | +|----------|------|-------------| +| `Name` | string | Workspace name | +| `Provider` | string | Provider name (e.g., "OpenAI", "Ollama") | +| `ApiKey` | string? | API key for authentication | +| `ModelName` | string | Model identifier (e.g., "gpt-4", "mistral") | +| `SystemPrompt` | string? | Default system prompt for the workspace | +| `Temperature` | float? | Temperature setting for response generation | +| `ApiBaseUrl` | string? | Custom API endpoint URL | +| `Description` | string? | Workspace description | +| `IsActive` | bool | Whether the workspace is active | +| `IsSystem` | bool | Whether it's a system workspace | +| `RequiredPermissionName` | string? | Permission required to use this workspace | + +### Example: Azure OpenAI Factory + +Here's an example of implementing a factory for Azure OpenAI: + +```csharp +using Azure.AI.OpenAI; +using Azure; +using Microsoft.Extensions.AI; +using Volo.AIManagement.Factory; +using Volo.Abp.DependencyInjection; + +namespace YourNamespace; + +public class AzureOpenAIChatClientFactory : IChatClientFactory, ITransientDependency +{ + public string Provider => "AzureOpenAI"; + + public Task CreateAsync(ChatClientCreationConfiguration configuration) + { + var client = new AzureOpenAIClient( + new Uri(configuration.ApiBaseUrl ?? throw new ArgumentNullException(nameof(configuration.ApiBaseUrl))), + new AzureKeyCredential(configuration.ApiKey ?? throw new ArgumentNullException(nameof(configuration.ApiKey))) + ); + + var chatClient = client.GetChatClient(configuration.ModelName); + return Task.FromResult(chatClient.AsIChatClient()); + } +} +``` + +### Using Your Custom Provider + +After implementing and registering your factory: + +1. **Through UI**: Navigate to the AI Management workspaces page and create a new workspace: + - Select your provider name (e.g., "Ollama", "AzureOpenAI") + - Configure the API settings + - Set the model name + +2. **Through Code** (data seeding): + +```csharp +await _workspaceRepository.InsertAsync(new Workspace( + GuidGenerator.Create(), + "MyOllamaWorkspace", + provider: "Ollama", + modelName: "mistral", + apiBaseUrl: "http://localhost:11434", + description: "Local Ollama workspace" +)); +``` + +> **Tip**: The provider name you use in `AddFactory("ProviderName")` must match the provider name stored in the workspace configuration in the database. + +## Internals + +### Domain Layer + +The AI Management module follows Domain-Driven Design principles and has a well-structured domain layer. + +#### Aggregates + +- **Workspace**: The main aggregate root representing an AI workspace configuration. + +#### Repositories + +The following custom repositories are defined: + +- `IWorkspaceRepository`: Repository for workspace management with custom queries. + +#### Domain Services + +- `ApplicationWorkspaceManager`: Manages workspace operations and validations. +- `WorkspaceConfigurationStore`: Retrieves workspace configuration with caching. +- `ChatClientResolver`: Resolves the appropriate `IChatClient` implementation for a workspace. + +#### Integration Services + +The module exposes the following integration services for inter-service communication: + +- `IAIChatCompletionIntegrationService`: Executes AI chat completions remotely. +- `IWorkspaceConfigurationIntegrationService`: Retrieves workspace configuration for remote setup. +- `IWorkspaceIntegrationService`: Manages workspaces remotely. + +> Integration services are exposed at `/integration-api` prefix and marked with `[IntegrationService]` attribute. + +### Application Layer + +#### Application Services + +- `WorkspaceAppService`: CRUD operations for workspace management. +- `ChatCompletionClientAppService`: Client-side chat completion services. +- `AIChatCompletionIntegrationService`: Integration service for remote AI execution. + +### Caching + +Workspace configurations are cached for performance. The cache key format: + +``` +WorkspaceConfiguration:{ApplicationName}:{WorkspaceName} +``` + +The cache is automatically invalidated when workspaces are created, updated, or deleted. + +### Multi-Tenancy + +The AI Management module is **fully multi-tenant aware**: + +- **Workspaces are isolated per tenant**: Each tenant has their own set of workspaces. +- **Independent configurations**: Tenants can configure their own AI providers and API keys. +- **Secure credential separation**: API keys and configurations are never shared between tenants. +- **System workspaces**: System workspaces defined in code are available to all tenants. + +When working with multi-tenant applications: + +```csharp +// Create workspace for specific tenant +using (_currentTenant.Change(tenantId)) +{ + await _workspaceRepository.InsertAsync(new Workspace( + name: "TenantSpecificWorkspace", + provider: "OpenAI", + modelName: "gpt-4" + )); +} +``` + +## See Also + +- [Artificial Intelligence Infrastructure](../../framework/infrastructure/artificial-intelligence.md): Learn about the underlying AI workspace infrastructure +- [Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/): Microsoft's unified AI abstractions +- [Semantic Kernel](https://learn.microsoft.com/en-us/semantic-kernel/): Microsoft's Semantic Kernel integration \ No newline at end of file diff --git a/framework/Volo.Abp.slnx b/framework/Volo.Abp.slnx index f4c0d2b373..5e53bec002 100644 --- a/framework/Volo.Abp.slnx +++ b/framework/Volo.Abp.slnx @@ -1,5 +1,7 @@ + + @@ -173,6 +175,7 @@ + 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 diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj index 847443f55c..0c70c8382a 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj index a440d03e6c..a3297e7d4b 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj index 92d013678e..c80d32507b 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj index 16907eaeb2..da78aca5ea 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj index 0997a8dc36..4f1a5010fa 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj index c2f9285e5b..fee01d2719 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj index 3141970a40..22336ef66f 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj index 1d41ed94f2..5c05116803 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj index d6c5dde613..8fc252c5f6 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj index 55c9ba671a..abfd60f0d6 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj index fd3dc12c60..5f3ac0fed5 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj index 4451cdd904..4b724c9505 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj @@ -13,8 +13,8 @@ - - + +