using GermanApp.Domain.Interfaces; using Microsoft.Extensions.Logging; namespace GermanApp.Application.Services; /// /// Service for handling AI service failures and providing fallback responses. /// This is part of the Application layer. /// public class AiFallbackService { private readonly IMistralService? _mistralService; private readonly IVoskService? _voskService; private readonly ITtsService? _ttsService; private readonly ILogger _logger; /// /// Creates a new AiFallbackService. /// /// The Mistral text generation service (optional) /// The Vosk speech recognition service (optional) /// The Coqui TTS service (optional) /// Logger for service operations public AiFallbackService( IMistralService? mistralService, IVoskService? voskService, ITtsService? ttsService, ILogger logger) { _mistralService = mistralService; _voskService = voskService; _ttsService = ttsService; _logger = logger; } /// /// Attempts to generate a story, with fallback if the primary service fails. /// /// The CEFR level /// The story topic /// List of vocabulary words /// Approximate word count /// Cancellation token /// Generated story text, or fallback if AI service fails public virtual async Task GenerateStoryWithFallbackAsync( string level, string topic, IReadOnlyList vocabularyWords, int length = 200, CancellationToken cancellationToken = default) { try { if (_mistralService != null) { return await _mistralService.GenerateStoryAsync( level, topic, vocabularyWords, length, cancellationToken); } } catch (Exception ex) { _logger.LogWarning(ex, "Mistral story generation failed, using fallback"); } // Fallback: Generate a simple story based on parameters return GenerateFallbackStory(level, topic, vocabularyWords, length); } /// /// Attempts to provide writing feedback, with fallback if the primary service fails. /// /// The user's text /// The CEFR level /// Optional custom prompt /// Cancellation token /// Feedback text, or fallback if AI service fails public virtual async Task ProvideFeedbackWithFallbackAsync( string userText, string level, string? customPrompt = null, CancellationToken cancellationToken = default) { try { if (_mistralService != null) { return await _mistralService.GenerateWritingFeedbackAsync( userText, level, customPrompt, cancellationToken); } } catch (Exception ex) { _logger.LogWarning(ex, "Mistral feedback generation failed, using fallback"); } // Fallback: Provide simple feedback based on text length return GenerateFallbackFeedback(userText, level); } /// /// Attempts to recognize speech, with fallback if the primary service fails. /// /// The audio data /// Cancellation token /// Recognized text, or empty string if service fails public virtual async Task RecognizeSpeechWithFallbackAsync( byte[] audioBytes, CancellationToken cancellationToken = default) { try { if (_voskService != null) { return await _voskService.RecognizeSpeechAsync( audioBytes, 16000, null, cancellationToken); } } catch (Exception ex) { _logger.LogWarning(ex, "Vosk speech recognition failed, using fallback"); } // Fallback: Return empty string or transcription placeholder return "[Speech transcription unavailable - please try again]"; } /// /// Attempts to generate audio, with fallback if the primary service fails. /// /// The text to convert to speech /// The language code /// Cancellation token /// Audio data, or empty array if service fails public virtual async Task GenerateAudioWithFallbackAsync( string text, string language = "de", CancellationToken cancellationToken = default) { try { if (_ttsService != null) { return await _ttsService.GenerateAudioAsync( text, null, language, cancellationToken); } } catch (Exception ex) { _logger.LogWarning(ex, "TTS audio generation failed, using fallback"); } // Fallback: Return empty audio array return new byte[0]; } /// /// Checks if AI services are healthy. /// /// Cancellation token /// Dictionary mapping service names to health status public virtual async Task> CheckServiceHealthAsync( CancellationToken cancellationToken = default) { var health = new Dictionary(); // Check Mistral service if (_mistralService != null) { try { var isHealthy = await _mistralService.TestConnectionAsync(cancellationToken); health["Mistral"] = isHealthy; } catch { health["Mistral"] = false; } } else { health["Mistral"] = false; } // Check Vosk service if (_voskService != null) { try { var isHealthy = await _voskService.TestModelAsync(cancellationToken); health["Vosk"] = isHealthy; } catch { health["Vosk"] = false; } } else { health["Vosk"] = false; } // Check Coqui TTS service if (_ttsService != null) { try { var isHealthy = await _ttsService.TestModelAsync(cancellationToken); health["CoquiTTS"] = isHealthy; } catch { health["CoquiTTS"] = false; } } else { health["CoquiTTS"] = false; } return health; } /// /// Gets a human-readable status message for AI services. /// /// Cancellation token /// Status message describing which services are available public virtual async Task GetServiceStatusMessageAsync( CancellationToken cancellationToken = default) { var health = await CheckServiceHealthAsync(cancellationToken); var available = health.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList(); var unavailable = health.Where(kvp => !kvp.Value).Select(kvp => kvp.Key).ToList(); var message = "AI Service Status: "; if (available.Count > 0) { message += "Available: "; message += string.Join(", ", available); } if (unavailable.Count > 0) { if (available.Count > 0) message += "; "; message += "Unavailable: "; message += string.Join(", ", unavailable); } return message; } /// /// Generates a fallback story when AI service is unavailable. /// private string GenerateFallbackStory( string level, string topic, IReadOnlyList vocabularyWords, int length) { _logger.LogInformation("Generating fallback story for: Level={Level}, Topic={Topic}", level, topic); // Create a simple story based on the topic and vocabulary var story = $"Ein {GetLevelDescription(level)} Story über {topic}."; if (vocabularyWords != null && vocabularyWords.Count > 0) { story += " Es enthält die Wörter: " + string.Join(", ", vocabularyWords.Take(5)); } // Add some generic story content based on topic story += " " + GetGenericStoryContent(topic, length); return story; } /// /// Generates fallback feedback when AI service is unavailable. /// private string GenerateFallbackFeedback(string userText, string level) { _logger.LogInformation("Generating fallback feedback for: Level={Level}", level); var feedback = $"Feedback for {level} level: "; // Simple length-based feedback if (string.IsNullOrWhiteSpace(userText)) { feedback += "Please write some text to receive feedback."; } else if (userText.Length < 20) { feedback += "Your text is quite short. Try writing a few more sentences."; } else if (userText.Length < 100) { feedback += "Good start! Your text is clear. Keep practicing to improve."; } else { feedback += "Great job! Your text is well-developed. Continue practicing to maintain your skills."; } return feedback; } /// /// Gets a description for the CEFR level. /// private string GetLevelDescription(string level) { return level.ToUpper() switch { "A1" => "einfacher", "A2" => "leichter", "B1" => "mittelschwerer", "B2" => "fortgeschrittener", "C1" => "komplexer", _ => "einfacher" }; } /// /// Gets generic story content based on topic. /// private string GetGenericStoryContent(string topic, int length) { // Simple topic-based content var topics = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["food"] = "Es geht um Essen und Trinken.", ["travel"] = "Es geht um Reisen und Abenteuer.", ["family"] = "Es geht um Familie und Beziehungen.", ["work"] = "Es geht um Arbeit und Beruf.", ["school"] = "Es geht um Schule und Lernen.", ["animals"] = "Es geht um Tiere und Natur.", ["sports"] = "Es geht um Sport und Bewegung.", ["holiday"] = "Es geht um Ferien und Feiertage.", ["weather"] = "Es geht um das Wetter.", ["shopping"] = "Es geht um Einkaufen." }; if (topics.TryGetValue(topic, out var content)) { return content; } return "Es geht um ein interessantes Thema."; } /// /// Tests the fallback service. /// /// Cancellation token /// True if service is working public virtual async Task TestServiceAsync(CancellationToken cancellationToken = default) { try { // Test fallback story generation var story = GenerateFallbackStory("A1", "Test", new List { "Test" }, 100); if (string.IsNullOrWhiteSpace(story)) return false; // Test fallback feedback generation var feedback = GenerateFallbackFeedback("Test", "A1"); if (string.IsNullOrWhiteSpace(feedback)) return false; // Test health check var health = await CheckServiceHealthAsync(cancellationToken); return health.Count > 0; } catch { return false; } } }