Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TClient>()` provider-registration helper to the completion-client-based `AddCoreAICompletionClient<TClient>(..., 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
Expand Down
4 changes: 2 additions & 2 deletions src/CrestApps.Core.Docs/docs/core/ai-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<AIOptions>(options =>
{
options.AddProfileSource("MySource", configure => { /* ... */ });
options.AddCompletionClient("MySource", configure => { /* ... */ });
options.AddDeploymentProvider("MyProvider", configure => { /* ... */ });
options.AddConnectionSource("MySource", configure => { /* ... */ });
});
Expand Down
8 changes: 4 additions & 4 deletions src/CrestApps.Core.Docs/docs/providers/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public sealed class AIProviderRegistration
{
public string Name { get; init; }
public AIProviderCapability Capabilities { get; init; }
public Action<AIProfileOptions> ConfigureProfile { get; init; }
public Action<AICompletionClientEntry> ConfigureCompletionClient { get; init; }
public Action<AIConnectionSourceOptions> ConfigureConnectionSource { get; init; }
}

Expand All @@ -241,7 +241,7 @@ public static class AIProviderServiceCollectionExtensions
configure(registration);

services.TryAddEnumerable(ServiceDescriptor.Scoped<IAIProvider, TProvider>());
services.AddCoreAIProfile(registration.Name, registration.ConfigureProfile);
services.AddCoreAICompletionClient(registration.Name, registration.ConfigureCompletionClient);
services.AddCoreAIConnectionSource(registration.Name, registration.ConfigureConnectionSource);
services.TryAddSingleton<IProviderCredentialResolverSelector, DefaultProviderCredentialResolverSelector>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IProviderCredentialResolver, DefaultProviderCredentialResolver>());
Expand All @@ -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 */ };
});

Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static IServiceCollection AddCoreAIAzureAIInference(this IServiceCollecti

services.TryAddEnumerable(ServiceDescriptor.Scoped<IAIClientProvider, AzureAIInferenceClientProvider>());

services.AddCoreAIProfile<ProviderAICompletionClient<AzureAIInferenceClientMarker>>(AzureAIInferenceConstants.ClientName, o =>
services.AddCoreAICompletionClient<ProviderAICompletionClient<AzureAIInferenceClientMarker>>(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.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static IServiceCollection AddCoreAIOllama(this IServiceCollection service

services.TryAddEnumerable(ServiceDescriptor.Scoped<IAIClientProvider, OllamaAIClientProvider>());

services.AddCoreAIProfile<ProviderAICompletionClient<OllamaClientMarker>>(OllamaConstants.ClientName, o =>
services.AddCoreAICompletionClient<ProviderAICompletionClient<OllamaClientMarker>>(OllamaConstants.ClientName, o =>
{
o.DisplayName = new LocalizedString("Ollama", "Ollama");
o.Description = new LocalizedString("Ollama", "Use locally hosted Ollama models for AI completion.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static IServiceCollection AddCoreAIAzureOpenAI(this IServiceCollection se
services.TryAddEnumerable(ServiceDescriptor.Scoped<IAIClientProvider, AzureSpeechClientProvider>());
services.TryAddEnumerable(ServiceDescriptor.Scoped<IAIProviderConnectionHandler, AzureOpenAIConnectionHandler>());

services.AddCoreAIProfile<AzureOpenAICompletionClient>(AzureOpenAIConstants.ClientName, o =>
services.AddCoreAICompletionClient<AzureOpenAICompletionClient>(AzureOpenAIConstants.ClientName, o =>
{
o.DisplayName = new LocalizedString("Azure OpenAI", "Azure OpenAI");
o.Description = new LocalizedString("Azure OpenAI", "Use Azure OpenAI models for AI completion.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static IServiceCollection AddCoreAIOpenAI(this IServiceCollection service
services.TryAddEnumerable(ServiceDescriptor.Scoped<IAIClientProvider, OpenAI.Services.OpenAIClientProvider>());
services.TryAddEnumerable(ServiceDescriptor.Scoped<IAIProviderConnectionHandler, OpenAIConnectionHandler>());

services.AddCoreAIProfile<ProviderAICompletionClient<OpenAIClientMarker>>(OpenAIConstants.ClientName, o =>
services.AddCoreAICompletionClient<ProviderAICompletionClient<OpenAIClientMarker>>(OpenAIConstants.ClientName, o =>
{
o.DisplayName = new LocalizedString("OpenAI", "OpenAI");
o.Description = new LocalizedString("OpenAI", "Use OpenAI models for AI completion.");
Expand Down
33 changes: 33 additions & 0 deletions src/Primitives/CrestApps.Core.AI/AICompletionClientEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.Extensions.Localization;

namespace CrestApps.Core.AI;

/// <summary>
/// Represents a registered AI completion client entry.
/// </summary>
public sealed class AICompletionClientEntry
{
/// <summary>
/// Initializes a new instance of the <see cref="AICompletionClientEntry"/> class.
/// </summary>
/// <param name="clientName">The client name.</param>
public AICompletionClientEntry(string clientName)
{
ClientName = clientName;
}

/// <summary>
/// Gets the client name.
/// </summary>
public string ClientName { get; }

/// <summary>
/// Gets or sets the display name.
/// </summary>
public LocalizedString DisplayName { get; set; }

/// <summary>
/// Gets or sets the description.
/// </summary>
public LocalizedString Description { get; set; }
}
21 changes: 11 additions & 10 deletions src/Primitives/CrestApps.Core.AI/AIOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace CrestApps.Core.AI;
public sealed class AIOptions
{
private readonly Dictionary<string, Type> _clients = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, AIProfileProviderEntry> _profileSources = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, AICompletionClientEntry> _completionClients = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, AIDeploymentProviderEntry> _deployments = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, AIProviderConnectionOptionsEntry> _connectionSources = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, AITemplateSourceEntry> _templateSources = new(StringComparer.OrdinalIgnoreCase);
Expand All @@ -26,13 +26,13 @@ public IReadOnlyDictionary<string, Type> Clients
}

/// <summary>
/// Gets the profile Sources.
/// Gets the registered completion clients.
/// </summary>
public IReadOnlyDictionary<string, AIProfileProviderEntry> ProfileSources
public IReadOnlyDictionary<string, AICompletionClientEntry> CompletionClients
{
get
{
return _profileSources;
return _completionClients;
}
}

Expand Down Expand Up @@ -77,16 +77,17 @@ internal void AddClient<TClient>(string name)
}

/// <summary>
/// Adds profile source.
/// Adds a completion client.
/// </summary>
/// <param name="clientName">The client name.</param>
/// <param name="configure">The configure.</param>
public void AddProfileSource(string clientName, Action<AIProfileProviderEntry> configure = null)
/// <param name="configure">The configuration action.</param>
public void AddCompletionClient(string clientName, Action<AICompletionClientEntry> 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)
Expand All @@ -99,7 +100,7 @@ public void AddProfileSource(string clientName, Action<AIProfileProviderEntry> c
entry.DisplayName = new LocalizedString(clientName, clientName);
}

_profileSources[clientName] = entry;
_completionClients[clientName] = entry;
}

/// <summary>
Expand Down
33 changes: 0 additions & 33 deletions src/Primitives/CrestApps.Core.AI/AIProfileProviderEntry.cs

This file was deleted.

17 changes: 12 additions & 5 deletions src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,12 @@ public static CrestAppsCoreBuilder AddAISuite(this CrestAppsCoreBuilder builder,
}

/// <summary>
/// Adds core ai profile.
/// Adds a core AI completion client and its registration metadata.
/// </summary>
public static IServiceCollection AddCoreAIProfile<TClient>(this IServiceCollection services, string clientName, Action<AIProfileProviderEntry> configure = null)
/// <param name="services">The service collection.</param>
/// <param name="clientName">The client name.</param>
/// <param name="configure">The configuration action.</param>
public static IServiceCollection AddCoreAICompletionClient<TClient>(this IServiceCollection services, string clientName, Action<AICompletionClientEntry> configure = null)
where TClient : class, IAICompletionClient
{
ArgumentNullException.ThrowIfNull(services);
Expand All @@ -230,7 +233,7 @@ public static IServiceCollection AddCoreAIProfile<TClient>(this IServiceCollecti
return services
.Configure<AIOptions>(o =>
{
o.AddProfileSource(clientName, configure);
o.AddCompletionClient(clientName, configure);
})
.AddCoreAICompletionClient<TClient>(clientName);
}
Expand All @@ -256,9 +259,13 @@ public static IServiceCollection AddCoreAIDeploymentProvider(this IServiceCollec
}

/// <summary>
/// Adds core ai completion client.
/// Adds a core AI completion client.
/// </summary>
public static IServiceCollection AddCoreAICompletionClient<TClient>(this IServiceCollection services, string clientName)
/// <param name="services">The service collection.</param>
/// <param name="clientName">The client name.</param>
public static IServiceCollection AddCoreAICompletionClient<TClient>(
this IServiceCollection services,
string clientName)
where TClient : class, IAICompletionClient
{
ArgumentNullException.ThrowIfNull(services);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ public void AIConnectionViewModel_ApplyTo_ShouldNormalizeAzureOpenAIProviderName
}

[Fact]
public void AddCoreAIProviders_ShouldRegisterDeploymentProvidersUsedByTheDeploymentCatalog()
public void AddCoreAIProviders_ShouldRegisterCompletionClientsAndDeploymentProvidersUsedByTheDeploymentCatalog()
{
var services = new ServiceCollection();
services.AddLogging();
Expand All @@ -509,12 +509,17 @@ public void AddCoreAIProviders_ShouldRegisterDeploymentProvidersUsedByTheDeploym

var options = serviceProvider.GetRequiredService<IOptions<AIOptions>>().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]
Expand Down
Loading