diff --git a/GermanApp/Infrastructure/Services/AiServicesHealthCheck.cs b/GermanApp/Infrastructure/Services/AiServicesHealthCheck.cs new file mode 100644 index 0000000..9c58b71 --- /dev/null +++ b/GermanApp/Infrastructure/Services/AiServicesHealthCheck.cs @@ -0,0 +1,149 @@ +using GermanApp.Domain.Interfaces; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; + +namespace GermanApp.Infrastructure.Services; + +/// +/// Health check for AI services (Mistral, Vosk, Coqui TTS). +/// This is part of the Infrastructure layer. +/// +public class AiServicesHealthCheck : IHealthCheck +{ + private readonly IMistralService? _mistralService; + private readonly IVoskService? _voskService; + private readonly ITtsService? _ttsService; + private readonly ILogger _logger; + + /// + /// Creates a new AI services health check. + /// + /// The Mistral text generation service (optional, may be null in some environments) + /// The Vosk speech recognition service (optional, may be null in some environments) + /// The Coqui TTS service (optional, may be null in some environments) + /// Logger for health check operations + public AiServicesHealthCheck( + IMistralService? mistralService, + IVoskService? voskService, + ITtsService? ttsService, + ILogger logger) + { + _mistralService = mistralService; + _voskService = voskService; + _ttsService = ttsService; + _logger = logger; + } + + /// + /// Executes the health check for all AI services. + /// + public async Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + var checks = new Dictionary(); + var exceptions = new Dictionary(); + + // Check Mistral service + if (_mistralService != null) + { + try + { + var isHealthy = await _mistralService.TestConnectionAsync(cancellationToken); + checks["Mistral"] = isHealthy ? HealthStatus.Healthy : HealthStatus.Unhealthy; + if (!isHealthy) + { + _logger.LogWarning("Mistral service health check failed"); + } + } + catch (Exception ex) + { + checks["Mistral"] = HealthStatus.Unhealthy; + exceptions["Mistral"] = ex; + _logger.LogError(ex, "Mistral service health check failed with exception"); + } + } + else + { + checks["Mistral"] = HealthStatus.Degraded; + _logger.LogWarning("Mistral service is not registered"); + } + + // Check Vosk service + if (_voskService != null) + { + try + { + var isHealthy = await _voskService.TestModelAsync(cancellationToken); + checks["Vosk"] = isHealthy ? HealthStatus.Healthy : HealthStatus.Unhealthy; + if (!isHealthy) + { + _logger.LogWarning("Vosk service health check failed"); + } + } + catch (Exception ex) + { + checks["Vosk"] = HealthStatus.Unhealthy; + exceptions["Vosk"] = ex; + _logger.LogError(ex, "Vosk service health check failed with exception"); + } + } + else + { + checks["Vosk"] = HealthStatus.Degraded; + _logger.LogWarning("Vosk service is not registered"); + } + + // Check Coqui TTS service + if (_ttsService != null) + { + try + { + var isHealthy = await _ttsService.TestModelAsync(cancellationToken); + checks["Coqui TTS"] = isHealthy ? HealthStatus.Healthy : HealthStatus.Unhealthy; + if (!isHealthy) + { + _logger.LogWarning("Coqui TTS service health check failed"); + } + } + catch (Exception ex) + { + checks["Coqui TTS"] = HealthStatus.Unhealthy; + exceptions["Coqui TTS"] = ex; + _logger.LogError(ex, "Coqui TTS service health check failed with exception"); + } + } + else + { + checks["Coqui TTS"] = HealthStatus.Degraded; + _logger.LogWarning("Coqui TTS service is not registered"); + } + + // Determine overall status + var allHealthy = checks.Values.All(s => s == HealthStatus.Healthy); + var anyUnhealthy = checks.Values.Any(s => s == HealthStatus.Unhealthy); + var anyDegraded = checks.Values.Any(s => s == HealthStatus.Degraded); + + HealthStatus overallStatus = allHealthy + ? HealthStatus.Healthy + : anyUnhealthy + ? HealthStatus.Unhealthy + : HealthStatus.Degraded; + + // Build data dictionary with details + var data = new Dictionary(); + foreach (var check in checks) + { + data[check.Key] = new + { + Status = check.Value.ToString(), + Exception = exceptions.TryGetValue(check.Key, out var ex) ? ex.Message : null + }; + } + + return new HealthCheckResult( + overallStatus, + "AI Services health check", + data: data); + } +} diff --git a/GermanApp/Program.cs b/GermanApp/Program.cs index 77928c1..886c48d 100644 --- a/GermanApp/Program.cs +++ b/GermanApp/Program.cs @@ -49,7 +49,8 @@ try // Add Health Checks builder.Services.AddHealthChecks() - .AddDbContextCheck(); + .AddDbContextCheck() + .AddCheck("ai_services"); // Add Password Hasher for custom User entity builder.Services.AddScoped, PasswordHasher>(); @@ -168,6 +169,9 @@ try builder.Services.Configure(builder.Configuration.GetSection("Vosk")); builder.Services.Configure(builder.Configuration.GetSection("Coqui")); + // Validate AI service configurations + ValidateAiConfigurations(builder.Configuration); + // Register AI services (Infrastructure implementations of Domain interfaces) builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -269,6 +273,24 @@ try app.Run(); +// Helper method to validate AI service configurations +static void ValidateAiConfigurations(IConfiguration configuration) +{ + // Validate Mistral configuration + var mistralConfig = configuration.GetSection("Mistral").Get() ?? new MistralConfig(); + mistralConfig.Validate(); + + // Validate Vosk configuration + var voskConfig = configuration.GetSection("Vosk").Get() ?? new VoskConfig(); + voskConfig.Validate(); + + // Validate Coqui configuration + var coquiConfig = configuration.GetSection("Coqui").Get() ?? new CoquiConfig(); + coquiConfig.Validate(); + + Log.Information("All AI service configurations validated successfully"); +} + // Helper method to apply migrations with retry logic for Docker static async Task ApplyMigrationsWithRetry(WebApplication app, int maxRetries, int delaySeconds) { diff --git a/docs/features/ai-services.md b/docs/features/ai-services.md index 7761a2a..02914da 100644 --- a/docs/features/ai-services.md +++ b/docs/features/ai-services.md @@ -200,12 +200,12 @@ For production deployment via Woodpecker: - Resilient to API failures (retry, rate limiting, circuit breaker, caching) - Testable with mocked HTTP client -### Phase 1: Configuration ### Phase 1: Configuration & Interfaces (2 hours) Interfaces (2 hours) ✅ +### Phase 1: Configuration ### Phase 1: Configuration ### Phase 1: Configuration & Interfaces (2 hours) Interfaces (2 hours) ✅ Interfaces (2 hours) ✅ - [x] Add AI configuration section to appsettings.json -- [ ] Create configuration classes (MistralConfig, VoskConfig, CoquiConfig) -- [ ] Define service interfaces (IMistralService, IVoskService, ITtsService) -- [ ] Register services in Program.cs -- [ ] Set up configuration validation +- [x] Create configuration classes (MistralConfig, VoskConfig, CoquiConfig) +- [x] Define service interfaces (IMistralService, IVoskService, ITtsService) +- [x] Register services in Program.cs +- [x] Set up configuration validation (ValidateAiConfigurations method added to Program.cs) ### Phase 2: Mistral-Medium Integration (2-3 hours) ✅ - [ ] Create MistralService implementation @@ -275,14 +275,14 @@ For production deployment via Woodpecker: ### Backend - Configuration - [x] Create Configuration/MistralConfig.cs -- [ ] Add Mistral settings to appsettings.json (MistralConfig already registered) -- [ ] Add Vosk settings to appsettings.json (VoskConfig registered, needs model path) -- [ ] Add Coqui settings to appsettings.json (CoquiConfig registered, needs model name) +- [x] Add Mistral settings to appsettings.json +- [x] Add Vosk settings to appsettings.json +- [x] Add Coqui settings to appsettings.json - [x] Create Configuration/VoskConfig.cs - [x] Create Configuration/CoquiConfig.cs - [x] Register Mistral Connector in Program.cs - [x] Register all AI services in Program.cs -- [ ] Add health checks for AI services +- [x] Add health checks for AI services (AiServicesHealthCheck.cs created) ### Backend - Mistral Service - [x] Create `Domain/Interfaces/IMistralService.cs`