+
-
{emoji}
-
{title}
+
{description}
diff --git a/src/CrestApps.Core.Docs/src/components/HomepageFeatures/styles.module.css b/src/CrestApps.Core.Docs/src/components/HomepageFeatures/styles.module.css
index b8d7eed4..e400a9cb 100644
--- a/src/CrestApps.Core.Docs/src/components/HomepageFeatures/styles.module.css
+++ b/src/CrestApps.Core.Docs/src/components/HomepageFeatures/styles.module.css
@@ -3,6 +3,10 @@
width: 100%;
}
+.featureColumn {
+ margin-bottom: 1.5rem;
+}
+
.featureCard {
height: 100%;
padding: 1.5rem;
@@ -12,7 +16,24 @@
box-shadow: 0 0.75rem 2rem rgba(0, 0, 0, 0.06);
}
+.featureHeader {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-bottom: 1rem;
+}
+
.featureEmoji {
font-size: 2.5rem;
- margin-bottom: 1rem;
+ line-height: 1;
+}
+
+.featureTitle {
+ margin-bottom: 0;
+}
+
+@media (min-width: 997px) {
+ .featureColumn {
+ margin-bottom: 0;
+ }
}
diff --git a/src/Primitives/CrestApps.Core.AI.A2A/Functions/FindToolsForTaskFunction.cs b/src/Primitives/CrestApps.Core.AI.A2A/Functions/FindToolsForTaskFunction.cs
index 4617cb8d..52fd67bb 100644
--- a/src/Primitives/CrestApps.Core.AI.A2A/Functions/FindToolsForTaskFunction.cs
+++ b/src/Primitives/CrestApps.Core.AI.A2A/Functions/FindToolsForTaskFunction.cs
@@ -78,17 +78,29 @@ protected override async ValueTask
InvokeCoreAsync(AIFunctionArguments a
{
var connectionStore = arguments.Services.GetRequiredService>();
var toolRegistry = arguments.Services.GetRequiredService();
+
var context = new AICompletionContext
{
- A2AConnectionIds = (await connectionStore.GetAllAsync()).Where(connection => !string.IsNullOrWhiteSpace(connection.Endpoint)).Select(connection => connection.ItemId).ToArray(),
+ A2AConnectionIds = (await connectionStore.GetAllAsync())
+ .Where(connection => !string.IsNullOrWhiteSpace(connection.Endpoint))
+ .Select(connection => connection.ItemId)
+ .ToArray(),
};
+
var results = await toolRegistry.SearchAsync(taskDescription, maxResults, context, cancellationToken);
+
if (results is null || results.Count == 0)
{
return "No tools were found matching the given task description.";
}
- var tools = results.Select(r => new { name = r.Name, description = r.Description, source = r.Source.ToString(), }).ToList();
+ var tools = results.Select(r => new
+ {
+ name = r.Name,
+ description = r.Description,
+ source = r.Source.ToString(),
+ });
+
return JsonSerializer.Serialize(tools);
}
catch (Exception ex)
diff --git a/src/Primitives/CrestApps.Core.AI.AzureAIInference/AzureAIInferenceConstants.cs b/src/Primitives/CrestApps.Core.AI.AzureAIInference/AzureAIInferenceConstants.cs
index 31d2e4c7..f8c8642d 100644
--- a/src/Primitives/CrestApps.Core.AI.AzureAIInference/AzureAIInferenceConstants.cs
+++ b/src/Primitives/CrestApps.Core.AI.AzureAIInference/AzureAIInferenceConstants.cs
@@ -3,8 +3,4 @@ namespace CrestApps.Core.AI.AzureAIInference;
public static class AzureAIInferenceConstants
{
public const string ClientName = "AzureAIInference";
-
- public const string ProviderName = ClientName;
-
- public const string ImplementationName = "AzureAIInference";
}
diff --git a/src/Primitives/CrestApps.Core.AI.AzureAIInference/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI.AzureAIInference/ServiceCollectionExtensions.cs
index e94c451a..e6d98454 100644
--- a/src/Primitives/CrestApps.Core.AI.AzureAIInference/ServiceCollectionExtensions.cs
+++ b/src/Primitives/CrestApps.Core.AI.AzureAIInference/ServiceCollectionExtensions.cs
@@ -18,13 +18,13 @@ public static IServiceCollection AddCoreAIAzureAIInference(this IServiceCollecti
services.TryAddEnumerable(ServiceDescriptor.Scoped());
- services.AddCoreAIProfile(AzureAIInferenceConstants.ImplementationName, AzureAIInferenceConstants.ProviderName, o =>
+ services.AddCoreAIProfile(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.");
});
- services.AddCoreAIConnectionSource(AzureAIInferenceConstants.ProviderName, o =>
+ services.AddCoreAIConnectionSource(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.AzureAIInference/Services/AzureAIInferenceCompletionClient.cs b/src/Primitives/CrestApps.Core.AI.AzureAIInference/Services/AzureAIInferenceCompletionClient.cs
index a2a3f7e2..fbae96e4 100644
--- a/src/Primitives/CrestApps.Core.AI.AzureAIInference/Services/AzureAIInferenceCompletionClient.cs
+++ b/src/Primitives/CrestApps.Core.AI.AzureAIInference/Services/AzureAIInferenceCompletionClient.cs
@@ -12,15 +12,17 @@ namespace CrestApps.Core.AI.AzureAIInference.Services;
public sealed class AzureAIInferenceCompletionClient : NamedAICompletionClient
{
- public AzureAIInferenceCompletionClient(IAIClientFactory aIClientFactory, ILoggerFactory loggerFactory, IDistributedCache distributedCache, IServiceProvider serviceProvider, IOptions providerOptions, IEnumerable handlers, IOptions defaultOptions, ITemplateService aiTemplateService, IAIDeploymentManager deploymentManager) : base(AzureAIInferenceConstants.ImplementationName, aIClientFactory, distributedCache, loggerFactory, serviceProvider, providerOptions.Value, defaultOptions.Value, handlers, aiTemplateService, deploymentManager)
+ public AzureAIInferenceCompletionClient(
+ IAIClientFactory aIClientFactory,
+ ILoggerFactory loggerFactory,
+ IDistributedCache distributedCache,
+ IServiceProvider serviceProvider,
+ IOptions providerOptions,
+ IEnumerable handlers,
+ IOptions defaultOptions,
+ ITemplateService aiTemplateService,
+ IAIDeploymentManager deploymentManager)
+ : base(AzureAIInferenceConstants.ClientName, aIClientFactory, distributedCache, loggerFactory, serviceProvider, providerOptions.Value, defaultOptions.Value, handlers, aiTemplateService, deploymentManager)
{
}
-
- protected override string ProviderName
- {
- get
- {
- return AzureAIInferenceConstants.ProviderName;
- }
- }
}
diff --git a/src/Primitives/CrestApps.Core.AI.Chat/Hubs/AIChatHubCore.cs b/src/Primitives/CrestApps.Core.AI.Chat/Hubs/AIChatHubCore.cs
index 435ca73d..640c5a78 100644
--- a/src/Primitives/CrestApps.Core.AI.Chat/Hubs/AIChatHubCore.cs
+++ b/src/Primitives/CrestApps.Core.AI.Chat/Hubs/AIChatHubCore.cs
@@ -1157,12 +1157,14 @@ protected async Task StreamSpeechAsync(ITextToSpeechClient textToSpeechClient, s
await foreach (var update in textToSpeechClient.GetStreamingAudioAsync(speechText, options, cancellationToken))
{
var audioContent = update.Contents.OfType().FirstOrDefault();
+
if (audioContent?.Data is not { Length: > 0 } audioData)
{
continue;
}
var base64Audio = Convert.ToBase64String(audioData.ToArray());
+
await Clients.Caller.ReceiveAudioChunk(identifier, base64Audio, audioContent.MediaType ?? "audio/mp3");
}
@@ -1175,6 +1177,7 @@ protected async Task StreamSpeechAsync(ITextToSpeechClient textToSpeechClient, s
protected async Task StreamSentencesAsSpeechAsync(ITextToSpeechClient textToSpeechClient, Func getIdentifier, ChannelReader sentenceReader, string voiceName, CancellationToken cancellationToken)
{
var options = new TextToSpeechOptions();
+
if (!string.IsNullOrWhiteSpace(voiceName))
{
options.VoiceId = voiceName;
@@ -1184,6 +1187,7 @@ protected async Task StreamSentencesAsSpeechAsync(ITextToSpeechClient textToSpee
{
var identifier = getIdentifier();
var speechText = SpeechTextSanitizer.Sanitize(sentence);
+
if (string.IsNullOrWhiteSpace(speechText))
{
continue;
diff --git a/src/Primitives/CrestApps.Core.AI.Chat/Hubs/ChatInteractionHubBase.cs b/src/Primitives/CrestApps.Core.AI.Chat/Hubs/ChatInteractionHubBase.cs
index 8007028e..57731a2a 100644
--- a/src/Primitives/CrestApps.Core.AI.Chat/Hubs/ChatInteractionHubBase.cs
+++ b/src/Primitives/CrestApps.Core.AI.Chat/Hubs/ChatInteractionHubBase.cs
@@ -997,12 +997,14 @@ protected async Task StreamSpeechAsync(
await foreach (var update in textToSpeechClient.GetStreamingAudioAsync(speechText, options, cancellationToken))
{
var audioContent = update.Contents.OfType().FirstOrDefault();
+
if (audioContent?.Data is not { Length: > 0 } audioData)
{
continue;
}
var base64Audio = Convert.ToBase64String(audioData.ToArray());
+
await Clients.Caller.ReceiveAudioChunk(identifier, base64Audio, audioContent.MediaType ?? "audio/mp3");
}
@@ -1052,10 +1054,6 @@ protected async Task StreamSentencesAsSpeechAsync(
}
}
- // ═══════════════════════════════════════════════════════════════════
- // CONVERSATION LOOP — STT transcription + AI response + TTS
- // ═══════════════════════════════════════════════════════════════════
-
private async Task RunConversationLoopAsync(
string itemId,
IAsyncEnumerable audioChunks,
diff --git a/src/Primitives/CrestApps.Core.AI.Elasticsearch/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI.Elasticsearch/ServiceCollectionExtensions.cs
index 2a840db8..74d90918 100644
--- a/src/Primitives/CrestApps.Core.AI.Elasticsearch/ServiceCollectionExtensions.cs
+++ b/src/Primitives/CrestApps.Core.AI.Elasticsearch/ServiceCollectionExtensions.cs
@@ -60,7 +60,9 @@ public static IServiceCollection AddCoreElasticsearchSource(this IServiceCollect
ArgumentNullException.ThrowIfNull(type);
services.AddCoreAIDefaultIndexProfileHandler();
- services.Configure(options => options.AddOrUpdate(ElasticsearchConstants.ProviderName, "Elasticsearch", type, configure));
+ services.Configure(options => options
+ .AddOrUpdate(ElasticsearchConstants.ProviderName, "Elasticsearch", type, configure)
+ );
return services;
}
diff --git a/src/Primitives/CrestApps.Core.AI.Mcp/Services/DefaultMcpCapabilityResolver.cs b/src/Primitives/CrestApps.Core.AI.Mcp/Services/DefaultMcpCapabilityResolver.cs
index bfb28d7d..85a2bb3b 100644
--- a/src/Primitives/CrestApps.Core.AI.Mcp/Services/DefaultMcpCapabilityResolver.cs
+++ b/src/Primitives/CrestApps.Core.AI.Mcp/Services/DefaultMcpCapabilityResolver.cs
@@ -188,6 +188,7 @@ private async Task> TryEmbeddingMatchAsync(
}
var promptVector = NormalizeL2(promptEmbeddings[0].Vector.ToArray());
+
var candidates = new List();
foreach (var embedding in capabilityEmbeddings)
diff --git a/src/Primitives/CrestApps.Core.AI.Ollama/OllamaConstants.cs b/src/Primitives/CrestApps.Core.AI.Ollama/OllamaConstants.cs
index 53e43f81..31a4041d 100644
--- a/src/Primitives/CrestApps.Core.AI.Ollama/OllamaConstants.cs
+++ b/src/Primitives/CrestApps.Core.AI.Ollama/OllamaConstants.cs
@@ -2,6 +2,5 @@ namespace CrestApps.Core.AI.Ollama;
public static class OllamaConstants
{
- public const string ProviderName = "Ollama";
- public const string ImplementationName = "Ollama";
+ public const string ClientName = "Ollama";
}
diff --git a/src/Primitives/CrestApps.Core.AI.Ollama/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI.Ollama/ServiceCollectionExtensions.cs
index b60e910b..3f6076bf 100644
--- a/src/Primitives/CrestApps.Core.AI.Ollama/ServiceCollectionExtensions.cs
+++ b/src/Primitives/CrestApps.Core.AI.Ollama/ServiceCollectionExtensions.cs
@@ -18,13 +18,13 @@ public static IServiceCollection AddCoreAIOllama(this IServiceCollection service
services.TryAddEnumerable(ServiceDescriptor.Scoped());
- services.AddCoreAIProfile(OllamaConstants.ImplementationName, OllamaConstants.ProviderName, o =>
+ services.AddCoreAIProfile(OllamaConstants.ClientName, o =>
{
o.DisplayName = new LocalizedString("Ollama", "Ollama");
o.Description = new LocalizedString("Ollama", "Use locally hosted Ollama models for AI completion.");
});
- services.AddCoreAIConnectionSource(OllamaConstants.ProviderName, o =>
+ services.AddCoreAIConnectionSource(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.Ollama/Services/OllamaAIClientProvider.cs b/src/Primitives/CrestApps.Core.AI.Ollama/Services/OllamaAIClientProvider.cs
index 1150ce5f..8a381314 100644
--- a/src/Primitives/CrestApps.Core.AI.Ollama/Services/OllamaAIClientProvider.cs
+++ b/src/Primitives/CrestApps.Core.AI.Ollama/Services/OllamaAIClientProvider.cs
@@ -14,7 +14,7 @@ public OllamaAIClientProvider(IServiceProvider serviceProvider) : base(servicePr
protected override string GetProviderName()
{
- return OllamaConstants.ProviderName;
+ return OllamaConstants.ClientName;
}
protected override IChatClient GetChatClient(AIProviderConnectionEntry connection, string deploymentName)
diff --git a/src/Primitives/CrestApps.Core.AI.Ollama/Services/OllamaCompletionClient.cs b/src/Primitives/CrestApps.Core.AI.Ollama/Services/OllamaCompletionClient.cs
index c43bbfb7..77a910f0 100644
--- a/src/Primitives/CrestApps.Core.AI.Ollama/Services/OllamaCompletionClient.cs
+++ b/src/Primitives/CrestApps.Core.AI.Ollama/Services/OllamaCompletionClient.cs
@@ -12,15 +12,17 @@ namespace CrestApps.Core.AI.Ollama.Services;
public sealed class OllamaCompletionClient : NamedAICompletionClient
{
- public OllamaCompletionClient(IAIClientFactory aIClientFactory, ILoggerFactory loggerFactory, IDistributedCache distributedCache, IServiceProvider serviceProvider, IOptions providerOptions, IEnumerable handlers, IOptions defaultOptions, ITemplateService aiTemplateService, IAIDeploymentManager deploymentManager) : base(OllamaConstants.ImplementationName, aIClientFactory, distributedCache, loggerFactory, serviceProvider, providerOptions.Value, defaultOptions.Value, handlers, aiTemplateService, deploymentManager)
+ public OllamaCompletionClient(
+ IAIClientFactory aIClientFactory,
+ ILoggerFactory loggerFactory,
+ IDistributedCache distributedCache,
+ IServiceProvider serviceProvider,
+ IOptions providerOptions,
+ IEnumerable handlers,
+ IOptions defaultOptions,
+ ITemplateService aiTemplateService,
+ IAIDeploymentManager deploymentManager)
+ : base(OllamaConstants.ClientName, aIClientFactory, distributedCache, loggerFactory, serviceProvider, providerOptions.Value, defaultOptions.Value, handlers, aiTemplateService, deploymentManager)
{
}
-
- protected override string ProviderName
- {
- get
- {
- return OllamaConstants.ProviderName;
- }
- }
}
diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/AzureClientOptionsConfiguration.cs b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/AzureClientOptionsConfiguration.cs
new file mode 100644
index 00000000..67ea0d6f
--- /dev/null
+++ b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/AzureClientOptionsConfiguration.cs
@@ -0,0 +1,22 @@
+using CrestApps.Core.AI.OpenAI.Azure.Models;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace CrestApps.Core.AI.OpenAI.Azure;
+
+internal sealed class AzureClientOptionsConfiguration : IConfigureOptions
+{
+ private readonly IConfiguration _configuration;
+
+ public AzureClientOptionsConfiguration(IConfiguration configuration)
+ {
+ _configuration = configuration;
+ }
+
+ public void Configure(AzureClientOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+
+ _configuration.GetSection("CrestApps:AI:AzureClient").Bind(options);
+ }
+}
diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/AzureOpenAIConstants.cs b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/AzureOpenAIConstants.cs
index d7f53454..e55c658a 100644
--- a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/AzureOpenAIConstants.cs
+++ b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/AzureOpenAIConstants.cs
@@ -2,9 +2,7 @@ namespace CrestApps.Core.AI.OpenAI.Azure;
public static class AzureOpenAIConstants
{
- public const string ProviderName = "Azure";
+ public const string ClientName = "Azure";
- public const string ClientName = ProviderName;
-
- public const string AzureSpeechProviderName = "AzureSpeech";
+ public const string AzureSpeechClientName = "AzureSpeech";
}
diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Models/AzureClientOptions.cs b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Models/AzureClientOptions.cs
new file mode 100644
index 00000000..3db096bd
--- /dev/null
+++ b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Models/AzureClientOptions.cs
@@ -0,0 +1,10 @@
+namespace CrestApps.Core.AI.OpenAI.Azure.Models;
+
+public sealed class AzureClientOptions
+{
+ public bool EnableLogging { get; set; }
+
+ public bool EnableMessageContentLogging { get; set; }
+
+ public bool EnableMessageLogging { get; set; }
+}
diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Models/AzureOpenAIConnectionMetadata.cs b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Models/AzureOpenAIConnectionMetadata.cs
deleted file mode 100644
index f0866a26..00000000
--- a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Models/AzureOpenAIConnectionMetadata.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using CrestApps.Core.Azure.Models;
-
-namespace CrestApps.Core.AI.OpenAI.Azure.Models;
-
-public class AzureOpenAIConnectionMetadata : AzureConnectionMetadata
-{
- public bool EnableLogging { get; set; }
-}
diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/ServiceCollectionExtensions.cs
index c5a14efd..f1e37ac8 100644
--- a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/ServiceCollectionExtensions.cs
+++ b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/ServiceCollectionExtensions.cs
@@ -1,9 +1,11 @@
using CrestApps.Core.AI.Clients;
+using CrestApps.Core.AI.OpenAI.Azure.Models;
using CrestApps.Core.AI.OpenAI.Azure.Services;
using CrestApps.Core.Builders;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Options;
namespace CrestApps.Core.AI.OpenAI.Azure;
@@ -16,22 +18,24 @@ public static IServiceCollection AddCoreAIAzureOpenAI(this IServiceCollection se
{
ArgumentNullException.ThrowIfNull(services);
+ services.AddOptions();
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, AzureClientOptionsConfiguration>());
services.TryAddEnumerable(ServiceDescriptor.Scoped());
services.TryAddEnumerable(ServiceDescriptor.Scoped());
- services.AddCoreAIProfile(AzureOpenAIConstants.ProviderName, AzureOpenAIConstants.ProviderName, o =>
+ services.AddCoreAIProfile(AzureOpenAIConstants.ClientName, o =>
{
o.DisplayName = new LocalizedString("Azure OpenAI", "Azure OpenAI");
o.Description = new LocalizedString("Azure OpenAI", "Use Azure OpenAI models for AI completion.");
});
- services.AddCoreAIConnectionSource(AzureOpenAIConstants.ProviderName, o =>
+ services.AddCoreAIConnectionSource(AzureOpenAIConstants.ClientName, o =>
{
o.DisplayName = new LocalizedString("Azure OpenAI", "Azure OpenAI");
o.Description = new LocalizedString("Azure OpenAI", "Use Azure OpenAI models for AI completion.");
});
- services.AddCoreAIDeploymentProvider(AzureOpenAIConstants.AzureSpeechProviderName, o =>
+ services.AddCoreAIDeploymentProvider(AzureOpenAIConstants.AzureSpeechClientName, o =>
{
o.SupportsContainedConnection = true;
o.DisplayName = new LocalizedString("Azure AI Services", "Azure AI Services");
@@ -46,6 +50,7 @@ public static CrestAppsAISuiteBuilder AddAzureOpenAI(this CrestAppsAISuiteBuilde
ArgumentNullException.ThrowIfNull(builder);
builder.Services.AddCoreAIAzureOpenAI();
+
return builder;
}
}
diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureOpenAIClientFactory.cs b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureOpenAIClientFactory.cs
new file mode 100644
index 00000000..b704b4c3
--- /dev/null
+++ b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureOpenAIClientFactory.cs
@@ -0,0 +1,46 @@
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using CrestApps.Core.AI.Models;
+using CrestApps.Core.AI.OpenAI.Azure.Models;
+using CrestApps.Core.Azure;
+using CrestApps.Core.Azure.Models;
+using CrestApps.Core.Infrastructure;
+using Microsoft.Extensions.Logging;
+
+namespace CrestApps.Core.AI.OpenAI.Azure.Services;
+
+internal static class AzureOpenAIClientFactory
+{
+ public static AzureOpenAIClient Create(
+ AIProviderConnectionEntry connection,
+ ILoggerFactory loggerFactory,
+ AzureClientOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(connection);
+ ArgumentNullException.ThrowIfNull(loggerFactory);
+
+ var endpoint = connection.GetEndpoint();
+ var clientOptions = new AzureOpenAIClientOptions
+ {
+ ClientLoggingOptions = new ClientLoggingOptions
+ {
+ LoggerFactory = loggerFactory,
+ EnableLogging = options?.EnableLogging ?? false,
+ EnableMessageLogging = options?.EnableMessageLogging ?? false,
+ EnableMessageContentLogging = options?.EnableMessageContentLogging ?? false,
+ },
+ };
+
+ var identityId = connection.GetIdentityId();
+
+ return connection.GetAzureAuthenticationType() switch
+ {
+ AzureAuthenticationType.ApiKey => new AzureOpenAIClient(endpoint, new ApiKeyCredential(connection.GetApiKey()), clientOptions),
+ AzureAuthenticationType.ManagedIdentity => new AzureOpenAIClient(endpoint, new ManagedIdentityCredential(string.IsNullOrEmpty(identityId) ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.FromUserAssignedClientId(identityId)), clientOptions),
+ AzureAuthenticationType.Default => new AzureOpenAIClient(endpoint, new DefaultAzureCredential(), clientOptions),
+ _ => throw new NotSupportedException("The provided authentication type is not supported."),
+ };
+ }
+}
diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureOpenAIClientProvider.cs b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureOpenAIClientProvider.cs
index b99e4fc1..cf3e63a6 100644
--- a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureOpenAIClientProvider.cs
+++ b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureOpenAIClientProvider.cs
@@ -1,41 +1,42 @@
-using System.ClientModel;
-using System.ClientModel.Primitives;
using Azure.AI.OpenAI;
-using Azure.Identity;
using CrestApps.Core.AI.Models;
+using CrestApps.Core.AI.OpenAI.Azure.Models;
using CrestApps.Core.AI.Services;
-using CrestApps.Core.Azure;
-using CrestApps.Core.Azure.Models;
-using CrestApps.Core.Infrastructure;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
namespace CrestApps.Core.AI.OpenAI.Azure.Services;
public sealed class AzureOpenAIClientProvider : AIClientProviderBase
{
private readonly ILoggerFactory _loggerFactory;
+ private readonly AzureClientOptions _azureClientSettings;
+
protected override string GetProviderName()
{
return AzureOpenAIConstants.ClientName;
}
- public AzureOpenAIClientProvider(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) : base(serviceProvider)
+ public AzureOpenAIClientProvider(
+ IServiceProvider serviceProvider,
+ ILoggerFactory loggerFactory,
+ IOptionsSnapshot azureClientSettings) : base(serviceProvider)
{
_loggerFactory = loggerFactory;
+ _azureClientSettings = azureClientSettings.Value;
}
protected override IChatClient GetChatClient(AIProviderConnectionEntry connection, string deploymentName)
{
- return GetClient(connection, connection.GetEndpoint())
+ return GetClient(connection)
.GetChatClient(deploymentName)
.AsIChatClient();
}
protected override IEmbeddingGenerator> GetEmbeddingGenerator(AIProviderConnectionEntry connection, string deploymentName)
{
- var endpoint = connection.GetEndpoint();
- return GetClient(connection, endpoint)
+ return GetClient(connection)
.GetEmbeddingClient(deploymentName)
.AsIEmbeddingGenerator();
}
@@ -44,44 +45,22 @@ protected override IEmbeddingGenerator> GetEmbeddingGen
protected override IImageGenerator GetImageGenerator(AIProviderConnectionEntry connection, string deploymentName)
{
- var endpoint = connection.GetEndpoint();
- return GetClient(connection, endpoint)
+ return GetClient(connection)
.GetImageClient(deploymentName)
.AsIImageGenerator();
}
protected override ISpeechToTextClient GetSpeechToTextClient(AIProviderConnectionEntry connection, string deploymentName)
{
- var endpoint = connection.GetEndpoint();
- return GetClient(connection, endpoint)
+ return GetClient(connection)
.GetAudioClient(deploymentName)
.AsISpeechToTextClient();
}
#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
- private AzureOpenAIClient GetClient(AIProviderConnectionEntry connection, Uri endpoint)
+ private AzureOpenAIClient GetClient(AIProviderConnectionEntry connection)
{
- var options = new AzureOpenAIClientOptions
- {
- ClientLoggingOptions = new ClientLoggingOptions
- {
- LoggerFactory = _loggerFactory,
- EnableLogging = connection.GetBooleanOrFalseValue("EnableLogging"),
- EnableMessageLogging = connection.GetBooleanOrFalseValue("EnableMessageLogging"),
- EnableMessageContentLogging = connection.GetBooleanOrFalseValue("EnableMessageContentLogging"),
- },
- };
-
- var identityId = connection.GetIdentityId();
- var azureClient = connection.GetAzureAuthenticationType() switch
- {
- AzureAuthenticationType.ApiKey => new AzureOpenAIClient(endpoint, new ApiKeyCredential(connection.GetApiKey()), options),
- AzureAuthenticationType.ManagedIdentity => new AzureOpenAIClient(endpoint, new ManagedIdentityCredential(string.IsNullOrEmpty(identityId) ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.FromUserAssignedClientId(identityId)), options),
- AzureAuthenticationType.Default => new AzureOpenAIClient(endpoint, new DefaultAzureCredential(), options),
- _ => throw new NotSupportedException("The provided authentication type is not supported.")
- };
-
- return azureClient;
+ return AzureOpenAIClientFactory.Create(connection, _loggerFactory, _azureClientSettings);
}
}
diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureOpenAICompletionClient.cs b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureOpenAICompletionClient.cs
index f997adce..1e2a5d64 100644
--- a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureOpenAICompletionClient.cs
+++ b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureOpenAICompletionClient.cs
@@ -1,19 +1,19 @@
-using System.ClientModel;
-using System.ClientModel.Primitives;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text.Json;
+using System.Text.Json.Nodes;
using Azure.AI.OpenAI;
-using Azure.Identity;
using CrestApps.Core.AI.Clients;
using CrestApps.Core.AI.Completions;
+using CrestApps.Core.AI.Connections;
using CrestApps.Core.AI.Deployments;
using CrestApps.Core.AI.Models;
+using CrestApps.Core.AI.OpenAI.Azure.Models;
using CrestApps.Core.AI.Services;
-using CrestApps.Core.Azure;
-using CrestApps.Core.Azure.Models;
using CrestApps.Core.Infrastructure;
+using CrestApps.Core.Support;
using CrestApps.Core.Templates.Services;
+using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -27,10 +27,11 @@ public sealed class AzureOpenAICompletionClient : AICompletionServiceBase, IAICo
private readonly ILoggerFactory _loggerFactory;
private readonly IEnumerable _completionServiceHandlers;
private readonly DefaultAIOptions _defaultOptions;
+ private readonly IAIProviderConnectionStore _connectionStore;
+ private readonly IDataProtectionProvider _dataProtectionProvider;
+ private readonly AzureClientOptions _azureClientOptions;
private readonly ILogger _logger;
- private AzureOpenAIClientOptions _clientOptions;
-
public AzureOpenAICompletionClient(
IOptions providerOptions,
IServiceProvider serviceProvider,
@@ -38,17 +39,23 @@ public AzureOpenAICompletionClient(
IEnumerable completionServiceHandlers,
DefaultAIOptions defaultOptions,
ITemplateService aiTemplateService,
- IAIDeploymentManager deploymentManager)
+ IAIDeploymentManager deploymentManager,
+ IAIProviderConnectionStore connectionStore,
+ IDataProtectionProvider dataProtectionProvider,
+ IOptionsSnapshot azureClientOptions)
: base(providerOptions.Value, aiTemplateService, deploymentManager)
{
_serviceProvider = serviceProvider;
_loggerFactory = loggerFactory;
_completionServiceHandlers = completionServiceHandlers;
_defaultOptions = defaultOptions;
+ _connectionStore = connectionStore;
+ _dataProtectionProvider = dataProtectionProvider;
+ _azureClientOptions = azureClientOptions.Value;
_logger = loggerFactory.CreateLogger();
}
- public string Name
+ public string ClientName
{
get
{
@@ -60,23 +67,10 @@ public string Name
{
ArgumentNullException.ThrowIfNull(messages);
ArgumentNullException.ThrowIfNull(context);
- if (!ProviderOptions.Providers.TryGetValue(AzureOpenAIConstants.ClientName, out var provider))
- {
- throw new ArgumentException($"Provider '{AzureOpenAIConstants.ClientName}' not found.");
- }
-
- // Use the deployment resolver with fallback to legacy dictionary-based resolution.
- var deployment = await ResolveDeploymentAsync(AIDeploymentType.Chat, provider, AzureOpenAIConstants.ClientName, deploymentName: context.ChatDeploymentName);
- var connectionName = deployment?.ConnectionName;
- if (string.IsNullOrEmpty(connectionName))
- {
- _logger.LogWarning("Unable to chat. Unable to find a deployment with a valid connection.");
- return null;
- }
- if (!provider.Connections.TryGetValue(connectionName, out var connectionProperties))
+ var (deployment, connectionProperties) = await ResolveChatConfigurationAsync(context.ChatDeploymentName);
+ if (deployment == null || connectionProperties == null)
{
- _logger.LogWarning("Unable to chat. Unable to find a connection '{ConnectionName}'", connectionName);
return null;
}
@@ -107,6 +101,7 @@ public string Name
}
var prompts = GetPrompts(context, azureMessages);
+ var connectionName = deployment.ConnectionName;
var azureClient = GetChatClient(connectionProperties);
var chatClient = azureClient.GetChatClient(deployment.ModelName);
var functions = await ResolveToolsAsync(context, deployment.ModelName);
@@ -173,17 +168,10 @@ public string Name
{
ArgumentNullException.ThrowIfNull(messages);
ArgumentNullException.ThrowIfNull(context);
- if (!ProviderOptions.Providers.TryGetValue(AzureOpenAIConstants.ClientName, out var provider))
- {
- throw new ArgumentException($"Provider '{AzureOpenAIConstants.ClientName}' not found.");
- }
- // Use the deployment resolver with fallback to legacy dictionary-based resolution.
- var deployment = await ResolveDeploymentAsync(AIDeploymentType.Chat, provider, AzureOpenAIConstants.ClientName, deploymentName: context.ChatDeploymentName);
- var connectionName = deployment?.ConnectionName;
- if (string.IsNullOrEmpty(connectionName) || !provider.Connections.TryGetValue(connectionName, out var connection))
+ var (deployment, connection) = await ResolveChatConfigurationAsync(context.ChatDeploymentName);
+ if (deployment == null || connection == null)
{
- _logger.LogWarning("Unable to chat. Unable to find a deployment with a valid connection.");
yield break;
}
@@ -194,7 +182,8 @@ public string Name
}
var azureMessages = new List();
- var currentPrompt = string.Empty;
+ string currentPrompt;
+
foreach (var message in messages)
{
if (string.IsNullOrWhiteSpace(message.Text))
@@ -213,6 +202,7 @@ public string Name
}
}
+ var connectionName = deployment.ConnectionName;
var azureClient = GetChatClient(connection);
var chatClient = azureClient.GetChatClient(deployment.ModelName);
var functions = await ResolveToolsAsync(context, deployment.ModelName);
@@ -229,6 +219,7 @@ public string Name
Microsoft.Extensions.AI.UsageDetails usage = null;
string responseId = null;
string modelId = null;
+
while (iterations <= _defaultOptions.MaximumIterationsPerRequest)
{
var hasToolCalls = false;
@@ -394,32 +385,73 @@ The function arguments were truncated because the response exceeded the output t
}
}
- private AzureOpenAIClient GetChatClient(AIProviderConnectionEntry connection)
+ private async ValueTask<(AIDeployment deployment, AIProviderConnectionEntry connection)> ResolveChatConfigurationAsync(string deploymentName)
{
- _clientOptions ??= new AzureOpenAIClientOptions()
+ var deployment = await ResolveDeploymentAsync(AIDeploymentType.Chat, AzureOpenAIConstants.ClientName, deploymentName: deploymentName);
+
+ if (deployment == null)
+ {
+ _logger.LogWarning("Unable to chat. Unable to find a deployment named '{DeploymentName}' or a default Azure deployment.", deploymentName);
+ return (null, null);
+ }
+
+ var connection = await ResolveConnectionAsync(deployment);
+
+ if (connection == null)
+ {
+ _logger.LogWarning("Unable to chat. Unable to find a valid connection for Azure deployment '{DeploymentName}'.", deployment.Name);
+ return (deployment, null);
+ }
+
+ return (deployment, connection);
+ }
+
+ private async ValueTask ResolveConnectionAsync(AIDeployment deployment)
+ {
+ if (!string.IsNullOrEmpty(deployment.ConnectionName))
{
- ClientLoggingOptions = new ClientLoggingOptions()
+ var connection = await _connectionStore.GetAsync(deployment.ConnectionName, deployment.ClientName);
+ if (connection == null)
{
- EnableLogging = connection.GetBooleanOrFalseValue("EnableLogging"),
- EnableMessageContentLogging = connection.GetBooleanOrFalseValue("EnableMessageContentLogging"),
- EnableMessageLogging = connection.GetBooleanOrFalseValue("EnableMessageLogging"),
- LoggerFactory = _loggerFactory,
- },
- };
- var endpoint = connection.GetEndpoint();
- var azureClient = connection.GetAzureAuthenticationType() switch
+ return null;
+ }
+
+ return CreateConnectionEntry(connection);
+ }
+
+ return AIDeploymentConnectionEntryFactory.Create(deployment, _dataProtectionProvider);
+ }
+
+ private static AIProviderConnectionEntry CreateConnectionEntry(AIProviderConnection connection)
+ {
+ var values = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ if (connection.Properties != null)
{
- AzureAuthenticationType.ApiKey => new AzureOpenAIClient(endpoint, new ApiKeyCredential(connection.GetApiKey()), _clientOptions),
- AzureAuthenticationType.ManagedIdentity => new AzureOpenAIClient(endpoint, new ManagedIdentityCredential(ManagedIdentityId.SystemAssigned), _clientOptions),
- AzureAuthenticationType.Default => new AzureOpenAIClient(endpoint, new DefaultAzureCredential(), _clientOptions),
- _ => throw new NotSupportedException("The specified authentication type is not supported.")
- };
- return azureClient;
+ foreach (var property in connection.Properties)
+ {
+ values[property.Key] = property.Value is JsonNode jsonNode
+ ? jsonNode.GetRawValue()
+ : property.Value;
+ }
+ }
+
+ values["DisplayText"] = string.IsNullOrWhiteSpace(connection.DisplayText)
+ ? connection.Name
+ : connection.DisplayText;
+
+ return new AIProviderConnectionEntry(values);
+ }
+
+ private AzureOpenAIClient GetChatClient(AIProviderConnectionEntry connection)
+ {
+ return AzureOpenAIClientFactory.Create(connection, _loggerFactory, _azureClientOptions);
}
private static async ValueTask> ConfigureOptionsAsync(ChatCompletionOptions chatOptions, AICompletionContext context, List prompts)
{
var optionsContext = new AzureOpenAIChatOptionsContext(chatOptions, context, prompts);
+
if (optionsContext.SystemFunctions.Count > 0)
{
foreach (var function in optionsContext.SystemFunctions)
@@ -446,6 +478,7 @@ private static ChatCompletionOptions GetOptions(AICompletionContext context, IEn
PresencePenalty = context.PresencePenalty,
MaxOutputTokenCount = context.MaxTokens,
};
+
if (!context.DisableTools)
{
foreach (var function in functions)
@@ -475,10 +508,11 @@ private static ChatCompletionOptions GetOptions(AICompletionContext context, IEn
var configureContext = new CompletionServiceConfigureContext(chatOptions, context, isFunctionInvocationSupported: true)
{
DeploymentName = deploymentName,
- ProviderName = Name,
- ImplemenationName = Name,
+ ClientName = ClientName,
+ ImplemenationName = ClientName,
IsStreaming = false,
};
+
foreach (var handler in _completionServiceHandlers)
{
await handler.ConfigureAsync(configureContext);
@@ -516,13 +550,15 @@ private static List GetPrompts(AICompletionContext context, List().ToArray();
- if (observers.Length == 0)
+ var observers = _serviceProvider.GetServices();
+
+ if (!observers.Any())
{
return;
}
- var record = AICompletionUsageRecordFactory.Create(context, AzureOpenAIConstants.ClientName, Name, connectionName, deploymentName, modelName, responseId, usage?.InputTokenCount ?? 0, usage?.OutputTokenCount ?? 0, usage?.TotalTokenCount ?? 0, responseLatencyMs, isStreaming);
+ var record = AICompletionUsageRecordFactory.Create(context, ClientName, connectionName, deploymentName, modelName, responseId, usage?.InputTokenCount ?? 0, usage?.OutputTokenCount ?? 0, usage?.TotalTokenCount ?? 0, responseLatencyMs, isStreaming);
+
await observers.InvokeHandlersAsync((observer, usageRecord) => observer.UsageRecordedAsync(usageRecord, cancellationToken), record, _logger);
}
}
diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureSpeechClientProvider.cs b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureSpeechClientProvider.cs
index 5760b7de..15e1c699 100644
--- a/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureSpeechClientProvider.cs
+++ b/src/Primitives/CrestApps.Core.AI.OpenAI.Azure/Services/AzureSpeechClientProvider.cs
@@ -26,7 +26,7 @@ public AzureSpeechClientProvider(
public bool CanHandle(string providerName)
{
- return string.Equals(AzureOpenAIConstants.AzureSpeechProviderName, providerName, StringComparison.OrdinalIgnoreCase);
+ return string.Equals(AzureOpenAIConstants.AzureSpeechClientName, providerName, StringComparison.OrdinalIgnoreCase);
}
public ValueTask GetChatClientAsync(AIProviderConnectionEntry connection, string deploymentName = null)
diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI/OpenAIConstants.cs b/src/Primitives/CrestApps.Core.AI.OpenAI/OpenAIConstants.cs
index 25379cc0..3d672f9e 100644
--- a/src/Primitives/CrestApps.Core.AI.OpenAI/OpenAIConstants.cs
+++ b/src/Primitives/CrestApps.Core.AI.OpenAI/OpenAIConstants.cs
@@ -2,9 +2,5 @@ namespace CrestApps.Core.AI.OpenAI;
public static class OpenAIConstants
{
- public const string ProviderName = "OpenAI";
-
- public const string ClientName = ProviderName;
-
- public const string ImplementationName = "OpenAI";
+ public const string ClientName = "OpenAI";
}
diff --git a/src/Primitives/CrestApps.Core.AI.OpenAI/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI.OpenAI/ServiceCollectionExtensions.cs
index ed6c2cdb..05c42b46 100644
--- a/src/Primitives/CrestApps.Core.AI.OpenAI/ServiceCollectionExtensions.cs
+++ b/src/Primitives/CrestApps.Core.AI.OpenAI/ServiceCollectionExtensions.cs
@@ -18,13 +18,13 @@ public static IServiceCollection AddCoreAIOpenAI(this IServiceCollection service
services.TryAddEnumerable(ServiceDescriptor.Scoped());
- services.AddCoreAIProfile(OpenAIConstants.ImplementationName, OpenAIConstants.ProviderName, o =>
+ services.AddCoreAIProfile(OpenAIConstants.ClientName, o =>
{
o.DisplayName = new LocalizedString("OpenAI", "OpenAI");
o.Description = new LocalizedString("OpenAI", "Use OpenAI models for AI completion.");
});
- services.AddCoreAIConnectionSource(OpenAIConstants.ProviderName, o =>
+ services.AddCoreAIConnectionSource(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.OpenAI/Services/OpenAICompletionClient.cs b/src/Primitives/CrestApps.Core.AI.OpenAI/Services/OpenAICompletionClient.cs
index 50002469..74c752e2 100644
--- a/src/Primitives/CrestApps.Core.AI.OpenAI/Services/OpenAICompletionClient.cs
+++ b/src/Primitives/CrestApps.Core.AI.OpenAI/Services/OpenAICompletionClient.cs
@@ -12,15 +12,17 @@ namespace CrestApps.Core.AI.OpenAI.Services;
public sealed class OpenAICompletionClient : NamedAICompletionClient
{
- public OpenAICompletionClient(IAIClientFactory aIClientFactory, ILoggerFactory loggerFactory, IDistributedCache distributedCache, IServiceProvider serviceProvider, IOptions providerOptions, IEnumerable handlers, IOptions defaultOptions, ITemplateService aiTemplateService, IAIDeploymentManager deploymentManager) : base(OpenAIConstants.ImplementationName, aIClientFactory, distributedCache, loggerFactory, serviceProvider, providerOptions.Value, defaultOptions.Value, handlers, aiTemplateService, deploymentManager)
+ public OpenAICompletionClient(
+ IAIClientFactory aIClientFactory,
+ ILoggerFactory loggerFactory,
+ IDistributedCache distributedCache,
+ IServiceProvider serviceProvider,
+ IOptions providerOptions,
+ IEnumerable handlers,
+ IOptions defaultOptions,
+ ITemplateService aiTemplateService,
+ IAIDeploymentManager deploymentManager)
+ : base(OpenAIConstants.ClientName, aIClientFactory, distributedCache, loggerFactory, serviceProvider, providerOptions.Value, defaultOptions.Value, handlers, aiTemplateService, deploymentManager)
{
}
-
- protected override string ProviderName
- {
- get
- {
- return OpenAIConstants.ProviderName;
- }
- }
}
diff --git a/src/Primitives/CrestApps.Core.AI/AIOptions.cs b/src/Primitives/CrestApps.Core.AI/AIOptions.cs
index 70e26aa1..23c0b478 100644
--- a/src/Primitives/CrestApps.Core.AI/AIOptions.cs
+++ b/src/Primitives/CrestApps.Core.AI/AIOptions.cs
@@ -57,12 +57,12 @@ internal void AddClient(string name)
_clients[name] = typeof(TClient);
}
- public void AddProfileSource(string name, string providerName, Action configure = null)
+ public void AddProfileSource(string clientName, Action configure = null)
{
- ArgumentException.ThrowIfNullOrEmpty(name);
- if (!_profileSources.TryGetValue(name, out var entry))
+ ArgumentException.ThrowIfNullOrEmpty(clientName);
+ if (!_profileSources.TryGetValue(clientName, out var entry))
{
- entry = new AIProfileProviderEntry(providerName);
+ entry = new AIProfileProviderEntry(clientName);
}
if (configure != null)
@@ -72,16 +72,16 @@ public void AddProfileSource(string name, string providerName, Action configure = null)
+ public void AddDeploymentProvider(string clientName, Action configure = null)
{
- ArgumentException.ThrowIfNullOrEmpty(providerName);
- if (!_deployments.TryGetValue(providerName, out var entry))
+ ArgumentException.ThrowIfNullOrEmpty(clientName);
+ if (!_deployments.TryGetValue(clientName, out var entry))
{
entry = new AIDeploymentProviderEntry();
}
@@ -93,18 +93,18 @@ public void AddDeploymentProvider(string providerName, Action configure = null)
+ public void AddConnectionSource(string clientName, Action configure = null)
{
- ArgumentException.ThrowIfNullOrEmpty(providerName);
- if (!_connectionSources.TryGetValue(providerName, out var entry))
+ ArgumentException.ThrowIfNullOrEmpty(clientName);
+ if (!_connectionSources.TryGetValue(clientName, out var entry))
{
- entry = new AIProviderConnectionOptionsEntry(providerName);
+ entry = new AIProviderConnectionOptionsEntry(clientName);
}
if (configure != null)
@@ -114,10 +114,10 @@ public void AddConnectionSource(string providerName, Action configure = null)
diff --git a/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs
index 4f7d4583..9aa193a5 100644
--- a/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs
+++ b/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs
@@ -190,30 +190,29 @@ public static CrestAppsCoreBuilder AddAISuite(this CrestAppsCoreBuilder builder,
return builder;
}
- public static IServiceCollection AddCoreAIProfile(this IServiceCollection services, string implementationName, string providerName, Action configure = null)
+ public static IServiceCollection AddCoreAIProfile(this IServiceCollection services, string clientName, Action configure = null)
where TClient : class, IAICompletionClient
{
ArgumentNullException.ThrowIfNull(services);
- ArgumentNullException.ThrowIfNull(implementationName);
- ArgumentNullException.ThrowIfNull(providerName);
+ ArgumentNullException.ThrowIfNull(clientName);
return services
.Configure(o =>
{
- o.AddProfileSource(implementationName, providerName, configure);
+ o.AddProfileSource(clientName, configure);
})
- .AddCoreAICompletionClient(implementationName);
+ .AddCoreAICompletionClient(clientName);
}
- public static IServiceCollection AddCoreAIDeploymentProvider(this IServiceCollection services, string providerName, Action configure = null)
+ public static IServiceCollection AddCoreAIDeploymentProvider(this IServiceCollection services, string clientName, Action configure = null)
{
ArgumentNullException.ThrowIfNull(services);
- ArgumentNullException.ThrowIfNull(providerName);
+ ArgumentNullException.ThrowIfNull(clientName);
services
.Configure(o =>
{
- o.AddDeploymentProvider(providerName, configure);
+ o.AddDeploymentProvider(clientName, configure);
});
return services;
@@ -236,14 +235,14 @@ public static IServiceCollection AddCoreAICompletionClient(this IServic
return services;
}
- public static IServiceCollection AddCoreAIConnectionSource(this IServiceCollection services, string providerName, Action configure = null)
+ public static IServiceCollection AddCoreAIConnectionSource(this IServiceCollection services, string clientName, Action configure = null)
{
ArgumentNullException.ThrowIfNull(services);
- ArgumentNullException.ThrowIfNull(providerName);
+ ArgumentNullException.ThrowIfNull(clientName);
services.Configure(o =>
{
- o.AddConnectionSource(providerName, configure);
+ o.AddConnectionSource(clientName, configure);
});
return services;
diff --git a/src/Primitives/CrestApps.Core.AI/Services/AICompletionServiceBase.cs b/src/Primitives/CrestApps.Core.AI/Services/AICompletionServiceBase.cs
index b2707de4..7b212651 100644
--- a/src/Primitives/CrestApps.Core.AI/Services/AICompletionServiceBase.cs
+++ b/src/Primitives/CrestApps.Core.AI/Services/AICompletionServiceBase.cs
@@ -33,7 +33,6 @@ protected AICompletionServiceBase(
///
protected virtual async ValueTask ResolveDeploymentAsync(
AIDeploymentType type,
- AIProvider provider,
string providerName,
string deploymentName = null)
{
diff --git a/src/Primitives/CrestApps.Core.AI/Services/AICompletionUsageRecordFactory.cs b/src/Primitives/CrestApps.Core.AI/Services/AICompletionUsageRecordFactory.cs
index 10d75e04..298e5316 100644
--- a/src/Primitives/CrestApps.Core.AI/Services/AICompletionUsageRecordFactory.cs
+++ b/src/Primitives/CrestApps.Core.AI/Services/AICompletionUsageRecordFactory.cs
@@ -5,16 +5,15 @@ namespace CrestApps.Core.AI.Services;
public static class AICompletionUsageRecordFactory
{
- public static AICompletionUsageRecord Create(AICompletionContext completionContext, string providerName, string clientName, string connectionName, string deploymentName, string modelName, string responseId, long inputTokenCount, long outputTokenCount, long totalTokenCount, double responseLatencyMs, bool isStreaming)
+ public static AICompletionUsageRecord Create(AICompletionContext completionContext, string clientName, string connectionName, string deploymentName, string modelName, string responseId, long inputTokenCount, long outputTokenCount, long totalTokenCount, double responseLatencyMs, bool isStreaming)
{
- return Create(completionContext?.AdditionalProperties, providerName, clientName, connectionName, deploymentName, modelName, responseId, inputTokenCount, outputTokenCount, totalTokenCount, responseLatencyMs, isStreaming);
+ return Create(completionContext?.AdditionalProperties, clientName, connectionName, deploymentName, modelName, responseId, inputTokenCount, outputTokenCount, totalTokenCount, responseLatencyMs, isStreaming);
}
- public static AICompletionUsageRecord Create(IReadOnlyDictionary additionalProperties, string providerName, string clientName, string connectionName, string deploymentName, string modelName, string responseId, long inputTokenCount, long outputTokenCount, long totalTokenCount, double responseLatencyMs, bool isStreaming)
+ public static AICompletionUsageRecord Create(IReadOnlyDictionary additionalProperties, string clientName, string connectionName, string deploymentName, string modelName, string responseId, long inputTokenCount, long outputTokenCount, long totalTokenCount, double responseLatencyMs, bool isStreaming)
{
var record = new AICompletionUsageRecord
{
- ProviderName = providerName,
ClientName = clientName,
ConnectionName = connectionName,
DeploymentName = deploymentName,
diff --git a/src/Primitives/CrestApps.Core.AI/Services/AICompletionUsageTrackingChatClient.cs b/src/Primitives/CrestApps.Core.AI/Services/AICompletionUsageTrackingChatClient.cs
index 2fc50187..49d83ea9 100644
--- a/src/Primitives/CrestApps.Core.AI/Services/AICompletionUsageTrackingChatClient.cs
+++ b/src/Primitives/CrestApps.Core.AI/Services/AICompletionUsageTrackingChatClient.cs
@@ -13,7 +13,6 @@ namespace CrestApps.Core.AI.Services;
internal sealed class AICompletionUsageTrackingChatClient : DelegatingChatClient
{
- private readonly string _providerName;
private readonly string _clientName;
private readonly string _connectionName;
private readonly string _deploymentName;
@@ -22,7 +21,6 @@ internal sealed class AICompletionUsageTrackingChatClient : DelegatingChatClient
public AICompletionUsageTrackingChatClient(
IChatClient innerClient,
- string providerName,
string clientName,
string connectionName,
string deploymentName,
@@ -30,7 +28,6 @@ public AICompletionUsageTrackingChatClient(
ILogger logger)
: base(innerClient)
{
- _providerName = providerName;
_clientName = clientName;
_connectionName = connectionName;
_deploymentName = deploymentName;
@@ -104,7 +101,6 @@ private async Task RecordUsageAsync(
var record = AICompletionUsageRecordFactory.Create(
additionalProperties,
- _providerName,
clientName,
_connectionName,
_deploymentName,
diff --git a/src/Primitives/CrestApps.Core.AI/Services/ConfigurationAIDeploymentSource.cs b/src/Primitives/CrestApps.Core.AI/Services/ConfigurationAIDeploymentSource.cs
index 65cec042..2f47182b 100644
--- a/src/Primitives/CrestApps.Core.AI/Services/ConfigurationAIDeploymentSource.cs
+++ b/src/Primitives/CrestApps.Core.AI/Services/ConfigurationAIDeploymentSource.cs
@@ -77,7 +77,7 @@ private void ReadConfiguredDeployments(Dictionary deployme
foreach (var sectionPath in _catalogOptions.DeploymentSections)
{
var section = _configuration.GetSection(sectionPath);
- var children = section.GetChildren().ToArray();
+ var children = section.GetChildren();
if (_logger.IsEnabled(LogLevel.Debug))
{
@@ -85,7 +85,7 @@ private void ReadConfiguredDeployments(Dictionary deployme
"Inspecting AI deployment section '{SectionPath}'. Exists: {SectionExists}. Child count: {ChildCount}. Child keys: [{ChildKeys}].",
sectionPath,
section.Exists(),
- children.Length,
+ children.Count(),
string.Join(", ", children.Select(static child => child.Key)));
}
@@ -283,8 +283,9 @@ private void AddDeployment(
private static JsonNode ReadConfigurationNode(IConfigurationSection section)
{
- var children = section.GetChildren().ToArray();
- if (children.Length == 0)
+ var children = section.GetChildren();
+
+ if (!children.Any())
{
return section.Value is null ? null : JsonValue.Create(ParseScalar(section.Value));
}
diff --git a/src/Primitives/CrestApps.Core.AI/Services/DefaultAIClientFactory.cs b/src/Primitives/CrestApps.Core.AI/Services/DefaultAIClientFactory.cs
index 0be81d5a..618bdca9 100644
--- a/src/Primitives/CrestApps.Core.AI/Services/DefaultAIClientFactory.cs
+++ b/src/Primitives/CrestApps.Core.AI/Services/DefaultAIClientFactory.cs
@@ -49,7 +49,6 @@ public async ValueTask CreateChatClientAsync(AIDeployment deploymen
return new AICompletionUsageTrackingChatClient(
client,
deployment.ClientName,
- deployment.ClientName,
deployment.ConnectionName,
deployment.ModelName,
_serviceProvider,
diff --git a/src/Primitives/CrestApps.Core.AI/Services/NamedAICompletionClient.cs b/src/Primitives/CrestApps.Core.AI/Services/NamedAICompletionClient.cs
index c47d88f1..257e5c88 100644
--- a/src/Primitives/CrestApps.Core.AI/Services/NamedAICompletionClient.cs
+++ b/src/Primitives/CrestApps.Core.AI/Services/NamedAICompletionClient.cs
@@ -24,8 +24,8 @@ public abstract class NamedAICompletionClient : AICompletionServiceBase, IAIComp
protected readonly ILogger Logger;
protected readonly ILoggerFactory LoggerFactory;
- public NamedAICompletionClient(
- string name,
+ protected NamedAICompletionClient(
+ string clientName,
IAIClientFactory aIClientFactory,
IDistributedCache distributedCache,
ILoggerFactory loggerFactory,
@@ -35,10 +35,11 @@ public NamedAICompletionClient(
IEnumerable handlers,
ITemplateService aiTemplateService,
IAIDeploymentManager deploymentManager)
- : base(providerOptions, aiTemplateService, deploymentManager)
+ : base(providerOptions, aiTemplateService, deploymentManager)
{
- ArgumentException.ThrowIfNullOrWhiteSpace(name);
- Name = name;
+ ArgumentException.ThrowIfNullOrWhiteSpace(clientName);
+
+ ClientName = clientName;
_aIClientFactory = aIClientFactory;
_distributedCache = distributedCache;
LoggerFactory = loggerFactory;
@@ -48,14 +49,7 @@ public NamedAICompletionClient(
_handlers = handlers;
}
- public string Name { get; }
-
- protected abstract string ProviderName { get; }
-
- [Obsolete("This method is obsolete and will be removed in future releases. Please use ConfigureChatOptionsAsync instead")]
- protected virtual void ConfigureChatOptions(ChatOptions options, string modelName)
- {
- }
+ public string ClientName { get; }
protected virtual ValueTask ConfigureChatOptionsAsync(CompletionServiceConfigureContext configureContext)
{
@@ -93,16 +87,10 @@ public async Task CompleteAsync(IEnumerable messages,
ArgumentNullException.ThrowIfNull(messages);
ArgumentNullException.ThrowIfNull(context);
- if (!ProviderOptions.Providers.TryGetValue(ProviderName, out var provider))
- {
- throw new ArgumentException($"Provider '{ProviderName}' not found.");
- }
-
// Use the deployment resolver with fallback to legacy dictionary-based resolution.
var deployment = await ResolveDeploymentAsync(
AIDeploymentType.Chat,
- provider,
- ProviderName,
+ ClientName,
deploymentName: context.ChatDeploymentName);
if (deployment == null)
@@ -135,7 +123,7 @@ public async Task CompleteAsync(IEnumerable messages,
}
catch (Exception ex)
{
- Logger.LogError(ex, "An error occurred while chatting with the {Name} service.", Name);
+ Logger.LogError(ex, "An error occurred while chatting with the {Name} service.", ClientName);
}
return null;
@@ -146,16 +134,10 @@ public async IAsyncEnumerable CompleteStreamingAsync(IEnumer
ArgumentNullException.ThrowIfNull(messages);
ArgumentNullException.ThrowIfNull(context);
- if (!ProviderOptions.Providers.TryGetValue(ProviderName, out var provider))
- {
- throw new ArgumentException($"Provider '{ProviderName}' not found.");
- }
-
// Use the deployment resolver with fallback to legacy dictionary-based resolution.
var deployment = await ResolveDeploymentAsync(
AIDeploymentType.Chat,
- provider,
- ProviderName,
+ ClientName,
deploymentName: context.ChatDeploymentName);
if (deployment == null)
@@ -229,8 +211,8 @@ private async Task GetChatOptionsAsync(AICompletionContext context,
var configureContext = new CompletionServiceConfigureContext(chatOptions, context, supportFunctions)
{
DeploymentName = deploymentName,
- ProviderName = ProviderName,
- ImplemenationName = Name,
+ ClientName = ClientName,
+ ImplemenationName = ClientName,
IsStreaming = isStreaming,
};
@@ -241,13 +223,9 @@ private async Task GetChatOptionsAsync(AICompletionContext context,
chatOptions.Tools = null;
}
-#pragma warning disable CS0618 // Type or member is obsolete
- ConfigureChatOptions(chatOptions, deploymentName);
-#pragma warning restore CS0618 // Type or member is obsolete
-
await ConfigureChatOptionsAsync(configureContext);
- chatOptions.AddUsageTracking(context, clientName: Name);
+ chatOptions.AddUsageTracking(context, clientName: ClientName);
return chatOptions;
}
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 e39dc6d6..31a2d686 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
@@ -290,13 +290,21 @@ private async Task PopulateDropdownsAsync(AIProfileViewModel model)
private async Task GetValidA2AConnectionIdsAsync(IEnumerable selectedIds)
{
var allIds = (await _a2aConnectionCatalog.GetAllAsync()).Select(connection => connection.ItemId).ToHashSet(StringComparer.Ordinal);
- return (selectedIds ?? []).Where(id => !string.IsNullOrWhiteSpace(id) && allIds.Contains(id)).Distinct(StringComparer.Ordinal).ToArray();
+
+ return (selectedIds ?? [])
+ .Where(id => !string.IsNullOrWhiteSpace(id) && allIds.Contains(id))
+ .Distinct(StringComparer.Ordinal)
+ .ToArray();
}
private async Task GetValidMcpConnectionIdsAsync(IEnumerable selectedIds)
{
var allIds = (await _mcpConnectionCatalog.GetAllAsync()).Select(c => c.ItemId).ToHashSet(StringComparer.Ordinal);
- return (selectedIds ?? []).Where(id => !string.IsNullOrWhiteSpace(id) && allIds.Contains(id)).Distinct(StringComparer.Ordinal).ToArray();
+
+ return (selectedIds ?? [])
+ .Where(id => !string.IsNullOrWhiteSpace(id) && allIds.Contains(id))
+ .Distinct(StringComparer.Ordinal)
+ .ToArray();
}
private async Task PopulateAttachedDocumentsAsync(AIProfileViewModel model, string referenceId, string referenceType)
@@ -307,7 +315,11 @@ private async Task PopulateAttachedDocumentsAsync(AIProfileViewModel model, stri
}
var storedDocuments = await _documentStore.GetDocumentsAsync(referenceId, referenceType);
- var documentsById = (model.AttachedDocuments ?? []).Where(d => !string.IsNullOrWhiteSpace(d.DocumentId)).ToDictionary(d => d.DocumentId, StringComparer.OrdinalIgnoreCase);
+
+ var documentsById = (model.AttachedDocuments ?? [])
+ .Where(d => !string.IsNullOrWhiteSpace(d.DocumentId))
+ .ToDictionary(d => d.DocumentId, StringComparer.OrdinalIgnoreCase);
+
foreach (var document in storedDocuments)
{
if (string.IsNullOrWhiteSpace(document.ItemId))
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 bab3194a..6bff9fdd 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
@@ -265,7 +265,11 @@ private async Task GetValidAgentNamesAsync(IEnumerable selecte
{
var allAgents = await _profileManager.GetAsync(AIProfileType.Agent) ?? [];
var validNames = allAgents.Select(a => a.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
- return (selectedNames ?? []).Where(name => !string.IsNullOrWhiteSpace(name) && validNames.Contains(name)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+
+ return (selectedNames ?? [])
+ .Where(name => !string.IsNullOrWhiteSpace(name) && validNames.Contains(name))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
}
private async Task GetValidA2AConnectionIdsAsync(IEnumerable selectedIds)
diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Services/AIProfileDocumentService.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Services/AIProfileDocumentService.cs
index 0b40d756..bdbaa7e4 100644
--- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Services/AIProfileDocumentService.cs
+++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AI/Services/AIProfileDocumentService.cs
@@ -142,7 +142,7 @@ public async Task RemoveDocumentsAsync(AIProfile profile, IReadOnlyCollection 0)
{
- await _documentIndexingService.DeleteChunksAsync(chunks.Select(c => c.ItemId).ToArray(), cancellationToken);
+ await _documentIndexingService.DeleteChunksAsync(chunks.Select(c => c.ItemId), cancellationToken);
}
await _chunkStore.DeleteByDocumentIdAsync(documentId);
diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Controllers/ChatExtractedDataController.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Controllers/ChatExtractedDataController.cs
index 911782a4..14b88c2d 100644
--- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Controllers/ChatExtractedDataController.cs
+++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Controllers/ChatExtractedDataController.cs
@@ -61,8 +61,13 @@ public async Task Export(ChatExtractedDataIndexViewModel model)
var records = await _extractedDataService.GetAsync(model.ProfileId, model.StartDateUtc, model.EndDateUtc);
var rows = BuildRows(records);
- var columns = rows.SelectMany(row => row.Values.Keys).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(name => name, StringComparer.OrdinalIgnoreCase).ToArray();
+
+ var columns = rows.SelectMany(row => row.Values.Keys).Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
var fileName = $"chat-extracted-data-{_timeProvider.GetUtcNow():yyyyMMdd-HHmmss}.csv";
+
return File(Encoding.UTF8.GetBytes(GenerateCsv(rows, columns)), "text/csv", fileName);
}
@@ -78,7 +83,10 @@ private static void ApplyReport(ChatExtractedDataIndexViewModel model, IReadOnly
{
var rows = BuildRows(records);
model.Rows = rows;
- model.Columns = rows.SelectMany(row => row.Values.Keys).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(name => name, StringComparer.OrdinalIgnoreCase).ToArray();
+ model.Columns = rows.SelectMany(row => row.Values.Keys)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
}
private static List BuildRows(IReadOnlyList records)
diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Controllers/UsageAnalyticsController.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Controllers/UsageAnalyticsController.cs
index 7b1f4e6f..1462af11 100644
--- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Controllers/UsageAnalyticsController.cs
+++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Controllers/UsageAnalyticsController.cs
@@ -44,7 +44,13 @@ private static void ApplyReport(UsageAnalyticsIndexViewModel model, IReadOnlyLis
model.TotalSessions = relevantRecords.Select(record => record.SessionId).Where(sessionId => !string.IsNullOrEmpty(sessionId)).Distinct(StringComparer.Ordinal).Count();
model.TotalChatInteractions = relevantRecords.Select(record => record.InteractionId).Where(interactionId => !string.IsNullOrEmpty(interactionId)).Distinct(StringComparer.Ordinal).Count();
model.TotalTokens = relevantRecords.Sum(record => (long)record.TotalTokenCount);
- model.Rows = relevantRecords.GroupBy(record => new { UserLabel = GetUserLabel(record), record.IsAuthenticated, ClientName = record.ClientName ?? record.ProviderName ?? "Unknown", ModelName = record.ModelName ?? record.DeploymentName ?? "Unknown", }).Select(group =>
+ model.Rows = relevantRecords.GroupBy(record => new
+ {
+ UserLabel = GetUserLabel(record),
+ record.IsAuthenticated,
+ ClientName = record.ClientName ?? "Unknown",
+ ModelName = record.ModelName ?? record.DeploymentName ?? "Unknown",
+ }).Select(group =>
{
var latencySamples = group.Where(record => record.ResponseLatencyMs > 0).ToList();
return new AICompletionUsageSummaryViewModel
@@ -61,7 +67,10 @@ private static void ApplyReport(UsageAnalyticsIndexViewModel model, IReadOnlyLis
TotalTokens = group.Sum(record => (long)record.TotalTokenCount),
AverageResponseLatencyMs = latencySamples.Count > 0 ? Math.Round(latencySamples.Average(record => record.ResponseLatencyMs), 0) : 0,
};
- }).OrderByDescending(row => row.TotalTokens).ThenByDescending(row => row.TotalCalls).ThenBy(row => row.UserLabel, StringComparer.OrdinalIgnoreCase).ToList();
+ }).OrderByDescending(row => row.TotalTokens)
+ .ThenByDescending(row => row.TotalCalls)
+ .ThenBy(row => row.UserLabel, StringComparer.OrdinalIgnoreCase)
+ .ToList();
}
private static string GetUserLabel(AICompletionUsageRecord record)
diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Services/MvcAIChatDocumentIndexingQueue.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Services/MvcAIChatDocumentIndexingQueue.cs
index ee1821f5..e765c95d 100644
--- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Services/MvcAIChatDocumentIndexingQueue.cs
+++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/AIChat/Services/MvcAIChatDocumentIndexingQueue.cs
@@ -10,6 +10,7 @@ public ValueTask QueueIndexAsync(AIDocument document, IReadOnlyCollection Chat(string id)
DataSourceIsInScope = ragMetadata?.IsInScope ?? false,
DataSourceFilter = ragMetadata?.Filter,
ClaudeModel = anthropicMetadata?.ClaudeModel,
- ClaudeEffortLevel = anthropicMetadata?.EffortLevel ?? CrestApps.Core.AI.Claude.Models.ClaudeEffortLevel.None,
+ ClaudeEffortLevel = anthropicMetadata?.EffortLevel ?? ClaudeEffortLevel.None,
SelectedA2AConnectionIds = interaction.A2AConnectionIds?.ToArray() ?? [],
SelectedMcpConnectionIds = interaction.McpConnectionIds?.ToArray() ?? [],
SelectedToolNames = interaction.ToolNames?.ToArray() ?? [],
diff --git a/src/Startup/CrestApps.Core.Mvc.Web/Areas/DataSources/Services/MvcAIDataSourceIndexingQueue.cs b/src/Startup/CrestApps.Core.Mvc.Web/Areas/DataSources/Services/MvcAIDataSourceIndexingQueue.cs
index 12523e80..031d1a6f 100644
--- a/src/Startup/CrestApps.Core.Mvc.Web/Areas/DataSources/Services/MvcAIDataSourceIndexingQueue.cs
+++ b/src/Startup/CrestApps.Core.Mvc.Web/Areas/DataSources/Services/MvcAIDataSourceIndexingQueue.cs
@@ -36,7 +36,11 @@ internal IAsyncEnumerable ReadAllAsync(Cancella
private ValueTask QueueDocumentIdsAsync(IReadOnlyCollection documentIds, Func, MvcAIDataSourceIndexingWorkItem> factory, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(documentIds);
- var ids = documentIds.Where(id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+
+ var ids = documentIds.Where(id => !string.IsNullOrWhiteSpace(id))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
if (ids.Length == 0)
{
return ValueTask.CompletedTask;
diff --git a/src/Startup/CrestApps.Core.Mvc.Web/appsettings.json b/src/Startup/CrestApps.Core.Mvc.Web/appsettings.json
index 4a045257..e9abf648 100644
--- a/src/Startup/CrestApps.Core.Mvc.Web/appsettings.json
+++ b/src/Startup/CrestApps.Core.Mvc.Web/appsettings.json
@@ -15,12 +15,17 @@
"Connections": [
// {
// "Name": "some unique name",
- // "ClientName": "AzureOpenAI", // OpenAI, Ollama, AzureOpenAI, AzureAIInference
+ // "ClientName": "Azure", // OpenAI, Azure, Ollama, AzureAIInference
// "Endpoint": "your service endpoint",
// "AuthenticationType": "ApiKey",
// "ApiKey": "The API Key",
// }
],
+ "AzureClient": {
+ // "EnableLogging": false,
+ // "EnableMessageLogging": false,
+ // "EnableMessageContentLogging": false
+ },
"Deployments": [
// {
// "ClientName": "AzureSpeech",
diff --git a/src/Stores/CrestApps.Core.Data.EntityCore/Services/DocumentCatalog.cs b/src/Stores/CrestApps.Core.Data.EntityCore/Services/DocumentCatalog.cs
index 5f7fe9c4..a8317dea 100644
--- a/src/Stores/CrestApps.Core.Data.EntityCore/Services/DocumentCatalog.cs
+++ b/src/Stores/CrestApps.Core.Data.EntityCore/Services/DocumentCatalog.cs
@@ -46,6 +46,7 @@ public async ValueTask> GetAsync(IEnumerable ids)
}
var records = await GetReadQuery().Where(x => itemIds.Contains(x.ItemId)).ToListAsync();
+
return records.Select(CatalogRecordFactory.Materialize).ToArray();
}
diff --git a/src/Stores/CrestApps.Core.Data.EntityCore/Services/EntityCoreSearchIndexProfileStore.cs b/src/Stores/CrestApps.Core.Data.EntityCore/Services/EntityCoreSearchIndexProfileStore.cs
index c05431f7..a7ab2213 100644
--- a/src/Stores/CrestApps.Core.Data.EntityCore/Services/EntityCoreSearchIndexProfileStore.cs
+++ b/src/Stores/CrestApps.Core.Data.EntityCore/Services/EntityCoreSearchIndexProfileStore.cs
@@ -14,6 +14,7 @@ public async Task> GetByTypeAsync(string
{
ArgumentException.ThrowIfNullOrEmpty(type);
var records = await GetReadQuery().Where(x => x.Type == type).ToListAsync();
+
return records.Select(CatalogRecordFactory.Materialize).ToArray();
}
}
diff --git a/src/Stores/CrestApps.Core.Data.YesSql/Indexes/AIChat/AICompletionUsageIndex.cs b/src/Stores/CrestApps.Core.Data.YesSql/Indexes/AIChat/AICompletionUsageIndex.cs
index ddf9d4c2..0df34d70 100644
--- a/src/Stores/CrestApps.Core.Data.YesSql/Indexes/AIChat/AICompletionUsageIndex.cs
+++ b/src/Stores/CrestApps.Core.Data.YesSql/Indexes/AIChat/AICompletionUsageIndex.cs
@@ -24,8 +24,6 @@ public sealed class AICompletionUsageIndex : MapIndex
public bool IsAuthenticated { get; set; }
- public string ProviderName { get; set; }
-
public string ClientName { get; set; }
public string ConnectionName { get; set; }
@@ -70,7 +68,6 @@ public override void Describe(DescribeContext context)
VisitorId = record.VisitorId,
ClientId = record.ClientId,
IsAuthenticated = record.IsAuthenticated,
- ProviderName = record.ProviderName,
ClientName = record.ClientName,
ConnectionName = record.ConnectionName,
DeploymentName = record.DeploymentName,
diff --git a/src/Stores/CrestApps.Core.Data.YesSql/Indexes/AIChat/AICompletionUsageIndexSchemaBuilderExtensions.cs b/src/Stores/CrestApps.Core.Data.YesSql/Indexes/AIChat/AICompletionUsageIndexSchemaBuilderExtensions.cs
index 126e5fb0..065e7389 100644
--- a/src/Stores/CrestApps.Core.Data.YesSql/Indexes/AIChat/AICompletionUsageIndexSchemaBuilderExtensions.cs
+++ b/src/Stores/CrestApps.Core.Data.YesSql/Indexes/AIChat/AICompletionUsageIndexSchemaBuilderExtensions.cs
@@ -19,7 +19,6 @@ await schemaBuilder.CreateMapIndexTableAsync(table => ta
.Column(nameof(AICompletionUsageIndex.VisitorId), column => column.WithLength(255))
.Column(nameof(AICompletionUsageIndex.ClientId), column => column.WithLength(255))
.Column(nameof(AICompletionUsageIndex.IsAuthenticated))
- .Column(nameof(AICompletionUsageIndex.ProviderName), column => column.WithLength(128))
.Column(nameof(AICompletionUsageIndex.ClientName), column => column.WithLength(128))
.Column(nameof(AICompletionUsageIndex.ConnectionName), column => column.WithLength(255))
.Column(nameof(AICompletionUsageIndex.DeploymentName), column => column.WithLength(255))
@@ -35,8 +34,15 @@ await schemaBuilder.CreateMapIndexTableAsync(table => ta
await schemaBuilder.AlterIndexTableAsync(table =>
{
- table.CreateIndex("IDX_AICompletionUsage_DocumentId", "DocumentId", nameof(AICompletionUsageIndex.SessionId), nameof(AICompletionUsageIndex.ProfileId));
- table.CreateIndex("IDX_AICompletionUsage_UserId", "DocumentId", nameof(AICompletionUsageIndex.UserId), nameof(AICompletionUsageIndex.CreatedUtc));
+ table.CreateIndex("IDX_AICompletionUsage_DocumentId", "DocumentId",
+ nameof(AICompletionUsageIndex.SessionId),
+ nameof(AICompletionUsageIndex.ProfileId));
+
+ table.CreateIndex("IDX_AICompletionUsage_UserId",
+ "DocumentId",
+ nameof(AICompletionUsageIndex.UserId),
+ nameof(AICompletionUsageIndex.CreatedUtc));
+
}, collection: options?.AICollectionName);
}
}
diff --git a/tests/CrestApps.Core.Tests/Framework/Mvc/AIProviderConnectionOptionsTests.cs b/tests/CrestApps.Core.Tests/Framework/Mvc/AIProviderConnectionOptionsTests.cs
index 6cc9d1fb..2fb0a81b 100644
--- a/tests/CrestApps.Core.Tests/Framework/Mvc/AIProviderConnectionOptionsTests.cs
+++ b/tests/CrestApps.Core.Tests/Framework/Mvc/AIProviderConnectionOptionsTests.cs
@@ -2,6 +2,7 @@
using CrestApps.Core.AI.Deployments;
using CrestApps.Core.AI.Models;
using CrestApps.Core.AI.OpenAI.Azure;
+using CrestApps.Core.AI.OpenAI.Azure.Models;
using CrestApps.Core.AI.Services;
using CrestApps.Core.Infrastructure;
using CrestApps.Core.Mvc.Web.Areas.AI.Controllers;
@@ -27,7 +28,6 @@ public void AddCrestAppsAI_WhenTopLevelConnectionsConfigured_ShouldMergeThemInto
["CrestApps:AI:Connections:0:Name"] = "config-primary",
["CrestApps:AI:Connections:0:ClientName"] = "OpenAI",
["CrestApps:AI:Connections:0:ApiKey"] = "secret",
- ["CrestApps:AI:Connections:0:EnableLogging"] = "true",
})
.Build();
@@ -43,7 +43,32 @@ public void AddCrestAppsAI_WhenTopLevelConnectionsConfigured_ShouldMergeThemInto
var provider = options.Providers["OpenAI"];
Assert.Contains("config-primary", provider.Connections.Keys);
Assert.Equal("config-primary", provider.Connections["config-primary"].GetStringValue("DisplayText", false));
- Assert.True(provider.Connections["config-primary"].GetBooleanOrFalseValue("EnableLogging"));
+ }
+
+ [Fact]
+ public void AddCoreAIAzureOpenAI_WhenAzureClientSettingsConfigured_ShouldBindAzureClientSettings()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["CrestApps:AI:AzureClient:EnableLogging"] = "true",
+ ["CrestApps:AI:AzureClient:EnableMessageLogging"] = "true",
+ ["CrestApps:AI:AzureClient:EnableMessageContentLogging"] = "false",
+ })
+ .Build();
+
+ var services = new ServiceCollection();
+ services.AddSingleton(configuration);
+ services.AddLogging();
+ services.AddCoreAIServices();
+ services.AddCoreAIAzureOpenAI();
+ using var serviceProvider = services.BuildServiceProvider();
+
+ var options = serviceProvider.GetRequiredService>().Value;
+
+ Assert.True(options.EnableLogging);
+ Assert.True(options.EnableMessageLogging);
+ Assert.False(options.EnableMessageContentLogging);
}
[Fact]
@@ -313,8 +338,8 @@ public void AddAzureOpenAIProvider_ShouldRegisterAzureSpeechAsDeploymentProvider
var options = serviceProvider.GetRequiredService>().Value;
- Assert.True(options.Deployments.ContainsKey(AzureOpenAIConstants.AzureSpeechProviderName));
- Assert.True(options.Deployments[AzureOpenAIConstants.AzureSpeechProviderName].SupportsContainedConnection);
+ Assert.True(options.Deployments.ContainsKey(AzureOpenAIConstants.AzureSpeechClientName));
+ Assert.True(options.Deployments[AzureOpenAIConstants.AzureSpeechClientName].SupportsContainedConnection);
}
[Fact]