Skip to content

Commit b27aec6

Browse files
authored
Use IAIProfileStore and Make AIProviderConnectionsOptionsConfiguration More Forgiving (#410)
1 parent 8be39b8 commit b27aec6

30 files changed

+156
-219
lines changed

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
44
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
5-
<OrchardCoreVersion>[3.0.0-preview-18959, )</OrchardCoreVersion>
5+
<OrchardCoreVersion>[3.0.0-preview-18960, )</OrchardCoreVersion>
66
<ModelContextProtocolVersion>1.1.0</ModelContextProtocolVersion>
77
</PropertyGroup>
88
<ItemGroup>

src/Core/CrestApps.OrchardCore.AI.Core/AIProfileStoreExtensions.cs

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/Core/CrestApps.OrchardCore.AI.Core/CatalogExtensions.cs

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/Core/CrestApps.OrchardCore.AI.Core/Handlers/AIProfileHandler.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using CrestApps.OrchardCore.AI.Models;
66
using CrestApps.OrchardCore.Core.Handlers;
77
using CrestApps.OrchardCore.Models;
8-
using CrestApps.OrchardCore.Services;
98
using Microsoft.AspNetCore.Http;
109
using Microsoft.Extensions.Localization;
1110
using OrchardCore.Liquid;
@@ -16,21 +15,21 @@ namespace CrestApps.OrchardCore.AI.Core.Handlers;
1615
public sealed class AIProfileHandler : CatalogEntryHandlerBase<AIProfile>
1716
{
1817
private readonly IHttpContextAccessor _httpContextAccessor;
19-
private readonly INamedCatalog<AIProfile> _profilesCatalog;
18+
private readonly IAIProfileStore _profileStore;
2019
private readonly ILiquidTemplateManager _liquidTemplateManager;
2120
private readonly IClock _clock;
2221

2322
internal readonly IStringLocalizer S;
2423

2524
public AIProfileHandler(
2625
IHttpContextAccessor httpContextAccessor,
27-
INamedCatalog<AIProfile> profilesCatalog,
26+
IAIProfileStore profileStore,
2827
ILiquidTemplateManager liquidTemplateManager,
2928
IClock clock,
3029
IStringLocalizer<AIProfileHandler> stringLocalizer)
3130
{
3231
_httpContextAccessor = httpContextAccessor;
33-
_profilesCatalog = profilesCatalog;
32+
_profileStore = profileStore;
3433
_liquidTemplateManager = liquidTemplateManager;
3534
_clock = clock;
3635
S = stringLocalizer;
@@ -50,7 +49,7 @@ public override async Task ValidatingAsync(ValidatingContext<AIProfile> context)
5049
}
5150
else
5251
{
53-
var profile = await _profilesCatalog.FindByNameAsync(context.Model.Name);
52+
var profile = await _profileStore.FindByNameAsync(context.Model.Name);
5453

5554
if (profile is not null && profile.ItemId != context.Model.ItemId)
5655
{

src/Core/CrestApps.OrchardCore.AI.Core/Services/AIProviderConnectionsOptionsConfiguration.cs

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,20 @@ public AIProviderConnectionsOptionsConfiguration(
2626

2727
public void Configure(AIProviderOptions options)
2828
{
29-
var document = _documentManager.GetOrCreateMutableAsync()
30-
.GetAwaiter()
31-
.GetResult();
29+
DictionaryDocument<AIProviderConnection> document;
30+
31+
try
32+
{
33+
document = _documentManager.GetOrCreateMutableAsync()
34+
.GetAwaiter()
35+
.GetResult();
36+
}
37+
catch (Exception ex)
38+
{
39+
_logger.LogWarning(ex, "Unable to load AI provider connections from the database. This may occur if the module migrations have not yet been applied or the data is corrupted.");
40+
41+
return;
42+
}
3243

3344
if (document.Records.Count == 0)
3445
{
@@ -45,65 +56,72 @@ public void Configure(AIProviderOptions options)
4556

4657
foreach (var group in groups)
4758
{
48-
if (!options.Providers.TryGetValue(group.ProviderName, out var provider))
59+
try
4960
{
50-
provider = new AIProvider()
61+
if (!options.Providers.TryGetValue(group.ProviderName, out var provider))
5162
{
52-
Connections = new Dictionary<string, AIProviderConnectionEntry>(),
53-
};
54-
}
55-
56-
AIProviderConnection defaultConnection = null;
63+
provider = new AIProvider()
64+
{
65+
Connections = new Dictionary<string, AIProviderConnectionEntry>(),
66+
};
67+
}
5768

58-
foreach (var connection in group.Connections)
59-
{
60-
var mappingContext = new InitializingAIProviderConnectionContext(connection);
69+
AIProviderConnection defaultConnection = null;
6170

62-
if (defaultConnection is null && connection.IsDefault)
71+
foreach (var connection in group.Connections)
6372
{
64-
defaultConnection = connection;
65-
}
73+
var mappingContext = new InitializingAIProviderConnectionContext(connection);
6674

67-
if (string.IsNullOrEmpty(connection.ItemId))
68-
{
69-
continue;
70-
}
75+
if (defaultConnection is null && connection.IsDefault)
76+
{
77+
defaultConnection = connection;
78+
}
79+
80+
if (string.IsNullOrEmpty(connection.ItemId))
81+
{
82+
continue;
83+
}
7184

7285
#pragma warning disable CS0618 // Obsolete deployment name fields retained for backward compatibility
73-
mappingContext.Values["ChatDeploymentName"] = connection.ChatDeploymentName;
74-
mappingContext.Values["EmbeddingDeploymentName"] = connection.EmbeddingDeploymentName;
75-
mappingContext.Values["UtilityDeploymentName"] = connection.UtilityDeploymentName;
76-
mappingContext.Values["ImagesDeploymentName"] = connection.ImagesDeploymentName;
77-
mappingContext.Values["SpeechToTextDeploymentName"] = connection.SpeechToTextDeploymentName;
86+
mappingContext.Values["ChatDeploymentName"] = connection.ChatDeploymentName;
87+
mappingContext.Values["EmbeddingDeploymentName"] = connection.EmbeddingDeploymentName;
88+
mappingContext.Values["UtilityDeploymentName"] = connection.UtilityDeploymentName;
89+
mappingContext.Values["ImagesDeploymentName"] = connection.ImagesDeploymentName;
90+
mappingContext.Values["SpeechToTextDeploymentName"] = connection.SpeechToTextDeploymentName;
7891
#pragma warning restore CS0618
79-
mappingContext.Values["ConnectionNameAlias"] = connection.Name;
92+
mappingContext.Values["ConnectionNameAlias"] = connection.Name;
8093

81-
_handlers.Invoke((handler, ctx) => handler.Initializing(ctx), mappingContext, _logger);
94+
_handlers.Invoke((handler, ctx) => handler.Initializing(ctx), mappingContext, _logger);
8295

83-
provider.Connections[connection.ItemId] = new AIProviderConnectionEntry(mappingContext.Values);
84-
}
96+
provider.Connections[connection.ItemId] = new AIProviderConnectionEntry(mappingContext.Values);
97+
}
8598

8699
#pragma warning disable CS0618 // Obsolete deployment name fields retained for backward compatibility
87-
if (defaultConnection is not null)
88-
{
89-
provider.DefaultConnectionName = defaultConnection.ItemId;
90-
provider.DefaultChatDeploymentName = defaultConnection.ChatDeploymentName;
91-
}
92-
else
93-
{
94-
if (string.IsNullOrEmpty(provider.DefaultChatDeploymentName))
100+
if (defaultConnection is not null)
95101
{
96-
provider.DefaultChatDeploymentName = provider.Connections.FirstOrDefault().Value.GetChatDeploymentOrDefaultName();
102+
provider.DefaultConnectionName = defaultConnection.ItemId;
103+
provider.DefaultChatDeploymentName = defaultConnection.ChatDeploymentName;
97104
}
105+
else
106+
{
107+
if (string.IsNullOrEmpty(provider.DefaultChatDeploymentName) && provider.Connections.Count > 0)
108+
{
109+
provider.DefaultChatDeploymentName = provider.Connections.First().Value?.GetChatDeploymentOrDefaultName(false);
110+
}
98111
#pragma warning restore CS0618
99112

100-
if (string.IsNullOrEmpty(provider.DefaultConnectionName))
101-
{
102-
provider.DefaultConnectionName = provider.Connections.FirstOrDefault().Key;
113+
if (string.IsNullOrEmpty(provider.DefaultConnectionName) && provider.Connections.Count > 0)
114+
{
115+
provider.DefaultConnectionName = provider.Connections.First().Key;
116+
}
103117
}
104-
}
105118

106-
options.Providers[group.ProviderName] = provider;
119+
options.Providers[group.ProviderName] = provider;
120+
}
121+
catch (Exception ex)
122+
{
123+
_logger.LogWarning(ex, "Failed to configure AI provider '{ProviderName}' from stored connections. The provider will be skipped. This may occur if the stored connection data is invalid or uses an outdated format. Please review the provider connection settings.", group.ProviderName);
124+
}
107125
}
108126
}
109127
}

src/CrestApps.OrchardCore.Documentations/docs/changelog/v2.0.0.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,9 @@ This branch also includes several important platform improvements that are worth
257257
- improved OpenXML Excel data extraction
258258
- unified `DataSourceMetadata` modeling
259259
- expanded recipe schema support for AI-assisted recipe authoring
260+
- **cleaned up `AIPermissionsProvider`** — replaced `INamedCatalog<AIProfile>` with `IAIProfileStore` and removed unnecessary wrapper extension methods (`GetProfilesAsync`, `GetAsync`) in favor of calling `IAIProfileStore.GetByTypeAsync` directly
261+
- **fixed `AIProviderConnectionsOptionsConfiguration` crash** that could make the entire site unusable when stored provider connection data was invalid, corrupted, or in an outdated v1.x format — the configuration now handles errors per provider and logs warnings instead of throwing unhandled exceptions
262+
- **replaced all `INamedCatalog<AIProfile>` usages with `IAIProfileStore`** across the codebase for improved type safety and clarity — the `INamedCatalog<AIProfile>` DI registration is retained for backward compatibility
260263

261264
Together, these improvements make the whole ecosystem feel more complete, more polished, and more ready for ambitious Orchard Core solutions.
262265

src/Modules/CrestApps.OrchardCore.AI.Chat/Drivers/AIChatAnalyticsProfileFilterDisplayDriver.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
using CrestApps.OrchardCore.AI.Chat.Models;
22
using CrestApps.OrchardCore.AI.Chat.ViewModels;
3-
using CrestApps.OrchardCore.AI.Core;
43
using CrestApps.OrchardCore.AI.Core.Indexes;
54
using CrestApps.OrchardCore.AI.Models;
6-
using CrestApps.OrchardCore.Services;
75
using Microsoft.AspNetCore.Mvc.Rendering;
86
using OrchardCore.DisplayManagement.Handlers;
97
using OrchardCore.DisplayManagement.Views;
@@ -15,20 +13,20 @@ namespace CrestApps.OrchardCore.AI.Chat.Drivers;
1513
/// </summary>
1614
public sealed class AIChatAnalyticsProfileFilterDisplayDriver : DisplayDriver<AIChatAnalyticsFilter>
1715
{
18-
private readonly INamedCatalog<AIProfile> _profilesCatalog;
16+
private readonly IAIProfileStore _profileStore;
1917

2018
public AIChatAnalyticsProfileFilterDisplayDriver(
21-
INamedCatalog<AIProfile> profilesCatalog)
19+
IAIProfileStore profileStore)
2220
{
23-
_profilesCatalog = profilesCatalog;
21+
_profileStore = profileStore;
2422
}
2523

2624
public override IDisplayResult Edit(AIChatAnalyticsFilter filter, BuildEditorContext context)
2725
{
2826
return Initialize<ChatAnalyticsProfileFilterViewModel>("ChatAnalyticsProfileFilter_Edit", async model =>
2927
{
3028
model.ProfileId = filter.ProfileId;
31-
model.Profiles = (await _profilesCatalog.GetAsync(AIProfileType.Chat))
29+
model.Profiles = (await _profileStore.GetByTypeAsync(AIProfileType.Chat))
3230
.Select(p => new SelectListItem(p.DisplayText, p.ItemId));
3331
}).Location("Content:2");
3432
}

src/Modules/CrestApps.OrchardCore.AI.Chat/Drivers/AIProfilePartDisplayDriver.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
using CrestApps.OrchardCore.AI.Chat.ViewModels;
2-
using CrestApps.OrchardCore.AI.Core;
32
using CrestApps.OrchardCore.AI.Core.Models;
43
using CrestApps.OrchardCore.AI.Models;
5-
using CrestApps.OrchardCore.Services;
64
using Microsoft.AspNetCore.Http;
75
using Microsoft.AspNetCore.Mvc.Rendering;
86
using Microsoft.Extensions.Localization;
@@ -18,21 +16,21 @@ namespace CrestApps.OrchardCore.AI.Chat.Drivers;
1816
public sealed class AIChatProfilePartDisplayDriver : ContentPartDisplayDriver<AIProfilePart>
1917
{
2018
private readonly IHttpContextAccessor _httpContextAccessor;
21-
private readonly INamedCatalog<AIProfile> _profilesCatalog;
19+
private readonly IAIProfileStore _profileStore;
2220
private readonly IAIChatSessionManager _chatSessionManager;
2321
private readonly PagerOptions _pagerOptions;
2422

2523
internal readonly IStringLocalizer S;
2624

2725
public AIChatProfilePartDisplayDriver(
2826
IHttpContextAccessor httpContextAccessor,
29-
INamedCatalog<AIProfile> profilesCatalog,
27+
IAIProfileStore profileStore,
3028
IAIChatSessionManager chatSessionManager,
3129
IOptions<PagerOptions> pagerOptions,
3230
IStringLocalizer<AIChatProfilePartDisplayDriver> stringLocalizer)
3331
{
3432
_httpContextAccessor = httpContextAccessor;
35-
_profilesCatalog = profilesCatalog;
33+
_profileStore = profileStore;
3634
_chatSessionManager = chatSessionManager;
3735
_pagerOptions = pagerOptions.Value;
3836
S = stringLocalizer;
@@ -53,7 +51,7 @@ public override async Task<IDisplayResult> DisplayAsync(AIProfilePart part, Buil
5351
return null;
5452
}
5553

56-
var profile = await _profilesCatalog.FindByIdAsync(part.ProfileId);
54+
var profile = await _profileStore.FindByIdAsync(part.ProfileId);
5755

5856
if (profile == null)
5957
{
@@ -86,7 +84,7 @@ public override IDisplayResult Edit(AIProfilePart part, BuildPartEditorContext c
8684

8785
model.MaxHistoryAllowed = _pagerOptions.MaxPageSize;
8886

89-
var profiles = await _profilesCatalog.GetProfilesAsync(AIProfileType.Chat);
87+
var profiles = await _profileStore.GetByTypeAsync(AIProfileType.Chat);
9088

9189
model.Profiles = profiles.Select(profile => new SelectListItem(profile.DisplayText, profile.ItemId));
9290

@@ -103,7 +101,7 @@ public override async Task<IDisplayResult> UpdateAsync(AIProfilePart part, Updat
103101
{
104102
context.Updater.ModelState.AddModelError(Prefix, nameof(model.ProfileId), S["The Profile is required."]);
105103
}
106-
else if (await _profilesCatalog.FindByIdAsync(model.ProfileId) == null)
104+
else if (await _profileStore.FindByIdAsync(model.ProfileId) == null)
107105
{
108106
context.Updater.ModelState.AddModelError(Prefix, nameof(model.ProfileId), S["The Profile is invalid."]);
109107
}

src/Modules/CrestApps.OrchardCore.AI.Chat/Services/ChatAdminMenu.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using CrestApps.OrchardCore.AI.Core;
22
using CrestApps.OrchardCore.AI.Models;
3-
using CrestApps.OrchardCore.Services;
43
using Microsoft.AspNetCore.Routing;
54
using Microsoft.Extensions.Localization;
65
using Microsoft.Extensions.Options;
@@ -11,24 +10,24 @@ namespace CrestApps.OrchardCore.AI.Chat.Services;
1110

1211
public sealed class ChatAdminMenu : AdminNavigationProvider
1312
{
14-
private readonly INamedCatalog<AIProfile> _profilesCatalog;
13+
private readonly IAIProfileStore _profileStore;
1514
private readonly AIOptions _aiOptions;
1615

1716
internal readonly IStringLocalizer S;
1817

1918
public ChatAdminMenu(
20-
INamedCatalog<AIProfile> profilesCatalog,
19+
IAIProfileStore profileStore,
2120
IOptions<AIOptions> aiOptions,
2221
IStringLocalizer<ChatAdminMenu> stringLocalizer)
2322
{
24-
_profilesCatalog = profilesCatalog;
23+
_profileStore = profileStore;
2524
_aiOptions = aiOptions.Value;
2625
S = stringLocalizer;
2726
}
2827

2928
protected override async ValueTask BuildAsync(NavigationBuilder builder)
3029
{
31-
var profiles = await _profilesCatalog.GetAsync(AIProfileType.Chat);
30+
var profiles = await _profileStore.GetByTypeAsync(AIProfileType.Chat);
3231

3332
builder
3433
.Add(S["Artificial Intelligence"], artificialIntelligence =>

src/Modules/CrestApps.OrchardCore.AI.Chat/Views/AIChatSessionChat.cshtml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
@using CrestApps.OrchardCore.AI.Chat.Hubs
55
@using CrestApps.OrchardCore.AI.Models
66
@using CrestApps.OrchardCore.AI.Chat.ViewModels
7-
@using CrestApps.OrchardCore.Services
87
@using CrestApps.OrchardCore.SignalR.Core.Services
98
@using OrchardCore.DisplayManagement
109
@using OrchardCore.DisplayManagement.ModelBinding
@@ -20,7 +19,7 @@
2019

2120
@inject IUpdateModelAccessor UpdateModelAccessor
2221
@inject IDisplayManager<AIChatSessionPrompt> DisplayManager
23-
@inject INamedCatalog<AIProfile> ProfilesCatalog
22+
@inject IAIProfileStore ProfilesCatalog
2423
@inject HubRouteManager HubRouteManager
2524
@inject IAIChatSessionPromptStore PromptStore
2625
@{
@@ -32,7 +31,7 @@
3231
var chatContainerHtmlId = $"{baseId}_ChatContainer";
3332
var documentBarHtmlId = $"{baseId}_DocumentBar";
3433

35-
var promptGeneratedProfiles = await ProfilesCatalog.GetAsync(AIProfileType.TemplatePrompt);
34+
var promptGeneratedProfiles = await ProfilesCatalog.GetByTypeAsync(AIProfileType.TemplatePrompt);
3635

3736
var shellFeaturesManager = ShellScope.Services.GetRequiredService<IShellFeaturesManager>();
3837
var enabledFeatures = await shellFeaturesManager.GetEnabledFeaturesAsync();

0 commit comments

Comments
 (0)