using GermanApp.Domain.Interfaces; using Microsoft.Extensions.Logging; namespace GermanApp.Application.Services; /// /// Application service for generating stories using Mistral AI. /// This is part of the Application layer. /// public class StoryGenerationService { private readonly IMistralService _mistralService; private readonly ILogger _logger; /// /// Creates a new StoryGenerationService. /// /// The Mistral text generation service /// Logger for service operations public StoryGenerationService( IMistralService mistralService, ILogger logger) { _mistralService = mistralService; _logger = logger; } /// /// Generates a story based on the given parameters. /// /// The CEFR level (A1, A2, B1, B2, C1) /// The story topic or theme /// List of German vocabulary words to include in the story /// Approximate word count for the story /// Cancellation token /// The generated story text in German public virtual async Task GenerateStoryAsync( string level, string topic, IReadOnlyList vocabularyWords, int length = 200, CancellationToken cancellationToken = default) { _logger.LogInformation( "Generating story: Level={Level}, Topic={Topic}, VocabularyCount={Count}, Length={Length}", level, topic, vocabularyWords?.Count ?? 0, length); try { var story = await _mistralService.GenerateStoryAsync( level, topic, vocabularyWords, length, cancellationToken); // Validate the generated story ValidateStory(story, level, vocabularyWords); _logger.LogInformation("Story generated successfully"); return story; } catch (Exception ex) { _logger.LogError(ex, "Failed to generate story"); throw new AiServiceException( "Failed to generate story: " + ex.Message, AiErrorCode.Temporary); } } /// /// Generates a story for a specific lesson. /// /// The lesson title for context /// The CEFR level /// List of vocabulary words from the lesson /// Approximate word count /// Cancellation token /// The generated story text public virtual async Task GenerateLessonStoryAsync( string lessonTitle, string level, IReadOnlyList vocabularyWords, int length = 250, CancellationToken cancellationToken = default) { _logger.LogInformation( "Generating lesson story: Lesson={Lesson}, Level={Level}, VocabularyCount={Count}", lessonTitle, level, vocabularyWords?.Count ?? 0); return await GenerateStoryAsync( level, lessonTitle, vocabularyWords, length, cancellationToken); } /// /// Generates multiple stories for different levels. /// /// The common topic for all stories /// Dictionary mapping CEFR levels to vocabulary words /// Dictionary mapping CEFR levels to story lengths /// Cancellation token /// Dictionary mapping levels to generated stories public virtual async Task> GenerateStoriesByLevelAsync( string topic, IDictionary> vocabularyByLevel, IDictionary? lengthByLevel = null, CancellationToken cancellationToken = default) { _logger.LogInformation( "Generating stories for multiple levels: Topic={Topic}, LevelCount={Count}", topic, vocabularyByLevel?.Count ?? 0); var results = new Dictionary(); lengthByLevel ??= new Dictionary(); foreach (var kvp in vocabularyByLevel) { var level = kvp.Key; var vocabulary = kvp.Value; var length = lengthByLevel.TryGetValue(level, out var l) ? l : 200; try { var story = await GenerateStoryAsync( level, topic, vocabulary, length, cancellationToken); results[level] = story; _logger.LogInformation("Generated story for level: {Level}", level); } catch (Exception ex) { _logger.LogError(ex, "Failed to generate story for level {Level}", level); results[level] = string.Empty; } } return results; } /// /// Validates the generated story meets basic requirements. /// /// The generated story text /// The target CEFR level /// The vocabulary words that should be included /// Thrown when story validation fails private void ValidateStory( string story, string level, IReadOnlyList? vocabularyWords) { if (string.IsNullOrWhiteSpace(story)) { _logger.LogError("Generated story is empty"); throw new InvalidOperationException("Generated story is empty"); } // Check minimum length based on level var minLength = GetMinStoryLength(level); if (story.Length < minLength) { _logger.LogWarning( "Generated story is too short: {Length} characters (min: {Min})", story.Length, minLength); // Don't throw - just log warning, AI might generate short stories } // Check if vocabulary words are included (if provided) if (vocabularyWords != null && vocabularyWords.Count > 0) { var storyLower = story.ToLower(); var missingWords = vocabularyWords .Where(w => !string.IsNullOrWhiteSpace(w)) .Where(w => !storyLower.Contains(w.ToLower())) .ToList(); if (missingWords.Count > vocabularyWords.Count * 0.5) { _logger.LogWarning( "Many vocabulary words missing from story: {MissingCount}/{TotalCount}", missingWords.Count, vocabularyWords.Count); } } } /// /// Gets the minimum expected story length based on CEFR level. /// /// The CEFR level /// Minimum character count private int GetMinStoryLength(string level) { return level.ToUpper() switch { "A1" => 100, "A2" => 150, "B1" => 200, "B2" => 300, "C1" => 400, _ => 100 }; } /// /// Tests the story generation service. /// /// Cancellation token /// True if service is working, false otherwise public virtual async Task TestServiceAsync(CancellationToken cancellationToken = default) { try { // Generate a simple test story var story = await GenerateStoryAsync( "A1", "Test", new List { "Hallo", "Welt" }, 50, cancellationToken); return !string.IsNullOrWhiteSpace(story); } catch (Exception ex) { _logger.LogError(ex, "Story generation service test failed"); return false; } } }