using DramaLing.Api.Models.DTOs; using DramaLing.Api.Models.Configuration; using Microsoft.Extensions.Options; using System.Text.Json; using System.Text; using System.Diagnostics; namespace DramaLing.Api.Services.AI; /// /// Google Gemini AI 提供商實作 /// public class GeminiAIProvider : IAIProvider { private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly GeminiOptions _options; private AIProviderStats _stats; public GeminiAIProvider(HttpClient httpClient, IOptions options, ILogger logger) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _options = options.Value ?? throw new ArgumentNullException(nameof(options)); _stats = new AIProviderStats(); _httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds); _httpClient.DefaultRequestHeaders.Add("User-Agent", "DramaLing/1.0"); _logger.LogInformation("GeminiAIProvider initialized with model: {Model}, timeout: {Timeout}s", _options.Model, _options.TimeoutSeconds); } #region IAIProvider 屬性 public string ProviderName => "Google Gemini"; public bool IsAvailable => !string.IsNullOrEmpty(_options.ApiKey); public decimal CostPerRequest => 0.001m; // 大概每次請求成本 public int MaxInputLength => _options.MaxOutputTokens / 4; // 粗略估計 public int AverageResponseTimeMs => _stats.AverageResponseTimeMs; #endregion #region 核心功能 public async Task AnalyzeSentenceAsync(string inputText, AnalysisOptions options) { var stopwatch = Stopwatch.StartNew(); _stats.TotalRequests++; try { _logger.LogInformation("Starting sentence analysis for text: {Text}", inputText.Substring(0, Math.Min(50, inputText.Length))); var prompt = BuildAnalysisPrompt(inputText); var aiResponse = await CallGeminiAPIAsync(prompt); if (string.IsNullOrWhiteSpace(aiResponse)) { throw new InvalidOperationException("Gemini API returned empty response"); } var analysisData = ParseAIResponse(inputText, aiResponse); stopwatch.Stop(); RecordSuccessfulRequest(stopwatch.ElapsedMilliseconds); _logger.LogInformation("Sentence analysis completed in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); return analysisData; } catch (Exception ex) { stopwatch.Stop(); RecordFailedRequest(stopwatch.ElapsedMilliseconds); _logger.LogError(ex, "Error analyzing sentence: {Text}", inputText); throw; } } public async Task CheckHealthAsync() { var stopwatch = Stopwatch.StartNew(); try { var testPrompt = "Test health check prompt"; var response = await CallGeminiAPIAsync(testPrompt); stopwatch.Stop(); return new AIProviderHealthStatus { IsHealthy = !string.IsNullOrEmpty(response), CheckedAt = DateTime.UtcNow, ResponseTimeMs = (int)stopwatch.ElapsedMilliseconds }; } catch (Exception ex) { stopwatch.Stop(); return new AIProviderHealthStatus { IsHealthy = false, ErrorMessage = ex.Message, CheckedAt = DateTime.UtcNow, ResponseTimeMs = (int)stopwatch.ElapsedMilliseconds }; } } public Task GetStatsAsync() { return Task.FromResult(_stats); } #endregion #region 私有方法 private string BuildAnalysisPrompt(string inputText) { return $@"You are an English learning assistant. Analyze this sentence and return ONLY a valid JSON response. **Input Sentence**: ""{inputText}"" **Required JSON Structure:** {{ ""sentenceTranslation"": ""Traditional Chinese translation of the entire sentence"", ""hasGrammarErrors"": true/false, ""grammarCorrections"": [ {{ ""original"": ""incorrect text"", ""corrected"": ""correct text"", ""type"": ""error type (tense/subject-verb/preposition/word-order)"", ""explanation"": ""brief explanation in Traditional Chinese"" }} ], ""vocabularyAnalysis"": {{ ""word1"": {{ ""word"": ""the word"", ""translation"": ""Traditional Chinese translation"", ""definition"": ""English definition"", ""partOfSpeech"": ""noun/verb/adjective/etc"", ""pronunciation"": ""/phonetic/"", ""difficultyLevel"": ""A1/A2/B1/B2/C1/C2"", ""frequency"": ""high/medium/low"", ""synonyms"": [""synonym1"", ""synonym2""], ""example"": ""example sentence"", ""exampleTranslation"": ""Traditional Chinese example translation"" }} }}, ""idioms"": [ {{ ""idiom"": ""idiomatic expression"", ""translation"": ""Traditional Chinese meaning"", ""definition"": ""English explanation"", ""pronunciation"": ""/phonetic notation/"", ""difficultyLevel"": ""A1/A2/B1/B2/C1/C2"", ""frequency"": ""high/medium/low"", ""synonyms"": [""synonym1"", ""synonym2""], ""example"": ""usage example"", ""exampleTranslation"": ""Traditional Chinese example"" }} ] }} **Analysis Guidelines:** 1. **Grammar Check**: Detect tense errors, subject-verb agreement, preposition usage, word order 2. **Vocabulary Analysis**: Include ALL significant words (exclude articles: a, an, the) 3. **CEFR Levels**: Assign accurate A1-C2 levels for each word 4. **Idioms**: Identify any idiomatic expressions or phrasal verbs 5. **Translations**: Use Traditional Chinese (Taiwan standard) **IMPORTANT**: Return ONLY the JSON object, no additional text or explanation."; } private async Task CallGeminiAPIAsync(string prompt) { try { var requestBody = new { contents = new[] { new { parts = new[] { new { text = prompt } } } }, generationConfig = new { temperature = _options.Temperature, topK = 40, topP = 0.95, maxOutputTokens = _options.MaxOutputTokens } }; var json = JsonSerializer.Serialize(requestBody); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync( $"{_options.BaseUrl}/v1beta/models/{_options.Model}:generateContent?key={_options.ApiKey}", content); response.EnsureSuccessStatusCode(); var responseJson = await response.Content.ReadAsStringAsync(); _logger.LogDebug("Raw Gemini API response: {Response}", responseJson.Substring(0, Math.Min(500, responseJson.Length))); return ExtractTextFromResponse(responseJson); } catch (Exception ex) { _logger.LogError(ex, "Gemini API call failed"); throw; } } private string ExtractTextFromResponse(string responseJson) { using var document = JsonDocument.Parse(responseJson); var root = document.RootElement; if (root.TryGetProperty("candidates", out var candidatesElement) && candidatesElement.ValueKind == JsonValueKind.Array) { var firstCandidate = candidatesElement.EnumerateArray().FirstOrDefault(); if (firstCandidate.ValueKind != JsonValueKind.Undefined && firstCandidate.TryGetProperty("content", out var contentElement) && contentElement.TryGetProperty("parts", out var partsElement) && partsElement.ValueKind == JsonValueKind.Array) { var firstPart = partsElement.EnumerateArray().FirstOrDefault(); if (firstPart.TryGetProperty("text", out var textElement)) { return textElement.GetString() ?? string.Empty; } } } // 檢查是否有安全過濾 if (root.TryGetProperty("promptFeedback", out _)) { _logger.LogWarning("Gemini content filtered due to safety policies"); return "The content analysis is temporarily unavailable due to safety filtering."; } return string.Empty; } private SentenceAnalysisData ParseAIResponse(string inputText, string aiResponse) { try { var cleanJson = CleanAIResponse(aiResponse); var aiAnalysis = JsonSerializer.Deserialize(cleanJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (aiAnalysis == null) { throw new InvalidOperationException("Failed to parse AI response JSON"); } return new SentenceAnalysisData { OriginalText = inputText, SentenceMeaning = aiAnalysis.SentenceTranslation ?? "", VocabularyAnalysis = ConvertVocabularyAnalysis(aiAnalysis.VocabularyAnalysis ?? new()), Idioms = ConvertIdioms(aiAnalysis.Idioms ?? new()), GrammarCorrection = ConvertGrammarCorrection(aiAnalysis), Metadata = new AnalysisMetadata { ProcessingDate = DateTime.UtcNow, AnalysisModel = _options.Model, AnalysisVersion = "2.0" } }; } catch (JsonException ex) { _logger.LogError(ex, "Failed to parse AI response as JSON: {Response}", aiResponse); return CreateFallbackAnalysis(inputText, aiResponse); } } private string CleanAIResponse(string aiResponse) { var cleanJson = aiResponse.Trim(); if (cleanJson.StartsWith("```json")) { cleanJson = cleanJson.Substring(7); } if (cleanJson.EndsWith("```")) { cleanJson = cleanJson.Substring(0, cleanJson.Length - 3); } return cleanJson.Trim(); } private Dictionary ConvertVocabularyAnalysis(Dictionary aiVocab) { var result = new Dictionary(); foreach (var kvp in aiVocab) { var aiWord = kvp.Value; result[kvp.Key] = new VocabularyAnalysisDto { Word = aiWord.Word ?? kvp.Key, Translation = aiWord.Translation ?? "", Definition = aiWord.Definition ?? "", PartOfSpeech = aiWord.PartOfSpeech ?? "unknown", Pronunciation = aiWord.Pronunciation ?? $"/{kvp.Key}/", DifficultyLevel = aiWord.DifficultyLevel ?? "A2", Frequency = aiWord.Frequency ?? "medium", Synonyms = aiWord.Synonyms ?? new List(), Example = aiWord.Example, ExampleTranslation = aiWord.ExampleTranslation, }; } return result; } private List ConvertIdioms(List aiIdioms) { var result = new List(); foreach (var aiIdiom in aiIdioms) { result.Add(new IdiomDto { Idiom = aiIdiom.Idiom ?? "", Translation = aiIdiom.Translation ?? "", Definition = aiIdiom.Definition ?? "", Pronunciation = aiIdiom.Pronunciation ?? "", DifficultyLevel = aiIdiom.DifficultyLevel ?? "B2", Frequency = aiIdiom.Frequency ?? "medium", Synonyms = aiIdiom.Synonyms ?? new List(), Example = aiIdiom.Example, ExampleTranslation = aiIdiom.ExampleTranslation }); } return result; } private GrammarCorrectionDto? ConvertGrammarCorrection(AiAnalysisResponse aiAnalysis) { if (!aiAnalysis.HasGrammarErrors || aiAnalysis.GrammarCorrections == null || !aiAnalysis.GrammarCorrections.Any()) { return null; } var corrections = aiAnalysis.GrammarCorrections.Select(gc => new GrammarErrorDto { Error = gc.Original ?? "", Correction = gc.Corrected ?? "", Type = gc.Type ?? "grammar", Explanation = gc.Explanation ?? "", Severity = "medium", Position = new ErrorPosition { Start = 0, End = 0 } }).ToList(); return new GrammarCorrectionDto { HasErrors = true, CorrectedText = string.Join(" ", corrections.Select(c => c.Correction)), Corrections = corrections }; } private SentenceAnalysisData CreateFallbackAnalysis(string inputText, string aiResponse) { _logger.LogWarning("Using fallback analysis due to JSON parsing failure"); return new SentenceAnalysisData { OriginalText = inputText, SentenceMeaning = aiResponse, VocabularyAnalysis = new Dictionary(), Metadata = new AnalysisMetadata { ProcessingDate = DateTime.UtcNow, AnalysisModel = $"{_options.Model}-fallback", AnalysisVersion = "2.0" }, }; } private void RecordSuccessfulRequest(long elapsedMs) { _stats.SuccessfulRequests++; _stats.LastUsedAt = DateTime.UtcNow; _stats.TotalCost += CostPerRequest; UpdateAverageResponseTime((int)elapsedMs); } private void RecordFailedRequest(long elapsedMs) { _stats.FailedRequests++; UpdateAverageResponseTime((int)elapsedMs); } private void UpdateAverageResponseTime(int responseTimeMs) { if (_stats.AverageResponseTimeMs == 0) { _stats.AverageResponseTimeMs = responseTimeMs; } else { _stats.AverageResponseTimeMs = (_stats.AverageResponseTimeMs + responseTimeMs) / 2; } } #endregion } #region AI Response Models (重用現有的模型) internal class AiAnalysisResponse { public string? SentenceTranslation { get; set; } public bool HasGrammarErrors { get; set; } public List? GrammarCorrections { get; set; } public Dictionary? VocabularyAnalysis { get; set; } public List? Idioms { get; set; } } internal class AiGrammarCorrection { public string? Original { get; set; } public string? Corrected { get; set; } public string? Type { get; set; } public string? Explanation { get; set; } } internal class AiVocabularyAnalysis { public string? Word { get; set; } public string? Translation { get; set; } public string? Definition { get; set; } public string? PartOfSpeech { get; set; } public string? Pronunciation { get; set; } public string? DifficultyLevel { get; set; } public string? Frequency { get; set; } public List? Synonyms { get; set; } public string? Example { get; set; } public string? ExampleTranslation { get; set; } } internal class AiIdiom { public string? Idiom { get; set; } public string? Translation { get; set; } public string? Definition { get; set; } public string? Pronunciation { get; set; } public string? DifficultyLevel { get; set; } public string? Frequency { get; set; } public List? Synonyms { get; set; } public string? Example { get; set; } public string? ExampleTranslation { get; set; } } #endregion