dramaling-vocab-learning/backend/DramaLing.Api/Services/GeminiService.cs

445 lines
17 KiB
C#

using DramaLing.Api.Models.DTOs;
using System.Text.Json;
using System.Text;
namespace DramaLing.Api.Services;
public interface IGeminiService
{
Task<SentenceAnalysisData> AnalyzeSentenceAsync(string inputText, string userLevel, AnalysisOptions options);
}
public class GeminiService : IGeminiService
{
private readonly HttpClient _httpClient;
private readonly ILogger<GeminiService> _logger;
private readonly string _apiKey;
public GeminiService(HttpClient httpClient, IConfiguration configuration, ILogger<GeminiService> logger)
{
_httpClient = httpClient;
_logger = logger;
_apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY")
?? configuration["AI:GeminiApiKey"]
?? configuration["Gemini:ApiKey"]
?? throw new InvalidOperationException("Gemini API Key not configured");
_logger.LogInformation("GeminiService initialized with API key: {ApiKeyStart}...",
_apiKey.Length > 10 ? _apiKey.Substring(0, 10) : "[key-not-set]");
_httpClient.DefaultRequestHeaders.Add("User-Agent", "DramaLing/1.0");
}
public async Task<SentenceAnalysisData> AnalyzeSentenceAsync(string inputText, string userLevel, AnalysisOptions options)
{
var startTime = DateTime.UtcNow;
try
{
_logger.LogInformation("Starting sentence analysis for text: {Text}, UserLevel: {UserLevel}",
inputText.Substring(0, Math.Min(50, inputText.Length)), userLevel);
// 符合產品需求規格的結構化 prompt
var prompt = $@"You are an English learning assistant. Analyze this sentence for a {userLevel} CEFR level learner and return ONLY a valid JSON response.
**Input Sentence**: ""{inputText}""
**Learner Level**: {userLevel}
**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"",
""isIdiom"": false,
""frequency"": ""high/medium/low"",
""synonyms"": [""synonym1"", ""synonym2""],
""example"": ""example sentence"",
""exampleTranslation"": ""Traditional Chinese example translation""
}}
}},
""idioms"": [
{{
""phrase"": ""idiomatic expression"",
""translation"": ""Traditional Chinese meaning"",
""definition"": ""English explanation"",
""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.";
var aiResponse = await CallGeminiAPI(prompt);
_logger.LogInformation("Gemini AI response received: {ResponseLength} characters", aiResponse?.Length ?? 0);
if (string.IsNullOrWhiteSpace(aiResponse))
{
throw new InvalidOperationException("Gemini API returned empty response");
}
// 檢查是否是安全過濾的回退訊息
if (aiResponse.Contains("temporarily unavailable due to safety filtering"))
{
// 這是安全過濾的情況,但我們仍然要處理它而不是拋出異常
_logger.LogWarning("Using safety filtering fallback response");
}
// 直接使用 AI 的回應創建分析數據
var analysisData = CreateAnalysisFromAIResponse(inputText, userLevel, aiResponse);
var processingTime = (DateTime.UtcNow - startTime).TotalSeconds;
_logger.LogInformation("Sentence analysis completed in {ProcessingTime}s", processingTime);
return analysisData;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error analyzing sentence: {Text}", inputText);
throw;
}
}
private SentenceAnalysisData CreateAnalysisFromAIResponse(string inputText, string userLevel, string aiResponse)
{
_logger.LogInformation("Creating analysis from AI response: {ResponsePreview}...",
aiResponse.Substring(0, Math.Min(100, aiResponse.Length)));
try
{
// 清理 AI 回應以確保是純 JSON
var cleanJson = aiResponse.Trim();
if (cleanJson.StartsWith("```json"))
{
cleanJson = cleanJson.Substring(7);
}
if (cleanJson.EndsWith("```"))
{
cleanJson = cleanJson.Substring(0, cleanJson.Length - 3);
}
// 解析 AI 回應的 JSON
var aiAnalysis = JsonSerializer.Deserialize<AiAnalysisResponse>(cleanJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (aiAnalysis == null)
{
throw new InvalidOperationException("Failed to parse AI response JSON");
}
// 轉換為 DTO 結構
var analysisData = new SentenceAnalysisData
{
OriginalText = inputText,
SentenceMeaning = aiAnalysis.SentenceTranslation ?? "",
VocabularyAnalysis = ConvertVocabularyAnalysis(aiAnalysis.VocabularyAnalysis ?? new()),
GrammarCorrection = ConvertGrammarCorrection(aiAnalysis),
Metadata = new AnalysisMetadata
{
UserLevel = userLevel,
ProcessingDate = DateTime.UtcNow,
AnalysisModel = "gemini-1.5-flash",
AnalysisVersion = "2.0"
}
};
// 計算統計
analysisData.Statistics = CalculateStatistics(analysisData.VocabularyAnalysis, userLevel);
return analysisData;
}
catch (JsonException ex)
{
_logger.LogError(ex, "Failed to parse AI response as JSON: {Response}", aiResponse);
// 回退到舊的處理方式
return CreateFallbackAnalysis(inputText, userLevel, aiResponse);
}
}
private Dictionary<string, VocabularyAnalysisDto> ConvertVocabularyAnalysis(Dictionary<string, AiVocabularyAnalysis> aiVocab)
{
var result = new Dictionary<string, VocabularyAnalysisDto>();
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",
IsIdiom = aiWord.IsIdiom,
Frequency = aiWord.Frequency ?? "medium",
Synonyms = aiWord.Synonyms ?? new List<string>(),
Example = aiWord.Example,
ExampleTranslation = aiWord.ExampleTranslation,
Tags = new List<string> { "ai-analyzed", "gemini" }
};
}
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 userLevel, string aiResponse)
{
_logger.LogWarning("Using fallback analysis due to JSON parsing failure");
return new SentenceAnalysisData
{
OriginalText = inputText,
SentenceMeaning = aiResponse,
VocabularyAnalysis = CreateBasicVocabularyFromText(inputText, aiResponse),
Metadata = new AnalysisMetadata
{
UserLevel = userLevel,
ProcessingDate = DateTime.UtcNow,
AnalysisModel = "gemini-1.5-flash-fallback",
AnalysisVersion = "1.0"
},
Statistics = new AnalysisStatistics()
};
}
private Dictionary<string, VocabularyAnalysisDto> CreateBasicVocabularyFromText(string inputText, string aiResponse)
{
var words = inputText.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var result = new Dictionary<string, VocabularyAnalysisDto>();
foreach (var word in words.Take(15))
{
var cleanWord = word.ToLower().Trim('.', ',', '!', '?', ';', ':', '"', '\'');
if (string.IsNullOrEmpty(cleanWord) || cleanWord.Length < 2) continue;
result[cleanWord] = new VocabularyAnalysisDto
{
Word = cleanWord,
Translation = $"{cleanWord} - AI分析請查看上方詳細說明",
Definition = $"Definition in AI analysis above",
PartOfSpeech = "word",
Pronunciation = $"/{cleanWord}/",
DifficultyLevel = EstimateBasicDifficulty(cleanWord),
IsIdiom = false, // 統一使用 IsIdiom
Frequency = "medium",
Synonyms = new List<string>(),
Example = $"See AI analysis for {cleanWord}",
ExampleTranslation = "詳見上方AI分析",
Tags = new List<string> { "ai-analyzed" }
};
}
return result;
}
private string EstimateBasicDifficulty(string word)
{
var basicWords = new[] { "i", "you", "he", "she", "it", "we", "they", "the", "a", "an", "is", "are", "was", "were" };
return basicWords.Contains(word.ToLower()) ? "A1" : "A2";
}
private AnalysisStatistics CalculateStatistics(Dictionary<string, VocabularyAnalysisDto> vocabulary, string userLevel)
{
var stats = new AnalysisStatistics
{
TotalWords = vocabulary.Count,
UniqueWords = vocabulary.Count,
SimpleWords = vocabulary.Count(kvp => kvp.Value.DifficultyLevel == "A1"),
ModerateWords = vocabulary.Count(kvp => kvp.Value.DifficultyLevel == "A2"),
DifficultWords = vocabulary.Count(kvp => !new[] { "A1", "A2" }.Contains(kvp.Value.DifficultyLevel)),
Idioms = vocabulary.Count(kvp => kvp.Value.IsIdiom), // 統一使用 Idioms
AverageDifficulty = userLevel
};
return stats;
}
private async Task<string> CallGeminiAPI(string prompt)
{
try
{
var requestBody = new
{
contents = new[]
{
new
{
parts = new[]
{
new { text = prompt }
}
}
},
generationConfig = new
{
temperature = 0.7,
topK = 40,
topP = 0.95,
maxOutputTokens = 2000
}
};
var json = JsonSerializer.Serialize(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={_apiKey}", content);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
_logger.LogInformation("Raw Gemini API response: {Response}", responseJson.Substring(0, Math.Min(500, responseJson.Length)));
var geminiResponse = JsonSerializer.Deserialize<GeminiApiResponse>(responseJson);
// Check for safety filter blocking
if (geminiResponse?.PromptFeedback != null)
{
_logger.LogWarning("Gemini prompt feedback received: {Feedback}", JsonSerializer.Serialize(geminiResponse.PromptFeedback));
}
// 詳細調試信息
_logger.LogInformation("Gemini response candidates count: {Count}", geminiResponse?.Candidates?.Count ?? 0);
var firstCandidate = geminiResponse?.Candidates?.FirstOrDefault();
_logger.LogInformation("First candidate content: {HasContent}", firstCandidate?.Content != null);
_logger.LogInformation("First candidate parts count: {Count}", firstCandidate?.Content?.Parts?.Count ?? 0);
// Try to get response text
var aiText = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text ?? string.Empty;
_logger.LogInformation("Gemini API returned: {ResponseLength} characters", aiText.Length);
if (!string.IsNullOrEmpty(aiText))
{
_logger.LogInformation("AI text preview: {Preview}", aiText.Substring(0, Math.Min(200, aiText.Length)));
}
// If no text but we have a successful response, it might be safety filtered
if (string.IsNullOrWhiteSpace(aiText) && geminiResponse?.PromptFeedback != null)
{
// Return a fallback response instead of throwing
return "The content analysis is temporarily unavailable due to safety filtering. Please try with different content.";
}
return aiText;
}
catch (Exception ex)
{
_logger.LogError(ex, "Gemini API call failed");
throw;
}
}
}
// Gemini API response models
internal class GeminiApiResponse
{
public List<GeminiCandidate>? Candidates { get; set; }
public object? PromptFeedback { get; set; }
}
internal class GeminiCandidate
{
public GeminiContent? Content { get; set; }
}
internal class GeminiContent
{
public List<GeminiPart>? Parts { get; set; }
}
internal class GeminiPart
{
public string? Text { get; set; }
}
// AI Response JSON Models
internal class AiAnalysisResponse
{
public string? SentenceTranslation { get; set; }
public bool HasGrammarErrors { get; set; }
public List<AiGrammarCorrection>? GrammarCorrections { get; set; }
public Dictionary<string, AiVocabularyAnalysis>? VocabularyAnalysis { get; set; }
public List<AiIdiom>? 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 bool IsIdiom { get; set; }
public string? Frequency { get; set; }
public List<string>? Synonyms { get; set; }
public string? Example { get; set; }
public string? ExampleTranslation { get; set; }
}
internal class AiIdiom
{
public string? Phrase { get; set; }
public string? Translation { get; set; }
public string? Definition { get; set; }
public string? Example { get; set; }
public string? ExampleTranslation { get; set; }
}