diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Connections/IAIProviderConnectionStore.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Connections/IAIProviderConnectionStore.cs index 8396fb0c..358f6ac1 100644 --- a/src/Abstractions/CrestApps.Core.AI.Abstractions/Connections/IAIProviderConnectionStore.cs +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Connections/IAIProviderConnectionStore.cs @@ -4,8 +4,9 @@ namespace CrestApps.Core.AI.Connections; /// -/// Provides persisted storage for AI provider connections while preserving the standard -/// named-and-sourced catalog operations used by connection managers and editors. +/// Provides the merged runtime view of AI provider connections across all registered +/// binding sources while preserving the standard named-and-sourced catalog operations +/// used by connection managers and editors. /// public interface IAIProviderConnectionStore : INamedSourceCatalog { diff --git a/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/IAIDeploymentStore.cs b/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/IAIDeploymentStore.cs index 1152310e..886385d9 100644 --- a/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/IAIDeploymentStore.cs +++ b/src/Abstractions/CrestApps.Core.AI.Abstractions/Deployments/IAIDeploymentStore.cs @@ -4,8 +4,9 @@ namespace CrestApps.Core.AI.Deployments; /// -/// Provides persisted storage for AI deployments while preserving the standard -/// named-and-sourced catalog operations used by deployment managers and editors. +/// Provides the merged runtime view of AI deployments across all registered binding +/// sources while preserving the standard named-and-sourced catalog operations used by +/// deployment managers and editors. /// public interface IAIDeploymentStore : INamedSourceCatalog { diff --git a/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md b/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md index 8c5312ba..9252d7ad 100644 --- a/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md +++ b/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md @@ -17,6 +17,7 @@ description: Initial standalone release notes for the CrestApps.Core repository. - includes a reference MVC host and an Aspire host for local composition and testing - includes a dedicated `CrestApps.Core.Tests` project for framework validation - publishes a framework-focused documentation site at [core.crestapps.com](https://core.crestapps.com) +- makes the generic AI deployment and connection catalog interfaces provider-backed database stores when YesSql or EntityCore is registered, while keeping `IAIDeploymentStore` and `IAIProviderConnectionStore` as the merged runtime views across configuration and database sources - merges appsettings-backed and UI-managed AI provider connections and deployments through runtime catalogs, so MVC selectors and AI resolution stay current without rebuilding options or restarting the app - exposes merged AI connection and deployment views through `INamedSourceCatalog` registrations, adds generic `Add*DocumentCatalog()` helpers for custom catalog registration, keeps deterministic name conflict handling so UI-managed records override conflicting appsettings entries, and standardizes settings so connections and deployments are configured separately - documents the default AI configuration sections (`CrestApps:AI:Connections` and `CrestApps:AI:Deployments`), tightens the quick-start path around Chat Interactions, and refreshes the docs navigation and landing page for faster onboarding diff --git a/src/CrestApps.Core.Docs/docs/core/ai-core.md b/src/CrestApps.Core.Docs/docs/core/ai-core.md index e6acc29e..f8b62431 100644 --- a/src/CrestApps.Core.Docs/docs/core/ai-core.md +++ b/src/CrestApps.Core.Docs/docs/core/ai-core.md @@ -58,7 +58,7 @@ An **AI connection** stores credentials and endpoint information for a specific | `INamedSourceCatalogSource` | `ConfigurationAIProviderConnectionSource` | Scoped | Reads connections from `appsettings.json` (Order 100) | | `ITemplateService` | *(from AddCoreAITemplating)* | Scoped | Template rendering | -It also chains `AddCoreAITemplating()` and `AddCoreServices()` automatically, and forwards `IAIDeploymentStore` to `INamedSourceCatalog`, `INamedCatalog`, `ISourceCatalog`, and `ICatalog` (and the same forwarding for `AIProviderConnection`). This means you can inject any of these catalog interfaces and they will resolve through the multi-source store. +It also chains `AddCoreAITemplating()` and `AddCoreServices()` automatically. `IAIDeploymentStore` and `IAIProviderConnectionStore` are the merged runtime views across all registered binding sources. The generic catalog interfaces for those two models are intentionally left unbound by `AddCoreAIServices()` alone so the persistence packages can map `INamedSourceCatalog`, `INamedCatalog`, `ISourceCatalog`, and `ICatalog` to the concrete database-backed catalogs. Optional format-specific packages stay opt-in. For example, Markdown-aware normalization lives in `CrestApps.Core.AI.Markdown`, so hosts that want Markdig-backed RAG normalization should register `AddCoreAIMarkdown()` explicitly instead of expecting `AddCoreAIServices()` to pull it in automatically. diff --git a/src/CrestApps.Core.Docs/docs/core/data-storage.md b/src/CrestApps.Core.Docs/docs/core/data-storage.md index 10628795..dd738a53 100644 --- a/src/CrestApps.Core.Docs/docs/core/data-storage.md +++ b/src/CrestApps.Core.Docs/docs/core/data-storage.md @@ -412,7 +412,7 @@ In `CrestApps.Core.Data.YesSql`, the shared convention is to keep each index typ Each feature requires specific stores to be registered. The table below lists what each feature needs and the corresponding registration calls for YesSql and Entity Framework Core. :::tip -`AddCoreAIServices()` registers the multi-source stores for `AIDeployment` and `AIProviderConnection` with appsettings-backed binding sources automatically. You only need to register the persistence-layer binding sources to enable database-backed storage. +`AddCoreAIServices()` registers the multi-source stores for `AIDeployment` and `AIProviderConnection` with appsettings-backed binding sources automatically. The persistence-layer packages then map the shared generic catalog interfaces to the concrete database-backed catalogs for those models. ::: ### Core AI (always required) @@ -424,7 +424,7 @@ Registered automatically by `AddCoreAIServices()`: | `AIDeployment` | `IAIDeploymentStore` | Auto-registered (multi-source, config source at Order 100) | | `AIProviderConnection` | `IAIProviderConnectionStore` | Auto-registered (multi-source, config source at Order 100) | -Add a DB binding source if you want database-backed deployments and connections: +Add a persistence provider if you want the generic catalog interfaces for deployments and connections to resolve to the database-backed store: ```csharp // Entity Framework Core (included in AddEntityCoreStores()) @@ -436,6 +436,13 @@ services.AddYesSqlNamedSourceBindingSource(); services.AddYesSqlNamedSourceBindingSource(); ``` +After a persistence provider is registered: + +| Model | All sources | Database-backed catalog | +|-------|-------------|------------------------| +| `AIDeployment` | `IAIDeploymentStore` | `INamedSourceCatalog` / `ICatalog` | +| `AIProviderConnection` | `IAIProviderConnectionStore` | `INamedSourceCatalog` / `ICatalog` | + ### AI Profiles | Model | Catalog registration | EntityCore | YesSql | @@ -691,7 +698,7 @@ The configuration sources read from `appsettings.json` using the sections config Hosts can override those lists to focus only on the standalone `Connections` array or to append additional provider-grouped sections for compatibility scenarios. -When a persistence package (YesSql or EntityCore) is added, it registers an additional **writable** DB binding source at Order 0, so database entries take priority over `appsettings.json` entries and all write operations go to the database. +When a persistence package (YesSql or EntityCore) is added, it registers an additional **writable** DB binding source at Order 0, so database entries take priority over `appsettings.json` entries and all write operations go to the database. `IAIDeploymentStore` and `IAIProviderConnectionStore` still remain the merged runtime view; resolve the shared generic catalog interfaces when you specifically want the database-backed store. ### Registering DB binding sources diff --git a/src/Primitives/CrestApps.Core.AI.Documents/Indexing/AIDocumentSearchIndexProfileHandler.cs b/src/Primitives/CrestApps.Core.AI.Documents/Indexing/AIDocumentSearchIndexProfileHandler.cs index db43445c..db4540ca 100644 --- a/src/Primitives/CrestApps.Core.AI.Documents/Indexing/AIDocumentSearchIndexProfileHandler.cs +++ b/src/Primitives/CrestApps.Core.AI.Documents/Indexing/AIDocumentSearchIndexProfileHandler.cs @@ -1,10 +1,9 @@ using CrestApps.Core.AI.Clients; +using CrestApps.Core.AI.Deployments; using CrestApps.Core.AI.Indexing; -using CrestApps.Core.AI.Models; using CrestApps.Core.Infrastructure; using CrestApps.Core.Infrastructure.Indexing; using CrestApps.Core.Infrastructure.Indexing.Models; -using CrestApps.Core.Services; using Microsoft.Extensions.Logging; namespace CrestApps.Core.AI.Documents.Indexing; @@ -21,7 +20,7 @@ public sealed class AIDocumentSearchIndexProfileHandler : EmbeddingSearchIndexPr /// The ai client factory. /// The logger. public AIDocumentSearchIndexProfileHandler( - ICatalog deploymentCatalog, + IAIDeploymentStore deploymentCatalog, IAIClientFactory aiClientFactory, ILogger logger) : base(IndexProfileTypes.AIDocuments, deploymentCatalog, aiClientFactory, logger) diff --git a/src/Primitives/CrestApps.Core.AI/Indexing/AIMemorySearchIndexProfileHandler.cs b/src/Primitives/CrestApps.Core.AI/Indexing/AIMemorySearchIndexProfileHandler.cs index f956be19..52515df6 100644 --- a/src/Primitives/CrestApps.Core.AI/Indexing/AIMemorySearchIndexProfileHandler.cs +++ b/src/Primitives/CrestApps.Core.AI/Indexing/AIMemorySearchIndexProfileHandler.cs @@ -1,8 +1,7 @@ using CrestApps.Core.AI.Clients; -using CrestApps.Core.AI.Models; +using CrestApps.Core.AI.Deployments; using CrestApps.Core.Infrastructure.Indexing; using CrestApps.Core.Infrastructure.Indexing.Models; -using CrestApps.Core.Services; using Microsoft.Extensions.Logging; namespace CrestApps.Core.AI.Indexing; @@ -27,7 +26,7 @@ public sealed class AIMemorySearchIndexProfileHandler : EmbeddingSearchIndexProf /// The ai client factory. /// The logger. public AIMemorySearchIndexProfileHandler( - ICatalog deploymentCatalog, + IAIDeploymentStore deploymentCatalog, IAIClientFactory aiClientFactory, ILogger logger) : base(IndexProfileTypes.AIMemory, deploymentCatalog, aiClientFactory, logger) @@ -40,43 +39,52 @@ public AIMemorySearchIndexProfileHandler( /// The vector dimensions. protected override IReadOnlyCollection BuildFields(int vectorDimensions) { - return [new SearchIndexField - { - Name = _memoryIdFieldName, - FieldType = SearchFieldType.Keyword, - IsKey = true, - IsFilterable = true, - }, new SearchIndexField - { - Name = _userIdFieldName, - FieldType = SearchFieldType.Keyword, - IsFilterable = true, - }, new SearchIndexField - { - Name = _nameFieldName, - FieldType = SearchFieldType.Text, - IsSearchable = true, - IsFilterable = true, - }, new SearchIndexField - { - Name = _descriptionFieldName, - FieldType = SearchFieldType.Text, - IsSearchable = true, - }, new SearchIndexField - { - Name = _contentFieldName, - FieldType = SearchFieldType.Text, - IsSearchable = true, - }, new SearchIndexField - { - Name = _updatedUtcFieldName, - FieldType = SearchFieldType.DateTime, - IsFilterable = true, - }, new SearchIndexField - { - Name = _embeddingFieldName, - FieldType = SearchFieldType.Vector, - VectorDimensions = vectorDimensions, - }, ]; + return + [ + new SearchIndexField + { + Name = _memoryIdFieldName, + FieldType = SearchFieldType.Keyword, + IsKey = true, + IsFilterable = true, + }, + new SearchIndexField + { + Name = _userIdFieldName, + FieldType = SearchFieldType.Keyword, + IsFilterable = true, + }, + new SearchIndexField + { + Name = _nameFieldName, + FieldType = SearchFieldType.Text, + IsSearchable = true, + IsFilterable = true, + }, + new SearchIndexField + { + Name = _descriptionFieldName, + FieldType = SearchFieldType.Text, + IsSearchable = true, + }, + new SearchIndexField + { + Name = _contentFieldName, + FieldType = SearchFieldType.Text, + IsSearchable = true, + }, + new SearchIndexField + { + Name = _updatedUtcFieldName, + FieldType = SearchFieldType.DateTime, + IsFilterable = true, + }, + new SearchIndexField + { + Name = _embeddingFieldName, + FieldType = SearchFieldType.Vector, + VectorDimensions = vectorDimensions, + }, + ]; } } diff --git a/src/Primitives/CrestApps.Core.AI/Indexing/DataSourceSearchIndexProfileHandler.cs b/src/Primitives/CrestApps.Core.AI/Indexing/DataSourceSearchIndexProfileHandler.cs index eb726127..a4dbf89d 100644 --- a/src/Primitives/CrestApps.Core.AI/Indexing/DataSourceSearchIndexProfileHandler.cs +++ b/src/Primitives/CrestApps.Core.AI/Indexing/DataSourceSearchIndexProfileHandler.cs @@ -1,9 +1,8 @@ using CrestApps.Core.AI.Clients; -using CrestApps.Core.AI.Models; +using CrestApps.Core.AI.Deployments; using CrestApps.Core.Infrastructure; using CrestApps.Core.Infrastructure.Indexing; using CrestApps.Core.Infrastructure.Indexing.Models; -using CrestApps.Core.Services; using Microsoft.Extensions.Logging; namespace CrestApps.Core.AI.Indexing; @@ -20,7 +19,7 @@ public sealed class DataSourceSearchIndexProfileHandler : EmbeddingSearchIndexPr /// The ai client factory. /// The logger. public DataSourceSearchIndexProfileHandler( - ICatalog deploymentCatalog, + IAIDeploymentStore deploymentCatalog, IAIClientFactory aiClientFactory, ILogger logger) : base(IndexProfileTypes.DataSource, deploymentCatalog, aiClientFactory, logger) @@ -33,51 +32,62 @@ public DataSourceSearchIndexProfileHandler( /// The vector dimensions. protected override IReadOnlyCollection BuildFields(int vectorDimensions) { - return [new SearchIndexField - { - Name = DataSourceConstants.ColumnNames.ChunkId, - FieldType = SearchFieldType.Keyword, - IsKey = true, - IsFilterable = true, - }, new SearchIndexField - { - Name = DataSourceConstants.ColumnNames.ReferenceId, - FieldType = SearchFieldType.Keyword, - IsFilterable = true, - }, new SearchIndexField - { - Name = DataSourceConstants.ColumnNames.DataSourceId, - FieldType = SearchFieldType.Keyword, - IsFilterable = true, - }, new SearchIndexField - { - Name = DataSourceConstants.ColumnNames.ReferenceType, - FieldType = SearchFieldType.Keyword, - IsFilterable = true, - }, new SearchIndexField - { - Name = DataSourceConstants.ColumnNames.ChunkIndex, - FieldType = SearchFieldType.Integer, - }, new SearchIndexField - { - Name = DataSourceConstants.ColumnNames.Title, - FieldType = SearchFieldType.Text, - IsSearchable = true, - }, new SearchIndexField - { - Name = DataSourceConstants.ColumnNames.Content, - FieldType = SearchFieldType.Text, - IsSearchable = true, - }, new SearchIndexField - { - Name = DataSourceConstants.ColumnNames.Timestamp, - FieldType = SearchFieldType.DateTime, - IsFilterable = true, - }, new SearchIndexField - { - Name = DataSourceConstants.ColumnNames.Embedding, - FieldType = SearchFieldType.Vector, - VectorDimensions = vectorDimensions, - }, ]; + return + [ + new SearchIndexField + { + Name = DataSourceConstants.ColumnNames.ChunkId, + FieldType = SearchFieldType.Keyword, + IsKey = true, + IsFilterable = true, + }, + new SearchIndexField + { + Name = DataSourceConstants.ColumnNames.ReferenceId, + FieldType = SearchFieldType.Keyword, + IsFilterable = true, + }, + new SearchIndexField + { + Name = DataSourceConstants.ColumnNames.DataSourceId, + FieldType = SearchFieldType.Keyword, + IsFilterable = true, + }, + new SearchIndexField + { + Name = DataSourceConstants.ColumnNames.ReferenceType, + FieldType = SearchFieldType.Keyword, + IsFilterable = true, + }, + new SearchIndexField + { + Name = DataSourceConstants.ColumnNames.ChunkIndex, + FieldType = SearchFieldType.Integer, + }, + new SearchIndexField + { + Name = DataSourceConstants.ColumnNames.Title, + FieldType = SearchFieldType.Text, + IsSearchable = true, + }, + new SearchIndexField + { + Name = DataSourceConstants.ColumnNames.Content, + FieldType = SearchFieldType.Text, + IsSearchable = true, + }, + new SearchIndexField + { + Name = DataSourceConstants.ColumnNames.Timestamp, + FieldType = SearchFieldType.DateTime, + IsFilterable = true, + }, + new SearchIndexField + { + Name = DataSourceConstants.ColumnNames.Embedding, + FieldType = SearchFieldType.Vector, + VectorDimensions = vectorDimensions, + }, + ]; } } diff --git a/src/Primitives/CrestApps.Core.AI/Indexing/EmbeddingSearchIndexProfileHandlerBase.cs b/src/Primitives/CrestApps.Core.AI/Indexing/EmbeddingSearchIndexProfileHandlerBase.cs index e7de9e13..b679f56f 100644 --- a/src/Primitives/CrestApps.Core.AI/Indexing/EmbeddingSearchIndexProfileHandlerBase.cs +++ b/src/Primitives/CrestApps.Core.AI/Indexing/EmbeddingSearchIndexProfileHandlerBase.cs @@ -1,9 +1,9 @@ using System.ComponentModel.DataAnnotations; using CrestApps.Core.AI.Clients; +using CrestApps.Core.AI.Deployments; using CrestApps.Core.AI.Models; using CrestApps.Core.Infrastructure.Indexing.Models; using CrestApps.Core.Models; -using CrestApps.Core.Services; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; @@ -15,7 +15,7 @@ namespace CrestApps.Core.AI.Indexing; public abstract class EmbeddingSearchIndexProfileHandlerBase : IndexProfileHandlerBase { private readonly string _type; - private readonly ICatalog _deploymentCatalog; + private readonly IAIDeploymentStore _deploymentCatalog; private readonly IAIClientFactory _aiClientFactory; private readonly ILogger _logger; @@ -28,7 +28,7 @@ public abstract class EmbeddingSearchIndexProfileHandlerBase : IndexProfileHandl /// The logger. protected EmbeddingSearchIndexProfileHandlerBase( string type, - ICatalog deploymentCatalog, + IAIDeploymentStore deploymentCatalog, IAIClientFactory aiClientFactory, ILogger logger) { diff --git a/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs index 2f7ae7d6..d624624b 100644 --- a/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs +++ b/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs @@ -145,18 +145,10 @@ public static IServiceCollection AddCoreAIServices(this IServiceCollection servi .AddScoped() .AddScoped(); - // Register the multi-source stores and forward all catalog interfaces. + // Register the multi-source stores used for merged runtime lookups. services.TryAddScoped(); - services.TryAddScoped>(sp => sp.GetRequiredService()); - services.TryAddScoped>(sp => sp.GetRequiredService()); - services.TryAddScoped>(sp => sp.GetRequiredService()); - services.TryAddScoped>(sp => sp.GetRequiredService()); services.TryAddScoped(); - services.TryAddScoped>(sp => sp.GetRequiredService()); - services.TryAddScoped>(sp => sp.GetRequiredService()); - services.TryAddScoped>(sp => sp.GetRequiredService()); - services.TryAddScoped>(sp => sp.GetRequiredService()); // Register the configuration-backed sources (Order=100, lower priority than DB). services.TryAddEnumerable(ServiceDescriptor.Scoped, ConfigurationAIDeploymentSource>()); diff --git a/src/Primitives/CrestApps.Core.AI/Services/DefaultAIClientFactory.cs b/src/Primitives/CrestApps.Core.AI/Services/DefaultAIClientFactory.cs index be94d58d..7f88aa53 100644 --- a/src/Primitives/CrestApps.Core.AI/Services/DefaultAIClientFactory.cs +++ b/src/Primitives/CrestApps.Core.AI/Services/DefaultAIClientFactory.cs @@ -1,6 +1,6 @@ using CrestApps.Core.AI.Clients; +using CrestApps.Core.AI.Connections; using CrestApps.Core.AI.Models; -using CrestApps.Core.Services; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; @@ -13,7 +13,7 @@ namespace CrestApps.Core.AI.Services; /// public sealed class DefaultAIClientFactory : IAIClientFactory { - private readonly INamedSourceCatalog _connectionCatalog; + private readonly IAIProviderConnectionStore _connectionCatalog; private readonly IEnumerable _clientProviders; private readonly IEnumerable _connectionHandlers; @@ -33,7 +33,7 @@ public DefaultAIClientFactory( IEnumerable connectionHandlers, IDataProtectionProvider dataProtectionProvider, IServiceProvider serviceProvider, - INamedSourceCatalog connectionCatalog) + IAIProviderConnectionStore connectionCatalog) { _connectionCatalog = connectionCatalog; _clientProviders = clientProviders; diff --git a/src/Primitives/CrestApps.Core.AI/Services/DefaultSpeechVoiceResolver.cs b/src/Primitives/CrestApps.Core.AI/Services/DefaultSpeechVoiceResolver.cs index fc4e545a..a09c8b6f 100644 --- a/src/Primitives/CrestApps.Core.AI/Services/DefaultSpeechVoiceResolver.cs +++ b/src/Primitives/CrestApps.Core.AI/Services/DefaultSpeechVoiceResolver.cs @@ -1,7 +1,7 @@ using CrestApps.Core.AI.Clients; +using CrestApps.Core.AI.Connections; using CrestApps.Core.AI.Models; using CrestApps.Core.AI.Speech; -using CrestApps.Core.Services; using Microsoft.AspNetCore.DataProtection; namespace CrestApps.Core.AI.Services; @@ -13,7 +13,7 @@ public sealed class DefaultSpeechVoiceResolver : ISpeechVoiceResolver { private readonly IEnumerable _clientProviders; private readonly IEnumerable _connectionHandlers; - private readonly INamedSourceCatalog _connectionCatalog; + private readonly IAIProviderConnectionStore _connectionCatalog; private readonly IDataProtectionProvider _dataProtectionProvider; /// @@ -27,7 +27,7 @@ public DefaultSpeechVoiceResolver( IEnumerable clientProviders, IEnumerable connectionHandlers, IDataProtectionProvider dataProtectionProvider, - INamedSourceCatalog connectionCatalog) + IAIProviderConnectionStore connectionCatalog) { _clientProviders = clientProviders; _connectionHandlers = connectionHandlers; diff --git a/src/Startup/CrestApps.Core.Aspire.AppHost/CrestApps.Core.Aspire.AppHost.csproj b/src/Startup/CrestApps.Core.Aspire.AppHost/CrestApps.Core.Aspire.AppHost.csproj index 2c6e1dda..81c71961 100644 --- a/src/Startup/CrestApps.Core.Aspire.AppHost/CrestApps.Core.Aspire.AppHost.csproj +++ b/src/Startup/CrestApps.Core.Aspire.AppHost/CrestApps.Core.Aspire.AppHost.csproj @@ -4,7 +4,6 @@ Exe - net10.0 true false diff --git a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIConnections/Create.razor b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIConnections/Create.razor index 194befaf..ef021493 100644 --- a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIConnections/Create.razor +++ b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIConnections/Create.razor @@ -1,9 +1,11 @@ @page "/ai/connections/create" @attribute [Authorize(Policy = "Admin")] +@using CrestApps.Core.AI.Connections @using CrestApps.Core.AI.Models @using CrestApps.Core.AI.Services @using CrestApps.Core.Blazor.Web.ViewModels @using CrestApps.Core.Services +@inject IAIProviderConnectionStore Store @inject INamedSourceCatalog Catalog @inject IStoreCommitter StoreCommitter @inject NavigationManager Navigation @@ -157,7 +159,7 @@ if (_nameError == null && !string.IsNullOrWhiteSpace(_model.Name)) { - var existing = await Catalog.FindByNameAsync(_model.Name); + var existing = await Store.FindByNameAsync(_model.Name); if (existing != null) { _nameError = "Name must be unique across appsettings and UI connections."; diff --git a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIConnections/Edit.razor b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIConnections/Edit.razor index 4753ebad..71f31135 100644 --- a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIConnections/Edit.razor +++ b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIConnections/Edit.razor @@ -1,9 +1,11 @@ @page "/ai/connections/edit/{Id}" @attribute [Authorize(Policy = "Admin")] +@using CrestApps.Core.AI.Connections @using CrestApps.Core.AI.Models @using CrestApps.Core.AI.Services @using CrestApps.Core.Blazor.Web.ViewModels @using CrestApps.Core.Services +@inject IAIProviderConnectionStore Store @inject INamedSourceCatalog Catalog @inject IStoreCommitter StoreCommitter @inject NavigationManager Navigation @@ -192,7 +194,7 @@ else if (_model != null) if (_nameError == null && !string.IsNullOrWhiteSpace(_model.Name)) { - var existing = await Catalog.FindByNameAsync(_model.Name); + var existing = await Store.FindByNameAsync(_model.Name); if (existing != null && !string.Equals(existing.ItemId, _model.ItemId, StringComparison.OrdinalIgnoreCase)) { _nameError = "Name must be unique across appsettings and UI connections."; diff --git a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIConnections/Index.razor b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIConnections/Index.razor index 72f0d657..77f2966f 100644 --- a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIConnections/Index.razor +++ b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIConnections/Index.razor @@ -1,9 +1,11 @@ @page "/ai/connections" @attribute [Authorize(Policy = "Admin")] +@using CrestApps.Core.AI.Connections @using CrestApps.Core.AI.Models @using CrestApps.Core.AI.Services @using CrestApps.Core.Blazor.Web.ViewModels @using CrestApps.Core.Services +@inject IAIProviderConnectionStore Store @inject INamedSourceCatalog Catalog @inject IStoreCommitter StoreCommitter @inject NavigationManager Navigation @@ -130,7 +132,7 @@ else private async Task LoadConnectionsAsync() { - var connections = await Catalog.GetAllAsync(); + var connections = await Store.GetAllAsync(); _connections = connections .Select(connection => diff --git a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIDeployments/Create.razor b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIDeployments/Create.razor index ceb75ef2..d16f28f0 100644 --- a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIDeployments/Create.razor +++ b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIDeployments/Create.razor @@ -1,12 +1,14 @@ @page "/ai/deployments/create" @attribute [Authorize(Policy = "Admin")] +@using CrestApps.Core.AI.Connections @using CrestApps.Core.AI.Deployments @using CrestApps.Core.AI.Models @using CrestApps.Core.AI.Services @using CrestApps.Core.Blazor.Web.ViewModels @using CrestApps.Core.Services -@inject IAIDeploymentStore DeploymentCatalog -@inject INamedSourceCatalog ConnectionCatalog +@inject IAIDeploymentStore DeploymentStore +@inject INamedSourceCatalog DeploymentCatalog +@inject IAIProviderConnectionStore ConnectionCatalog @inject IStoreCommitter StoreCommitter @inject NavigationManager Navigation @inject IJSRuntime JS @@ -265,7 +267,7 @@ if (!string.IsNullOrWhiteSpace(_model.TechnicalName)) { - var existing = await DeploymentCatalog.FindByNameAsync(_model.TechnicalName); + var existing = await DeploymentStore.FindByNameAsync(_model.TechnicalName); if (existing != null) { diff --git a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIDeployments/Edit.razor b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIDeployments/Edit.razor index a509e7a5..32d05e24 100644 --- a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIDeployments/Edit.razor +++ b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIDeployments/Edit.razor @@ -1,12 +1,14 @@ @page "/ai/deployments/edit/{Id}" @attribute [Authorize(Policy = "Admin")] +@using CrestApps.Core.AI.Connections @using CrestApps.Core.AI.Deployments @using CrestApps.Core.AI.Models @using CrestApps.Core.AI.Services @using CrestApps.Core.Blazor.Web.ViewModels @using CrestApps.Core.Services -@inject IAIDeploymentStore DeploymentCatalog -@inject INamedSourceCatalog ConnectionCatalog +@inject IAIDeploymentStore DeploymentStore +@inject INamedSourceCatalog DeploymentCatalog +@inject IAIProviderConnectionStore ConnectionCatalog @inject IStoreCommitter StoreCommitter @inject NavigationManager Navigation @@ -296,7 +298,7 @@ else if (!string.IsNullOrWhiteSpace(_model.TechnicalName)) { - var existing = await DeploymentCatalog.FindByNameAsync(_model.TechnicalName); + var existing = await DeploymentStore.FindByNameAsync(_model.TechnicalName); if (existing != null && !string.Equals(existing.ItemId, _model.ItemId, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIDeployments/Index.razor b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIDeployments/Index.razor index 432153ba..17dd48d1 100644 --- a/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIDeployments/Index.razor +++ b/src/Startup/CrestApps.Core.Blazor.Web/Components/Pages/AI/AIDeployments/Index.razor @@ -5,7 +5,8 @@ @using CrestApps.Core.AI.Services @using CrestApps.Core.Blazor.Web.ViewModels @using CrestApps.Core.Services -@inject IAIDeploymentStore Catalog +@inject IAIDeploymentStore Store +@inject INamedSourceCatalog Catalog @inject IStoreCommitter StoreCommitter @inject NavigationManager Navigation @inject IJSRuntime JS @@ -149,7 +150,7 @@ else private async Task LoadDeploymentsAsync() { - var deployments = await Catalog.GetAllAsync(); + var deployments = await Store.GetAllAsync(); _deployments = deployments .Select(deployment => diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIConnectionController.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIConnectionController.cs index d9fb483c..6443526b 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIConnectionController.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIConnectionController.cs @@ -1,3 +1,4 @@ +using CrestApps.Core.AI.Connections; using CrestApps.Core.AI.Models; using CrestApps.Core.AI.Services; using CrestApps.Core.Mvc.Web.Areas.AI.ViewModels; @@ -12,17 +13,35 @@ namespace CrestApps.Core.Mvc.Web.Areas.AI.Controllers; [Authorize(Policy = "Admin")] public sealed class AIConnectionController : Controller { + private static readonly List _providers = +[ + new("OpenAI", "OpenAI"), + new("Azure OpenAI", "Azure"), + new("Azure AI Inference (GitHub Models)", "AzureAIInference"), + new("Ollama", "Ollama"), + ]; + + private static readonly List _authTypes = + [ + new("API Key", "ApiKey"), + new("Default Azure Credential", "Default"), + new("Managed Identity", "ManagedIdentity"), + ]; + + private readonly IAIProviderConnectionStore _store; private readonly INamedSourceCatalog _catalog; - private static readonly List _providers = [new("OpenAI", "OpenAI"), new("Azure OpenAI", "Azure"), new("Azure AI Inference (GitHub Models)", "AzureAIInference"), new("Ollama", "Ollama"),]; - private static readonly List _authTypes = [new("API Key", "ApiKey"), new("Default Azure Credential", "Default"), new("Managed Identity", "ManagedIdentity"),]; - public AIConnectionController(INamedSourceCatalog catalog) + + public AIConnectionController( + IAIProviderConnectionStore store, + INamedSourceCatalog catalog) { + _store = store; _catalog = catalog; } public async Task Index() { - var connections = await _catalog.GetAllAsync(); + var connections = await _store.GetAllAsync(); var models = connections.Select(connection => { var model = AIConnectionViewModel.FromConnection(connection); @@ -192,7 +211,7 @@ private async Task ValidateUniqueNameAsync(string name, string currentItemId = n return; } - var existing = await _catalog.FindByNameAsync(name); + var existing = await _store.FindByNameAsync(name); if (existing != null && !string.Equals(existing.ItemId, currentItemId, StringComparison.OrdinalIgnoreCase)) { ModelState.AddModelError(nameof(AIConnectionViewModel.Name), "Name must be unique across appsettings and UI connections."); diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIDeploymentController.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIDeploymentController.cs index dc62254b..9918687d 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIDeploymentController.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIDeploymentController.cs @@ -1,3 +1,4 @@ +using CrestApps.Core.AI.Connections; using CrestApps.Core.AI.Deployments; using CrestApps.Core.AI.Models; using CrestApps.Core.AI.Services; @@ -13,8 +14,9 @@ namespace CrestApps.Core.Mvc.Web.Areas.AI.Controllers; [Authorize(Policy = "Admin")] public sealed class AIDeploymentController : Controller { - private readonly IAIDeploymentStore _deploymentCatalog; - private readonly INamedSourceCatalog _connectionCatalog; + private readonly IAIDeploymentStore _deploymentStore; + private readonly INamedSourceCatalog _deploymentCatalog; + private readonly IAIProviderConnectionStore _connectionCatalog; private static readonly List _providers = [ @@ -39,16 +41,18 @@ public sealed class AIDeploymentController : Controller }; public AIDeploymentController( - IAIDeploymentStore deploymentCatalog, - INamedSourceCatalog connectionCatalog) + IAIDeploymentStore deploymentStore, + INamedSourceCatalog deploymentCatalog, + IAIProviderConnectionStore connectionCatalog) { + _deploymentStore = deploymentStore; _deploymentCatalog = deploymentCatalog; _connectionCatalog = connectionCatalog; } public async Task Index() { - var deployments = await _deploymentCatalog.GetAllAsync(); + var deployments = await _deploymentStore.GetAllAsync(); return View(deployments .Select(deployment => @@ -263,7 +267,7 @@ private async Task ValidateUniqueNameAsync(string technicalName, string currentI return; } - var existing = await _deploymentCatalog.FindByNameAsync(technicalName); + var existing = await _deploymentStore.FindByNameAsync(technicalName); if (existing != null && !string.Equals(existing.ItemId, currentItemId, StringComparison.OrdinalIgnoreCase)) { ModelState.AddModelError(nameof(AIDeploymentViewModel.TechnicalName), "Technical name must be unique across appsettings and UI deployments."); diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIProfileController.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIProfileController.cs index 2cdb435e..1a3df9c0 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIProfileController.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AIProfileController.cs @@ -5,6 +5,7 @@ using CrestApps.Core.AI.Copilot.Models; using CrestApps.Core.AI.Copilot.Services; using CrestApps.Core.AI.DataSources; +using CrestApps.Core.AI.Deployments; using CrestApps.Core.AI.Documents; using CrestApps.Core.AI.Documents.Models; using CrestApps.Core.AI.Mcp.Models; @@ -33,7 +34,7 @@ namespace CrestApps.Core.Mvc.Web.Areas.AI.Controllers; public sealed class AIProfileController : Controller { private readonly IAIProfileManager _profileManager; - private readonly ICatalog _deploymentCatalog; + private readonly IAIDeploymentStore _deploymentCatalog; private readonly IAIProfileTemplateManager _templateManager; private readonly ICatalog _a2aConnectionCatalog; private readonly ICatalog _mcpConnectionCatalog; @@ -52,7 +53,7 @@ public sealed class AIProfileController : Controller private readonly IAIDataSourceStore _dataSourceStore; public AIProfileController( IAIProfileManager profileManager, - ICatalog deploymentCatalog, + IAIDeploymentStore deploymentCatalog, IAIProfileTemplateManager templateManager, ICatalog a2aConnectionCatalog, ICatalog mcpConnectionCatalog, diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AITemplateController.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AITemplateController.cs index 60a643e5..8327de98 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AITemplateController.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Controllers/AITemplateController.cs @@ -6,6 +6,7 @@ using CrestApps.Core.AI.Copilot.Models; using CrestApps.Core.AI.Copilot.Services; using CrestApps.Core.AI.DataSources; +using CrestApps.Core.AI.Deployments; using CrestApps.Core.AI.Documents.Models; using CrestApps.Core.AI.Mcp.Models; using CrestApps.Core.AI.Models; @@ -32,7 +33,7 @@ namespace CrestApps.Core.Mvc.Web.Areas.AI.Controllers; public sealed class AITemplateController : Controller { private readonly ICatalog _catalog; - private readonly ICatalog _deploymentCatalog; + private readonly IAIDeploymentStore _deploymentCatalog; private readonly ICatalog _a2aConnectionCatalog; private readonly ICatalog _mcpConnectionCatalog; private readonly IAIDataSourceStore _dataSourceStore; @@ -49,7 +50,7 @@ public sealed class AITemplateController : Controller private readonly AIToolDefinitionOptions _toolOptions; public AITemplateController( ICatalog catalog, - ICatalog deploymentCatalog, + IAIDeploymentStore deploymentCatalog, ICatalog a2aConnectionCatalog, ICatalog mcpConnectionCatalog, IAIDataSourceStore dataSourceStore, diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Controllers/ChatInteractionController.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Controllers/ChatInteractionController.cs index 0980856d..eb02c990 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Controllers/ChatInteractionController.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/ChatInteractions/Controllers/ChatInteractionController.cs @@ -40,7 +40,7 @@ public sealed class ChatInteractionController : Controller private readonly ICatalogManager _interactionManager; private readonly ICatalog _interactionCatalog; private readonly IChatInteractionPromptStore _promptStore; - private readonly ICatalog _deploymentCatalog; + private readonly IAIDeploymentStore _deploymentCatalog; private readonly ICatalog _a2aConnectionCatalog; private readonly ICatalog _mcpConnectionCatalog; private readonly ICatalog _dataSourceCatalog; @@ -69,7 +69,7 @@ public ChatInteractionController( ICatalogManager interactionManager, ICatalog interactionCatalog, IChatInteractionPromptStore promptStore, - ICatalog deploymentCatalog, + IAIDeploymentStore deploymentCatalog, ICatalog a2aConnectionCatalog, ICatalog mcpConnectionCatalog, ICatalog dataSourceCatalog, diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/Indexing/Controllers/IndexProfileController.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/Indexing/Controllers/IndexProfileController.cs index 8803d1db..1472d13e 100644 --- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/Indexing/Controllers/IndexProfileController.cs +++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/Indexing/Controllers/IndexProfileController.cs @@ -1,9 +1,9 @@ +using CrestApps.Core.AI.Deployments; using CrestApps.Core.AI.Models; using CrestApps.Core.Infrastructure.Indexing; using CrestApps.Core.Infrastructure.Indexing.Models; using CrestApps.Core.Models; using CrestApps.Core.Mvc.Web.Areas.Indexing.ViewModels; -using CrestApps.Core.Services; using CrestApps.Core.Support; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -18,7 +18,7 @@ public sealed class IndexProfileController : Controller { private readonly ISearchIndexProfileManager _indexProfileManager; private readonly ISearchIndexProfileProvisioningService _provisioningService; - private readonly ICatalog _deploymentCatalog; + private readonly IAIDeploymentStore _deploymentCatalog; private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private readonly IReadOnlyList _sources; @@ -26,7 +26,7 @@ public sealed class IndexProfileController : Controller public IndexProfileController( ISearchIndexProfileManager indexProfileManager, ISearchIndexProfileProvisioningService provisioningService, - ICatalog deploymentCatalog, + IAIDeploymentStore deploymentCatalog, IServiceProvider serviceProvider, IOptions sourceOptions, ILogger logger) diff --git a/src/Stores/CrestApps.Core.Data.EntityCore/ServiceCollectionExtensions.cs b/src/Stores/CrestApps.Core.Data.EntityCore/ServiceCollectionExtensions.cs index 5218212f..41dc9d96 100644 --- a/src/Stores/CrestApps.Core.Data.EntityCore/ServiceCollectionExtensions.cs +++ b/src/Stores/CrestApps.Core.Data.EntityCore/ServiceCollectionExtensions.cs @@ -269,6 +269,16 @@ public static IServiceCollection AddEntityCoreNamedSourceBindingSource(t services.AddScoped>(sp => new WritableCatalogBindingSource(sp.GetRequiredService>())); + services.RemoveAll>(); + services.RemoveAll>(); + services.RemoveAll>(); + services.RemoveAll>(); + + services.AddScoped>(sp => sp.GetRequiredService>()); + services.AddScoped>(sp => sp.GetRequiredService>()); + services.AddScoped>(sp => sp.GetRequiredService>()); + services.AddScoped>(sp => sp.GetRequiredService>()); + return services; } diff --git a/src/Stores/CrestApps.Core.Data.YesSql/ServiceCollectionExtensions.cs b/src/Stores/CrestApps.Core.Data.YesSql/ServiceCollectionExtensions.cs index 464029c3..0588fa7c 100644 --- a/src/Stores/CrestApps.Core.Data.YesSql/ServiceCollectionExtensions.cs +++ b/src/Stores/CrestApps.Core.Data.YesSql/ServiceCollectionExtensions.cs @@ -879,6 +879,16 @@ private static IServiceCollection AddYesSqlNamedSourceBindingSource>(sp => new WritableCatalogBindingSource(sp.GetRequiredService>())); + services.RemoveAll>(); + services.RemoveAll>(); + services.RemoveAll>(); + services.RemoveAll>(); + + services.AddScoped>(sp => sp.GetRequiredService>()); + services.AddScoped>(sp => sp.GetRequiredService>()); + services.AddScoped>(sp => sp.GetRequiredService>()); + services.AddScoped>(sp => sp.GetRequiredService>()); + return services; } } diff --git a/tests/CrestApps.Core.Tests/Core/Services/AIServiceCollectionExtensionsTests.cs b/tests/CrestApps.Core.Tests/Core/Services/AIServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..ad261ea8 --- /dev/null +++ b/tests/CrestApps.Core.Tests/Core/Services/AIServiceCollectionExtensionsTests.cs @@ -0,0 +1,100 @@ +using CrestApps.Core.AI; +using CrestApps.Core.AI.Connections; +using CrestApps.Core.AI.Deployments; +using CrestApps.Core.AI.Models; +using CrestApps.Core.AI.Services; +using CrestApps.Core.Data.EntityCore; +using CrestApps.Core.Data.YesSql; +using CrestApps.Core.Data.YesSql.Indexes.AI; +using CrestApps.Core.Services; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using YesSql; + +namespace CrestApps.Core.Tests.Core.Services; + +public sealed class AIServiceCollectionExtensionsTests +{ + [Fact] + public void AddCoreAIServices_DoesNotRegisterGenericConnectionOrDeploymentCatalogs() + { + var services = CreateBaseServices(); + services.AddCoreAIServices(); + + using var serviceProvider = services.BuildServiceProvider(); + using var scope = serviceProvider.CreateScope(); + var scopedServices = scope.ServiceProvider; + + Assert.IsType(scopedServices.GetRequiredService()); + Assert.IsType(scopedServices.GetRequiredService()); + Assert.Null(scopedServices.GetService>()); + Assert.Null(scopedServices.GetService>()); + Assert.Null(scopedServices.GetService>()); + Assert.Null(scopedServices.GetService>()); + Assert.Null(scopedServices.GetService>()); + Assert.Null(scopedServices.GetService>()); + Assert.Null(scopedServices.GetService>()); + Assert.Null(scopedServices.GetService>()); + } + + [Fact] + public void AddCoreAIServicesStoresEntityCore_RegistersDatabaseCatalogInterfaces() + { + var services = CreateBaseServices(); + services.AddCoreAIServices(); + services.AddCoreEntityCoreSqliteDataStore("Data Source=:memory:"); + services.AddCoreAIServicesStoresEntityCore(); + + using var serviceProvider = services.BuildServiceProvider(); + using var scope = serviceProvider.CreateScope(); + var scopedServices = scope.ServiceProvider; + + Assert.IsType(scopedServices.GetRequiredService()); + Assert.IsType(scopedServices.GetRequiredService()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + } + + [Fact] + public void AddCoreAIServicesStoresYesSql_RegistersDatabaseCatalogInterfaces() + { + var services = CreateBaseServices(); + services.AddScoped(_ => Mock.Of()); + services.AddOptions(); + services.AddCoreAIServices(); + services.AddCoreAIServicesStoresYesSql(); + + using var serviceProvider = services.BuildServiceProvider(); + using var scope = serviceProvider.CreateScope(); + var scopedServices = scope.ServiceProvider; + + Assert.IsType(scopedServices.GetRequiredService()); + Assert.IsType(scopedServices.GetRequiredService()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + Assert.IsType>(scopedServices.GetRequiredService>()); + } + + private static ServiceCollection CreateBaseServices() + { + var services = new ServiceCollection(); + + services.AddLogging(); + services.AddOptions(); + services.AddSingleton(TimeProvider.System); + services.AddSingleton(new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build()); + + return services; + } +} diff --git a/tests/CrestApps.Core.Tests/Framework/Mvc/AIProviderConnectionOptionsTests.cs b/tests/CrestApps.Core.Tests/Framework/Mvc/AIProviderConnectionOptionsTests.cs index 0c4d60c7..a5ef42b7 100644 --- a/tests/CrestApps.Core.Tests/Framework/Mvc/AIProviderConnectionOptionsTests.cs +++ b/tests/CrestApps.Core.Tests/Framework/Mvc/AIProviderConnectionOptionsTests.cs @@ -282,8 +282,9 @@ public async Task AddCrestAppsAI_WhenLegacyDeploymentSettingsConfigured_ShouldNo [Fact] public async Task AIDeploymentController_Create_ShouldPopulateConnectionsFromMergedCatalog() { - var deploymentCatalog = new Mock(); - var connectionCatalog = new Mock>(); + var deploymentStore = new Mock(); + var deploymentCatalog = new Mock>(); + var connectionCatalog = new Mock(); connectionCatalog.Setup(catalog => catalog.GetAllAsync()).ReturnsAsync( [ new AIProviderConnection @@ -303,6 +304,7 @@ public async Task AIDeploymentController_Create_ShouldPopulateConnectionsFromMer ]); var controller = new AIDeploymentController( + deploymentStore.Object, deploymentCatalog.Object, connectionCatalog.Object); @@ -318,8 +320,9 @@ public async Task AIDeploymentController_Create_ShouldPopulateConnectionsFromMer [Fact] public async Task AIConnectionController_Index_ShouldIncludeMergedConnectionsAndMarkConfiguredOnesReadOnly() { + var connectionStore = new Mock(); var connectionCatalog = new Mock>(); - connectionCatalog.Setup(catalog => catalog.GetAllAsync()).ReturnsAsync( + connectionStore.Setup(catalog => catalog.GetAllAsync()).ReturnsAsync( [ new AIProviderConnection { @@ -337,7 +340,7 @@ public async Task AIConnectionController_Index_ShouldIncludeMergedConnectionsAnd }, ]); - var controller = new AIConnectionController(connectionCatalog.Object); + var controller = new AIConnectionController(connectionStore.Object, connectionCatalog.Object); var result = await controller.Index(); @@ -351,8 +354,9 @@ public async Task AIConnectionController_Index_ShouldIncludeMergedConnectionsAnd [Fact] public async Task AIDeploymentController_Index_ShouldMarkConfiguredDeploymentsAsReadOnly() { - var deploymentCatalog = new Mock(); - deploymentCatalog.Setup(catalog => catalog.GetAllAsync()).ReturnsAsync( + var deploymentStore = new Mock(); + var deploymentCatalog = new Mock>(); + deploymentStore.Setup(catalog => catalog.GetAllAsync()).ReturnsAsync( [ new AIDeployment { @@ -364,8 +368,9 @@ public async Task AIDeploymentController_Index_ShouldMarkConfiguredDeploymentsAs }, ]); - var connectionCatalog = new Mock>(); + var connectionCatalog = new Mock(); var controller = new AIDeploymentController( + deploymentStore.Object, deploymentCatalog.Object, connectionCatalog.Object); diff --git a/tests/CrestApps.Core.Tests/Framework/Mvc/IndexProfileTypeRulesTests.cs b/tests/CrestApps.Core.Tests/Framework/Mvc/IndexProfileTypeRulesTests.cs index df5685cb..a9220150 100644 --- a/tests/CrestApps.Core.Tests/Framework/Mvc/IndexProfileTypeRulesTests.cs +++ b/tests/CrestApps.Core.Tests/Framework/Mvc/IndexProfileTypeRulesTests.cs @@ -1,4 +1,5 @@ using System.Text.Json.Nodes; +using CrestApps.Core.AI.Deployments; using CrestApps.Core.AI.Models; using CrestApps.Core.Elasticsearch; using CrestApps.Core.Infrastructure.Indexing; @@ -6,7 +7,6 @@ using CrestApps.Core.Models; using CrestApps.Core.Mvc.Web.Areas.Indexing.Controllers; using CrestApps.Core.Mvc.Web.Areas.Indexing.Services; -using CrestApps.Core.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -397,7 +397,7 @@ public ValueTask ValidateAsync(SearchIndexProfile model } } - private sealed class TestDeploymentCatalog : ICatalog + private sealed class TestDeploymentCatalog : IAIDeploymentStore { public ValueTask CreateAsync(AIDeployment entry, CancellationToken cancellationToken = default) { @@ -424,6 +424,21 @@ public ValueTask> GetAsync(IEnumerable return ValueTask.FromResult>([]); } + public ValueTask> GetAsync(string source, CancellationToken cancellationToken = default) + { + return ValueTask.FromResult>([]); + } + + public ValueTask GetAsync(string name, string source, CancellationToken cancellationToken = default) + { + return ValueTask.FromResult(null); + } + + public ValueTask FindByNameAsync(string name, CancellationToken cancellationToken = default) + { + return ValueTask.FromResult(null); + } + public ValueTask> PageAsync(int page, int pageSize, TQuery context, CancellationToken cancellationToken = default) where TQuery : QueryContext {