- 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>
358 lines
12 KiB
C#
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;
|
|
}
|