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