From ece9938bfb83a6e206d3456228e642c123a7932d Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Thu, 25 Jun 2026 13:18:04 -0700 Subject: [PATCH] feat(ai): add DB-backed church AI config provider --- API/ROLAC.API/Services/Ai/ChurchAiConfig.cs | 13 +++++++++ .../Services/Ai/ChurchAiConfigProvider.cs | 29 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 API/ROLAC.API/Services/Ai/ChurchAiConfig.cs create mode 100644 API/ROLAC.API/Services/Ai/ChurchAiConfigProvider.cs diff --git a/API/ROLAC.API/Services/Ai/ChurchAiConfig.cs b/API/ROLAC.API/Services/Ai/ChurchAiConfig.cs new file mode 100644 index 0000000..cfc0e66 --- /dev/null +++ b/API/ROLAC.API/Services/Ai/ChurchAiConfig.cs @@ -0,0 +1,13 @@ +namespace ROLAC.API.Services.Ai; + +/// Active AI configuration resolved from the ChurchProfile singleton (blanks filled with defaults). +public sealed record ChurchAiConfig( + string Provider, + string ClaudeModel, string? ClaudeApiKey, + string GeminiModel, string? GeminiApiKey); + +/// Reads the church's AI settings from the database for the current request. +public interface IChurchAiConfigProvider +{ + Task GetAsync(CancellationToken ct = default); +} diff --git a/API/ROLAC.API/Services/Ai/ChurchAiConfigProvider.cs b/API/ROLAC.API/Services/Ai/ChurchAiConfigProvider.cs new file mode 100644 index 0000000..d37b153 --- /dev/null +++ b/API/ROLAC.API/Services/Ai/ChurchAiConfigProvider.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore; +using ROLAC.API.Data; + +namespace ROLAC.API.Services.Ai; + +/// +/// Loads AI settings from the singleton ChurchProfile row, substituting default model names +/// for any blank field so a freshly migrated install still names a valid model. The API keys are +/// passed through as-is (null when unset → the calling service treats AI as disabled). +/// +public sealed class ChurchAiConfigProvider : IChurchAiConfigProvider +{ + private const string DefaultClaudeModel = "claude-haiku-4-5-20251001"; + private const string DefaultGeminiModel = "gemini-2.5-flash-lite"; + + private readonly AppDbContext _db; + public ChurchAiConfigProvider(AppDbContext db) => _db = db; + + public async Task GetAsync(CancellationToken ct = default) + { + var p = await _db.ChurchProfiles.AsNoTracking().OrderBy(x => x.Id).FirstOrDefaultAsync(ct); + + var provider = string.IsNullOrWhiteSpace(p?.AiProvider) ? "Claude" : p.AiProvider; + var claudeModel = string.IsNullOrWhiteSpace(p?.ClaudeModel) ? DefaultClaudeModel : p!.ClaudeModel!; + var geminiModel = string.IsNullOrWhiteSpace(p?.GeminiModel) ? DefaultGeminiModel : p!.GeminiModel!; + + return new ChurchAiConfig(provider, claudeModel, p?.ClaudeApiKey, geminiModel, p?.GeminiApiKey); + } +}