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;
}
}
}