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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ public sealed class AIProviderConnectionCatalogOptions

public IList<string> ProviderSections { get; } =
[
"CrestApps:Providers",
"CrestApps:AI:Providers",
];
}
4 changes: 4 additions & 0 deletions src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ description: Initial standalone release notes for the CrestApps.Core repository.
- exposes merged AI connection and deployment views through `INamedSourceCatalog<T>` registrations, adds generic `Add*DocumentCatalog<TModel, TIndex, T>()` 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
- adds Debug-level diagnostics in `ConfigurationAIDeploymentCatalog` so hosts can trace which configuration sections were evaluated and how standalone deployments were parsed
- treats `CrestApps:AI:Deployments` as shared deployment metadata for all providers, not only providers with contained-connection support, so hosts can keep credentials in `Connections` while still declaring deployment names and types in appsettings
- evaluates every configured AI connection and deployment section when importing appsettings records, including provider-grouped connection sections and deployment entries that reference shared `ConnectionName` values
- 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
2 changes: 1 addition & 1 deletion src/CrestApps.Core.Docs/docs/core/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ builder.Services.AddCrestAppsCore(crestApps => crestApps
.AddChatInteractions()));
```

By default, connections are discovered from `CrestApps:AI:Connections` and standalone deployments are discovered from `CrestApps:AI:Deployments`.
By default, connections are discovered from `CrestApps:AI:Connections` and deployments are discovered from `CrestApps:AI:Deployments`. Connection-based deployments can reference a shared `ConnectionName`, while contained-connection deployments can embed provider-specific settings directly in the deployment entry.

## Problem & Solution

Expand Down
6 changes: 3 additions & 3 deletions src/CrestApps.Core.Docs/docs/core/mvc-example.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@ builder.Services.AddCrestAppsCore(crestApps => crestApps

The MVC sample still binds static provider metadata from `CrestApps:AI:Providers`, but mutable AI connections and deployments now come from first-class merged catalogs instead of rebuilding `AIProviderOptions` after admin edits.

`AddCoreAIAzureOpenAI()` also registers the `AzureSpeech` deployment provider used by MVC speech-to-text and text-to-speech selectors, so standalone Azure AI Services deployments from `CrestApps:AI:Deployments` participate in the same merged deployment catalog as UI-managed deployments.
`AddCoreAIAzureOpenAI()` also registers the `AzureSpeech` deployment provider used by MVC speech-to-text and text-to-speech selectors, so Azure AI Services deployments from `CrestApps:AI:Deployments` participate in the same merged deployment catalog as UI-managed deployments.

The MVC runtime now reads connection definitions from `CrestApps:AI:Connections`, provider-grouped connections under `CrestApps:Providers:{ProviderName}:Connections:{ConnectionName}` or `CrestApps:AI:Providers:{ProviderName}:Connections:{ConnectionName}`, and UI-managed connection records from the store into one merged connection catalog. The deployment catalog layers together UI-managed typed deployments and standalone `CrestApps:AI:Deployments` entries. That means dropdowns, deployment resolution, and connection resolution all see the same unified set without an app restart.
The MVC runtime now reads connection definitions from `CrestApps:AI:Connections`, provider-grouped connections under `CrestApps:Providers:{ProviderName}:Connections:{ConnectionName}` or `CrestApps:AI:Providers:{ProviderName}:Connections:{ConnectionName}`, and UI-managed connection records from the store into one merged connection catalog. The deployment catalog layers together UI-managed typed deployments and `CrestApps:AI:Deployments` entries from every configured section. Those deployment entries can either reference a shared `ConnectionName` or carry contained-connection settings directly. That means dropdowns, deployment resolution, and connection resolution all see the same unified set without an app restart.

Both merged catalogs also expose configurable section lists through `AIProviderConnectionCatalogOptions` and `AIDeploymentCatalogOptions`, so a host can append additional configuration paths without replacing the MVC/UI store integration. By default, connection discovery reads `CrestApps:AI:Connections`, `CrestApps:Providers`, and `CrestApps:AI:Providers`, while deployment discovery reads `CrestApps:AI:Deployments`.

Expand All @@ -169,7 +169,7 @@ Both merged catalogs also expose configurable section lists through `AIProviderC
}
```

Provider-grouped connection settings under `CrestApps:Providers:{ProviderName}:Connections:{ConnectionName}` and `CrestApps:AI:Providers:{ProviderName}:Connections:{ConnectionName}` still work too. The merged connection catalog keeps those provider-defined records and the `CrestApps:AI:Connections` array visible alongside UI-managed MVC connections, and the MVC AI Deployment editor reads that catalog directly when it builds the connection dropdown. Connection settings only describe the provider connection itself; deployment names and types belong in `CrestApps:AI:Deployments` or in the UI deployment editor.
Provider-grouped connection settings under `CrestApps:Providers:{ProviderName}:Connections:{ConnectionName}` and `CrestApps:AI:Providers:{ProviderName}:Connections:{ConnectionName}` still work too. The merged connection catalog keeps those provider-defined records and the `CrestApps:AI:Connections` array visible alongside UI-managed MVC connections, and the MVC AI Deployment editor reads that catalog directly when it builds the connection dropdown. Connection settings only describe the provider connection itself; deployment names and types belong in `CrestApps:AI:Deployments` or in the UI deployment editor, and config deployments can optionally point back to a shared connection by setting `ConnectionName`.


```json
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using CrestApps.Core.AI.Models;
using CrestApps.Core.Infrastructure;

namespace CrestApps.Core.AI;

public static class AIProviderConnectionEntryLegacyDeploymentExtensions
{
public static string GetLegacyChatDeploymentName(this AIProviderConnectionEntry connection)
=> GetLegacyString(connection, "ChatDeploymentName", "DeploymentName", "DefaultChatDeploymentName", "DefaultDeploymentName");

public static string GetLegacyUtilityDeploymentName(this AIProviderConnectionEntry connection)
=> GetLegacyString(connection, "UtilityDeploymentName", "DefaultUtilityDeploymentName");

public static string GetLegacyEmbeddingDeploymentName(this AIProviderConnectionEntry connection)
=> GetLegacyString(connection, "EmbeddingDeploymentName", "DefaultEmbeddingDeploymentName");

public static string GetLegacyImageDeploymentName(this AIProviderConnectionEntry connection)
=> GetLegacyString(connection, "ImagesDeploymentName", "DefaultImagesDeploymentName");

public static string GetLegacySpeechToTextDeploymentName(this AIProviderConnectionEntry connection)
=> GetLegacyString(connection, "SpeechToTextDeploymentName", "DefaultSpeechToTextDeploymentName");

private static string GetLegacyString(AIProviderConnectionEntry connection, params string[] keys)
{
ArgumentNullException.ThrowIfNull(connection);

foreach (var key in keys)
{
var value = connection.GetStringValue(key, false);

if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}

return null;
}
}
1 change: 1 addition & 0 deletions src/Primitives/CrestApps.Core.AI/CrestApps.Core.AI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

<ItemGroup>
<ProjectReference Include="../../Abstractions/CrestApps.Core.AI.Abstractions/CrestApps.Core.AI.Abstractions.csproj" />
<ProjectReference Include="../../Utilities/CrestApps.Core.Support/CrestApps.Core.Support.csproj" />
<ProjectReference Include="../CrestApps.Core/CrestApps.Core.csproj" />
<ProjectReference Include="../CrestApps.Core.Infrastructure/CrestApps.Core.Infrastructure.csproj" />
<ProjectReference Include="../CrestApps.Core.Templates/CrestApps.Core.Templates.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ namespace CrestApps.Core.AI.Models;
public sealed class AIDeploymentConfigurationEntry
{
/// <summary>
/// Gets or sets the deployment provider name for standalone configuration entries.
/// Gets or sets the deployment provider name for configuration entries.
/// </summary>
public string ProviderName { get; set; }
public string ClientName { get; set; }

/// <summary>
/// Gets or sets the unique technical deployment name used for lookups.
Expand All @@ -25,14 +25,15 @@ public sealed class AIDeploymentConfigurationEntry
public string ModelName { get; set; }

/// <summary>
/// Gets or sets the deployment capability types (Chat, Utility, Embedding, Image, SpeechToText, TextToSpeech).
/// Gets or sets the shared provider connection name for connection-based deployments.
/// Leave empty for contained-connection deployments.
/// </summary>
public AIDeploymentType Type { get; set; }
public string ConnectionName { get; set; }

/// <summary>
/// Gets or sets whether this deployment is the default for its type within its connection or provider.
/// Gets or sets the deployment capability types (Chat, Utility, Embedding, Image, SpeechToText, TextToSpeech).
/// </summary>
public bool IsDefault { get; set; }
public AIDeploymentType Type { get; set; }

/// <summary>
/// Gets or sets provider-specific properties for contained-connection deployments.
Expand Down
20 changes: 4 additions & 16 deletions src/Primitives/CrestApps.Core.AI/Services/AIClientProviderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ public ValueTask<IChatClient> GetChatClientAsync(AIProviderConnectionEntry conne
{
if (string.IsNullOrEmpty(deploymentName))
{
#pragma warning disable CS0618 // Obsolete deployment name methods retained for backward compatibility

deploymentName = connection.GetChatDeploymentOrDefaultName(false);
#pragma warning restore CS0618
deploymentName = connection.GetLegacyChatDeploymentName();
}

if (string.IsNullOrEmpty(deploymentName))
Expand All @@ -42,10 +39,7 @@ public ValueTask<IEmbeddingGenerator<string, Embedding<float>>> GetEmbeddingGene
{
if (string.IsNullOrEmpty(deploymentName))
{
#pragma warning disable CS0618 // Obsolete deployment name methods retained for backward compatibility

deploymentName = connection.GetEmbeddingDeploymentOrDefaultName(false);
#pragma warning restore CS0618
deploymentName = connection.GetLegacyEmbeddingDeploymentName();
}

if (string.IsNullOrEmpty(deploymentName))
Expand All @@ -66,10 +60,7 @@ public ValueTask<IImageGenerator> GetImageGeneratorAsync(AIProviderConnectionEnt
{
if (string.IsNullOrEmpty(deploymentName))
{
#pragma warning disable CS0618 // Obsolete deployment name methods retained for backward compatibility

deploymentName = connection.GetImagesDeploymentOrDefaultName(false);
#pragma warning restore CS0618
deploymentName = connection.GetLegacyImageDeploymentName();
}

if (string.IsNullOrEmpty(deploymentName))
Expand All @@ -89,10 +80,7 @@ public ValueTask<ISpeechToTextClient> GetSpeechToTextClientAsync(AIProviderConne
{
if (string.IsNullOrEmpty(deploymentName))
{
#pragma warning disable CS0618 // Obsolete deployment name methods retained for backward compatibility

deploymentName = connection.GetSpeechToTextDeploymentOrDefaultName(false);
#pragma warning restore CS0618
deploymentName = connection.GetLegacySpeechToTextDeploymentName();
}

if (string.IsNullOrEmpty(deploymentName))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using CrestApps.Core.AI.Deployments;
using CrestApps.Core.AI.Models;
using CrestApps.Core.Infrastructure;
using CrestApps.Core.Templates.Services;

namespace CrestApps.Core.AI.Services;
Expand Down Expand Up @@ -32,9 +31,7 @@ protected virtual string GetDefaultDeploymentName(AIProvider provider, string co
{
if (connectionName is not null && provider.Connections.TryGetValue(connectionName, out var connection))
{
#pragma warning disable CS0618 // Obsolete deployment name methods retained for backward compatibility
var deploymentName = connection.GetChatDeploymentOrDefaultName();
#pragma warning restore CS0618
var deploymentName = connection.GetLegacyChatDeploymentName();

if (!string.IsNullOrEmpty(deploymentName))
{
Expand All @@ -44,6 +41,7 @@ protected virtual string GetDefaultDeploymentName(AIProvider provider, string co

return null;
}

/// <summary>
/// Resolves a deployment name and connection name using the <see cref="IAIDeploymentManager"/>
/// with fallback to legacy connection entry values when they are still present.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json.Nodes;
using CrestApps.Core.AI.Models;
using CrestApps.Core.Support;
using Microsoft.AspNetCore.DataProtection;

namespace CrestApps.Core.AI.Services;
Expand All @@ -17,7 +18,7 @@ public static AIProviderConnectionEntry Create(AIDeployment deployment, IDataPro
foreach (var property in deployment.Properties)
{
values[property.Key] = property.Value is JsonNode jsonNode
? ConvertJsonNode(jsonNode)
? jsonNode.GetRawValue()
: property.Value;
}
}
Expand All @@ -26,25 +27,6 @@ public static AIProviderConnectionEntry Create(AIDeployment deployment, IDataPro

return new AIProviderConnectionEntry(values);
}

private static object ConvertJsonNode(JsonNode node)
{
return node switch
{
JsonObject jsonObject => jsonObject.ToDictionary(
property => property.Key,
property => ConvertJsonNode(property.Value),
StringComparer.OrdinalIgnoreCase),
JsonArray jsonArray => jsonArray.Select(ConvertJsonNode).ToList(),
JsonValue jsonValue when jsonValue.TryGetValue<string>(out var s) => s,
JsonValue jsonValue when jsonValue.TryGetValue<bool>(out var b) => b,
JsonValue jsonValue when jsonValue.TryGetValue<int>(out var i) => i,
JsonValue jsonValue when jsonValue.TryGetValue<long>(out var l) => l,
JsonValue jsonValue when jsonValue.TryGetValue<double>(out var d) => d,
_ => node?.ToString(),
};
}

private static void UnprotectApiKeys(IDictionary<string, object> values, IDataProtectionProvider dataProtectionProvider)
{
foreach (var (key, value) in values.ToList())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json.Nodes;
using CrestApps.Core.AI.Models;
using CrestApps.Core.Support;

namespace CrestApps.Core.AI.Services;

Expand All @@ -16,7 +17,7 @@ public static AIProviderConnectionEntry Create(AIProviderConnection connection)
foreach (var property in connection.Properties)
{
values[property.Key] = property.Value is JsonNode jsonNode
? ConvertJsonNode(jsonNode)
? jsonNode.GetRawValue()
: property.Value;
}
}
Expand All @@ -27,22 +28,4 @@ public static AIProviderConnectionEntry Create(AIProviderConnection connection)

return new AIProviderConnectionEntry(values);
}

private static object ConvertJsonNode(JsonNode node)
{
return node switch
{
JsonObject jsonObject => jsonObject.ToDictionary(
property => property.Key,
property => ConvertJsonNode(property.Value),
StringComparer.OrdinalIgnoreCase),
JsonArray jsonArray => jsonArray.Select(ConvertJsonNode).ToList(),
JsonValue jsonValue when jsonValue.TryGetValue<string>(out var s) => s,
JsonValue jsonValue when jsonValue.TryGetValue<bool>(out var b) => b,
JsonValue jsonValue when jsonValue.TryGetValue<int>(out var i) => i,
JsonValue jsonValue when jsonValue.TryGetValue<long>(out var l) => l,
JsonValue jsonValue when jsonValue.TryGetValue<double>(out var d) => d,
_ => node?.ToString(),
};
}
}
Loading
Loading