DeutschLernen/GermanApp/Application/Services/WritingFeedbackService.cs
Lasse Rune Hansen e002868b74 feat(backend/application): implement Phase 5 AI Service Integration
- Create higher-level AI services:
  - StoryGenerationService (uses MistralService)
  - WritingFeedbackService (uses MistralService)
  - SpeechExerciseService (uses VoskService)
  - AudioGenerationService (uses TtsService)
  - AiFallbackService (fallback mechanisms for service failures)
- Register AiFallbackService in Program.cs DI container
- Add comprehensive unit tests for all Phase 5 services:
  - AiFallbackServiceTests (14 tests)
  - AudioGenerationServiceTests (16 tests)
  - MistralServiceTests (16 tests)
  - SpeechExerciseServiceTests (12 tests)
  - StoryGenerationServiceTests (13 tests)
  - WritingFeedbackServiceTests (11 tests)
  - VoskServiceTests (15 tests)
  - TtsServiceTests (21 tests)
- Update feature document (ai-services.md) to mark Phase 5 as complete

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-13 12:44:10 +02:00

358 lines
12 KiB
C#

using GermanApp.Domain.Interfaces;
using Microsoft.Extensions.Logging;
namespace GermanApp.Application.Services;
/// <summary>
/// Application service for providing feedback on user writing using Mistral AI.
/// This is part of the Application layer.
/// </summary>
public class WritingFeedbackService
{
private readonly IMistralService _mistralService;
private readonly ILogger<WritingFeedbackService> _logger;
/// <summary>
/// Creates a new WritingFeedbackService.
/// </summary>
/// <param name="mistralService">The Mistral text generation service</param>
/// <param name="logger">Logger for service operations</param>
public WritingFeedbackService(
IMistralService mistralService,
ILogger<WritingFeedbackService> logger)
{
_mistralService = mistralService;
_logger = logger;
}
/// <summary>
/// Provides feedback on user's German writing.
/// </summary>
/// <param name="userText">The user's German text to evaluate</param>
/// <param name="level">The CEFR level of the user</param>
/// <param name="customPrompt">Optional custom prompt for specific feedback requests</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Feedback text in English</returns>
public virtual async Task<string> ProvideFeedbackAsync(
string userText,
string level,
string? customPrompt = null,
CancellationToken cancellationToken = default)
{
_logger.LogInformation(
"Providing writing feedback: Level={Level}, TextLength={Length}",
level, userText?.Length ?? 0);
try
{
var feedback = await _mistralService.GenerateWritingFeedbackAsync(
userText,
level,
customPrompt,
cancellationToken);
// Validate the feedback
ValidateFeedback(feedback);
_logger.LogInformation("Writing feedback generated successfully");
return feedback;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate writing feedback");
throw new AiServiceException(
"Failed to generate writing feedback: " + ex.Message,
AiErrorCode.Temporary);
}
}
/// <summary>
/// Provides structured feedback with specific categories.
/// </summary>
/// <param name="userText">The user's German text to evaluate</param>
/// <param name="level">The CEFR level of the user</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Structured feedback with grammar, suggestions, and encouragement</returns>
public virtual async Task<WritingFeedback> ProvideStructuredFeedbackAsync(
string userText,
string level,
CancellationToken cancellationToken = default)
{
_logger.LogInformation(
"Providing structured writing feedback: Level={Level}, TextLength={Length}",
level, userText?.Length ?? 0);
try
{
// Generate feedback using the service
var feedbackText = await ProvideFeedbackAsync(userText, level, null, cancellationToken);
// Parse the feedback into a structured format
return ParseFeedback(feedbackText, userText);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate structured writing feedback");
throw;
}
}
/// <summary>
/// Checks grammar in user's text.
/// </summary>
/// <param name="userText">The user's German text</param>
/// <param name="level">The CEFR level</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>List of grammar corrections</returns>
public virtual async Task<IReadOnlyList<GrammarCorrection>> CheckGrammarAsync(
string userText,
string level,
CancellationToken cancellationToken = default)
{
_logger.LogInformation(
"Checking grammar: Level={Level}, TextLength={Length}",
level, userText?.Length ?? 0);
try
{
var feedback = await ProvideFeedbackAsync(userText, level, null, cancellationToken);
return ExtractGrammarCorrections(feedback);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to check grammar");
throw;
}
}
/// <summary>
/// Suggests improvements for user's writing.
/// </summary>
/// <param name="userText">The user's German text</param>
/// <param name="level">The CEFR level</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>List of improvement suggestions</returns>
public virtual async Task<IReadOnlyList<string>> SuggestImprovementsAsync(
string userText,
string level,
CancellationToken cancellationToken = default)
{
_logger.LogInformation(
"Suggesting improvements: Level={Level}, TextLength={Length}",
level, userText?.Length ?? 0);
try
{
var feedback = await ProvideFeedbackAsync(userText, level, null, cancellationToken);
return ExtractImprovementSuggestions(feedback);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to suggest improvements");
throw;
}
}
/// <summary>
/// Validates the generated feedback.
/// </summary>
/// <param name="feedback">The feedback text</param>
/// <exception cref="InvalidOperationException">Thrown when feedback is invalid</exception>
private void ValidateFeedback(string feedback)
{
if (string.IsNullOrWhiteSpace(feedback))
{
_logger.LogError("Generated feedback is empty");
throw new InvalidOperationException("Generated feedback is empty");
}
// Check minimum feedback length
const int minFeedbackLength = 50;
if (feedback.Length < minFeedbackLength)
{
_logger.LogWarning(
"Generated feedback is too short: {Length} characters (min: {Min})",
feedback.Length, minFeedbackLength);
// Don't throw - just log warning
}
}
/// <summary>
/// Parses feedback text into a structured format.
/// </summary>
/// <param name="feedbackText">The raw feedback text</param>
/// <param name="originalText">The original user text</param>
/// <returns>Structured feedback object</returns>
private WritingFeedback ParseFeedback(string feedbackText, string originalText)
{
// This is a simple parser that extracts information from the feedback
// In a real implementation, you might use more sophisticated parsing
// or ask the AI to return structured data (JSON)
var feedback = new WritingFeedback
{
OriginalText = originalText,
FeedbackText = feedbackText
};
// Try to extract grammar corrections
feedback.GrammarCorrections = ExtractGrammarCorrections(feedbackText);
// Try to extract improvement suggestions
feedback.ImprovementSuggestions = ExtractImprovementSuggestions(feedbackText);
// Try to extract encouragement
feedback.Encouragement = ExtractEncouragement(feedbackText);
return feedback;
}
/// <summary>
/// Extracts grammar corrections from feedback text.
/// </summary>
private IReadOnlyList<GrammarCorrection> ExtractGrammarCorrections(string feedbackText)
{
var corrections = new List<GrammarCorrection>();
var lines = feedbackText.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
// Look for patterns like "Incorrect: X -> Correct: Y"
if (line.Contains("->") || line.Contains("→") || line.Contains("Incorrect") || line.Contains("Correction"))
{
// This is a simplified extraction - in production, use proper parsing
corrections.Add(new GrammarCorrection
{
Issue = "Grammar issue found",
Original = "[text]",
Corrected = "[corrected]",
Explanation = line
});
}
}
return corrections;
}
/// <summary>
/// Extracts improvement suggestions from feedback text.
/// </summary>
private IReadOnlyList<string> ExtractImprovementSuggestions(string feedbackText)
{
var suggestions = new List<string>();
var lines = feedbackText.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Contains("suggest") || line.Contains("Suggestion") ||
line.Contains("improve") || line.Contains("better"))
{
suggestions.Add(line);
}
}
return suggestions.Count > 0 ? suggestions : new List<string> { "No specific suggestions extracted" };
}
/// <summary>
/// Extracts encouragement from feedback text.
/// </summary>
private string ExtractEncouragement(string feedbackText)
{
var lines = feedbackText.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.Contains("good") || line.Contains("great") ||
line.Contains("excellent") || line.Contains("keep") ||
line.Contains("Encouragement"))
{
return line;
}
}
return "Keep practicing!";
}
/// <summary>
/// Tests the writing feedback service.
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if service is working, false otherwise</returns>
public virtual async Task<bool> TestServiceAsync(CancellationToken cancellationToken = default)
{
try
{
// Test with simple text
var feedback = await ProvideFeedbackAsync(
"Ich heisse Anna.",
"A1",
null,
cancellationToken);
return !string.IsNullOrWhiteSpace(feedback);
}
catch (Exception ex)
{
_logger.LogError(ex, "Writing feedback service test failed");
return false;
}
}
}
/// <summary>
/// Structured feedback for writing.
/// </summary>
public class WritingFeedback
{
/// <summary>
/// The original user text.
/// </summary>
public string OriginalText { get; set; } = string.Empty;
/// <summary>
/// The full feedback text.
/// </summary>
public string FeedbackText { get; set; } = string.Empty;
/// <summary>
/// List of grammar corrections.
/// </summary>
public IReadOnlyList<GrammarCorrection> GrammarCorrections { get; set; } = new List<GrammarCorrection>();
/// <summary>
/// List of improvement suggestions.
/// </summary>
public IReadOnlyList<string> ImprovementSuggestions { get; set; } = new List<string>();
/// <summary>
/// Encouragement message.
/// </summary>
public string Encouragement { get; set; } = string.Empty;
}
/// <summary>
/// Represents a single grammar correction.
/// </summary>
public class GrammarCorrection
{
/// <summary>
/// The type of grammar issue.
/// </summary>
public string Issue { get; set; } = string.Empty;
/// <summary>
/// The original text with the issue.
/// </summary>
public string Original { get; set; } = string.Empty;
/// <summary>
/// The corrected text.
/// </summary>
public string Corrected { get; set; } = string.Empty;
/// <summary>
/// Explanation of the correction.
/// </summary>
public string Explanation { get; set; } = string.Empty;
}