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 70b6c99c..6107ae83 100644 --- a/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md +++ b/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md @@ -28,6 +28,7 @@ description: Initial standalone release notes for the CrestApps.Core repository. - adds shared `JsonNode` support extensions for common string, boolean, and raw-value extraction so AI configuration parsing and Elasticsearch document readers reuse one implementation instead of duplicating private helpers - replaces removed obsolete connection-level deployment-name helpers with non-obsolete legacy lookup extensions for `AIProviderConnectionEntry`, keeping backward-compatible fallback resolution without depending on deleted APIs - renames `CrestApps.Core.AI.AISearch` to `CrestApps.Core.AI.Azure.AISearch`, groups the docs navigation around orchestrators, surfaces the Claude docs page, renames AI Providers to AI Clients, and updates the OpenAI docs to call out common OpenAI-compatible endpoints plus the dedicated Claude path +- renames the old `AddCoreAIProfile()` provider-registration helper to the completion-client-based `AddCoreAICompletionClient(..., configure)` overload, and updates `AIOptions` metadata from `ProfileSources` / `AIProfileProviderEntry` to `CompletionClients` / `AICompletionClientEntry` - aligns the built-in Entity Framework Core stores with the same `IStoreCommitter` unit-of-work pattern as YesSql and refreshes the storage/getting-started docs to explain MVC, Minimal API, SignalR, and background commit boundaries consistently - adds hierarchical document retrieval mode support so document RAG can rank on chunks and then inject full matched document text when hosts or profiles opt into that behavior - moves data-source source-to-knowledge-base synchronization into shared framework services so `AIDataSource` mappings react automatically to `ISearchDocumentManager` upserts and deletes, and adds nightly background reconciliation to repair drift without host-specific observer code diff --git a/src/CrestApps.Core.Docs/docs/core/ai-core.md b/src/CrestApps.Core.Docs/docs/core/ai-core.md index 53b8840d..d0ff967c 100644 --- a/src/CrestApps.Core.Docs/docs/core/ai-core.md +++ b/src/CrestApps.Core.Docs/docs/core/ai-core.md @@ -149,12 +149,12 @@ public interface IAICompletionClient ### `AIOptions` -Central options class for registering profile sources, deployment providers, connection sources, and template sources. By default, connections are loaded from `CrestApps:AI:Connections` and deployments are loaded from `CrestApps:AI:Deployments`. +Central options class for registering completion clients, deployment providers, connection sources, and template sources. By default, connections are loaded from `CrestApps:AI:Connections` and deployments are loaded from `CrestApps:AI:Deployments`. ```csharp services.Configure(options => { - options.AddProfileSource("MySource", configure => { /* ... */ }); + options.AddCompletionClient("MySource", configure => { /* ... */ }); options.AddDeploymentProvider("MyProvider", configure => { /* ... */ }); options.AddConnectionSource("MySource", configure => { /* ... */ }); }); diff --git a/src/CrestApps.Core.Docs/docs/providers/architecture.md b/src/CrestApps.Core.Docs/docs/providers/architecture.md index 07a35804..9d7feff1 100644 --- a/src/CrestApps.Core.Docs/docs/providers/architecture.md +++ b/src/CrestApps.Core.Docs/docs/providers/architecture.md @@ -226,7 +226,7 @@ public sealed class AIProviderRegistration { public string Name { get; init; } public AIProviderCapability Capabilities { get; init; } - public Action ConfigureProfile { get; init; } + public Action ConfigureCompletionClient { get; init; } public Action ConfigureConnectionSource { get; init; } } @@ -241,7 +241,7 @@ public static class AIProviderServiceCollectionExtensions configure(registration); services.TryAddEnumerable(ServiceDescriptor.Scoped()); - services.AddCoreAIProfile(registration.Name, registration.ConfigureProfile); + services.AddCoreAICompletionClient(registration.Name, registration.ConfigureCompletionClient); services.AddCoreAIConnectionSource(registration.Name, registration.ConfigureConnectionSource); services.TryAddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); @@ -261,7 +261,7 @@ public static IServiceCollection AddCoreAIOllama(this IServiceCollection service { r.Name = OllamaConstants.ClientName; r.Capabilities = AIProviderCapability.Chat | AIProviderCapability.Embeddings; - r.ConfigureProfile = profile => { /* Ollama-specific profile defaults */ }; + r.ConfigureCompletionClient = client => { /* Ollama-specific completion-client metadata */ }; r.ConfigureConnectionSource = source => { /* Ollama-specific connection metadata */ }; }); @@ -270,7 +270,7 @@ public static IServiceCollection AddCoreAIOllama(this IServiceCollection service } ``` -Compared to today, the only thing the package owns is the `OllamaProvider` itself, the `OllamaClientFactory`, and any provider-specific completion / response handler. The registration helper covers the boilerplate that today every package re-implements (profile source, connection source, default credential resolver wiring). +Compared to today, the only thing the package owns is the `OllamaProvider` itself, the `OllamaClientFactory`, and any provider-specific completion / response handler. The registration helper covers the boilerplate that today every package re-implements (completion-client metadata, connection source, default credential resolver wiring). ### 6. Reference port: Ollama diff --git a/src/Primitives/CrestApps.Core.AI.AzureAIInference/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI.AzureAIInference/ServiceCollectionExtensions.cs index c85197f5..438e0feb 100644 --- a/src/Primitives/CrestApps.Core.AI.AzureAIInference/ServiceCollectionExtensions.cs +++ b/src/Primitives/CrestApps.Core.AI.AzureAIInference/ServiceCollectionExtensions.cs @@ -23,7 +23,7 @@ public static IServiceCollection AddCoreAIAzureAIInference(this IServiceCollecti services.TryAddEnumerable(ServiceDescriptor.Scoped()); - services.AddCoreAIProfile>(AzureAIInferenceConstants.ClientName, o => + services.AddCoreAICompletionClient>(AzureAIInferenceConstants.ClientName, o => { o.DisplayName = new LocalizedString("Azure AI Inference", "Azure AI Inference / GitHub Models"); o.Description = new LocalizedString("Azure AI Inference", "Use Azure AI Inference or GitHub Models for AI completion."); diff --git a/src/Primitives/CrestApps.Core.AI.Ollama/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI.Ollama/ServiceCollectionExtensions.cs index a8e7b317..a5656f48 100644 --- a/src/Primitives/CrestApps.Core.AI.Ollama/ServiceCollectionExtensions.cs +++ b/src/Primitives/CrestApps.Core.AI.Ollama/ServiceCollectionExtensions.cs @@ -23,7 +23,7 @@ public static IServiceCollection AddCoreAIOllama(this IServiceCollection service services.TryAddEnumerable(ServiceDescriptor.Scoped()); - services.AddCoreAIProfile>(OllamaConstants.ClientName, o => + services.AddCoreAICompletionClient>(OllamaConstants.ClientName, o => { o.DisplayName = new LocalizedString("Ollama", "Ollama"); o.Description = new LocalizedString("Ollama", "Use locally hosted Ollama models for AI completion."); diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/ServiceCollectionExtensions.cs index 5e529266..b47ac8f6 100644 --- a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/ServiceCollectionExtensions.cs +++ b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/ServiceCollectionExtensions.cs @@ -30,7 +30,7 @@ public static IServiceCollection AddCoreAIAzureOpenAI(this IServiceCollection se services.TryAddEnumerable(ServiceDescriptor.Scoped()); services.TryAddEnumerable(ServiceDescriptor.Scoped()); - services.AddCoreAIProfile(AzureOpenAIConstants.ClientName, o => + services.AddCoreAICompletionClient(AzureOpenAIConstants.ClientName, o => { o.DisplayName = new LocalizedString("Azure OpenAI", "Azure OpenAI"); o.Description = new LocalizedString("Azure OpenAI", "Use Azure OpenAI models for AI completion."); diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI.OpenAI/ServiceCollectionExtensions.cs index de7fe10b..c06b4ff6 100644 --- a/src/Primitives/CrestApps.Core.AI.OpenAI/ServiceCollectionExtensions.cs +++ b/src/Primitives/CrestApps.Core.AI.OpenAI/ServiceCollectionExtensions.cs @@ -25,7 +25,7 @@ public static IServiceCollection AddCoreAIOpenAI(this IServiceCollection service services.TryAddEnumerable(ServiceDescriptor.Scoped()); services.TryAddEnumerable(ServiceDescriptor.Scoped()); - services.AddCoreAIProfile>(OpenAIConstants.ClientName, o => + services.AddCoreAICompletionClient>(OpenAIConstants.ClientName, o => { o.DisplayName = new LocalizedString("OpenAI", "OpenAI"); o.Description = new LocalizedString("OpenAI", "Use OpenAI models for AI completion."); diff --git a/src/Primitives/CrestApps.Core.AI/AICompletionClientEntry.cs b/src/Primitives/CrestApps.Core.AI/AICompletionClientEntry.cs new file mode 100644 index 00000000..cbfeb347 --- /dev/null +++ b/src/Primitives/CrestApps.Core.AI/AICompletionClientEntry.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Localization; + +namespace CrestApps.Core.AI; + +/// +/// Represents a registered AI completion client entry. +/// +public sealed class AICompletionClientEntry +{ + /// + /// Initializes a new instance of the class. + /// + /// The client name. + public AICompletionClientEntry(string clientName) + { + ClientName = clientName; + } + + /// + /// Gets the client name. + /// + public string ClientName { get; } + + /// + /// Gets or sets the display name. + /// + public LocalizedString DisplayName { get; set; } + + /// + /// Gets or sets the description. + /// + public LocalizedString Description { get; set; } +} diff --git a/src/Primitives/CrestApps.Core.AI/AIOptions.cs b/src/Primitives/CrestApps.Core.AI/AIOptions.cs index a4cb19c9..8473c08f 100644 --- a/src/Primitives/CrestApps.Core.AI/AIOptions.cs +++ b/src/Primitives/CrestApps.Core.AI/AIOptions.cs @@ -9,7 +9,7 @@ namespace CrestApps.Core.AI; public sealed class AIOptions { private readonly Dictionary _clients = new(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _profileSources = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _completionClients = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _deployments = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _connectionSources = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _templateSources = new(StringComparer.OrdinalIgnoreCase); @@ -26,13 +26,13 @@ public IReadOnlyDictionary Clients } /// - /// Gets the profile Sources. + /// Gets the registered completion clients. /// - public IReadOnlyDictionary ProfileSources + public IReadOnlyDictionary CompletionClients { get { - return _profileSources; + return _completionClients; } } @@ -77,16 +77,17 @@ internal void AddClient(string name) } /// - /// Adds profile source. + /// Adds a completion client. /// /// The client name. - /// The configure. - public void AddProfileSource(string clientName, Action configure = null) + /// The configuration action. + public void AddCompletionClient(string clientName, Action configure = null) { ArgumentException.ThrowIfNullOrEmpty(clientName); - if (!_profileSources.TryGetValue(clientName, out var entry)) + + if (!_completionClients.TryGetValue(clientName, out var entry)) { - entry = new AIProfileProviderEntry(clientName); + entry = new AICompletionClientEntry(clientName); } if (configure != null) @@ -99,7 +100,7 @@ public void AddProfileSource(string clientName, Action c entry.DisplayName = new LocalizedString(clientName, clientName); } - _profileSources[clientName] = entry; + _completionClients[clientName] = entry; } /// diff --git a/src/Primitives/CrestApps.Core.AI/AIProfileProviderEntry.cs b/src/Primitives/CrestApps.Core.AI/AIProfileProviderEntry.cs deleted file mode 100644 index 03c141ae..00000000 --- a/src/Primitives/CrestApps.Core.AI/AIProfileProviderEntry.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.Extensions.Localization; - -namespace CrestApps.Core.AI; - -/// -/// Represents the AI Profile Provider Entry. -/// -public sealed class AIProfileProviderEntry -{ - /// - /// Initializes a new instance of the class. - /// - /// The provider name. - public AIProfileProviderEntry(string providerName) - { - ProviderName = providerName; - } - - /// - /// Gets the provider Name. - /// - public string ProviderName { get; } - - /// - /// Gets or sets the display Name. - /// - public LocalizedString DisplayName { get; set; } - - /// - /// Gets or sets the description. - /// - public LocalizedString Description { get; set; } -} diff --git a/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs index abe51973..355a574d 100644 --- a/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs +++ b/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs @@ -219,9 +219,12 @@ public static CrestAppsCoreBuilder AddAISuite(this CrestAppsCoreBuilder builder, } /// - /// Adds core ai profile. + /// Adds a core AI completion client and its registration metadata. /// - public static IServiceCollection AddCoreAIProfile(this IServiceCollection services, string clientName, Action configure = null) + /// The service collection. + /// The client name. + /// The configuration action. + public static IServiceCollection AddCoreAICompletionClient(this IServiceCollection services, string clientName, Action configure = null) where TClient : class, IAICompletionClient { ArgumentNullException.ThrowIfNull(services); @@ -230,7 +233,7 @@ public static IServiceCollection AddCoreAIProfile(this IServiceCollecti return services .Configure(o => { - o.AddProfileSource(clientName, configure); + o.AddCompletionClient(clientName, configure); }) .AddCoreAICompletionClient(clientName); } @@ -256,9 +259,13 @@ public static IServiceCollection AddCoreAIDeploymentProvider(this IServiceCollec } /// - /// Adds core ai completion client. + /// Adds a core AI completion client. /// - public static IServiceCollection AddCoreAICompletionClient(this IServiceCollection services, string clientName) + /// The service collection. + /// The client name. + public static IServiceCollection AddCoreAICompletionClient( + this IServiceCollection services, + string clientName) where TClient : class, IAICompletionClient { ArgumentNullException.ThrowIfNull(services); diff --git a/tests/CrestApps.Core.Tests/Framework/Mvc/AIProviderConnectionOptionsTests.cs b/tests/CrestApps.Core.Tests/Framework/Mvc/AIProviderConnectionOptionsTests.cs index 0b2f59bd..0b851035 100644 --- a/tests/CrestApps.Core.Tests/Framework/Mvc/AIProviderConnectionOptionsTests.cs +++ b/tests/CrestApps.Core.Tests/Framework/Mvc/AIProviderConnectionOptionsTests.cs @@ -496,7 +496,7 @@ public void AIConnectionViewModel_ApplyTo_ShouldNormalizeAzureOpenAIProviderName } [Fact] - public void AddCoreAIProviders_ShouldRegisterDeploymentProvidersUsedByTheDeploymentCatalog() + public void AddCoreAIProviders_ShouldRegisterCompletionClientsAndDeploymentProvidersUsedByTheDeploymentCatalog() { var services = new ServiceCollection(); services.AddLogging(); @@ -509,12 +509,17 @@ public void AddCoreAIProviders_ShouldRegisterDeploymentProvidersUsedByTheDeploym var options = serviceProvider.GetRequiredService>().Value; + Assert.True(options.CompletionClients.ContainsKey(OpenAIConstants.ClientName)); + Assert.True(options.CompletionClients.ContainsKey(AzureOpenAIConstants.ClientName)); + Assert.True(options.CompletionClients.ContainsKey(OllamaConstants.ClientName)); + Assert.True(options.CompletionClients.ContainsKey(AzureAIInferenceConstants.ClientName)); Assert.True(options.Deployments.ContainsKey(OpenAIConstants.ClientName)); Assert.True(options.Deployments.ContainsKey(AzureOpenAIConstants.ClientName)); Assert.True(options.Deployments.ContainsKey(AzureOpenAIConstants.AzureSpeechClientName)); Assert.True(options.Deployments.ContainsKey(OllamaConstants.ClientName)); Assert.True(options.Deployments.ContainsKey(AzureAIInferenceConstants.ClientName)); Assert.True(options.Deployments[AzureOpenAIConstants.AzureSpeechClientName].UseContainedConnection); + Assert.Equal(OpenAIConstants.ClientName, options.CompletionClients[OpenAIConstants.ClientName].ClientName); } [Fact]