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

354 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text.Json;
using System.Text;
using DramaLing.Api.Models.Entities;
namespace DramaLing.Api.Services;
public interface IGeminiService
{
Task<List<GeneratedCard>> GenerateCardsAsync(string inputText, string extractionType, int cardCount);
Task<ValidationResult> ValidateCardAsync(Flashcard card);
}
public class GeminiService : IGeminiService
{
private readonly HttpClient _httpClient;
private readonly IConfiguration _configuration;
private readonly ILogger<GeminiService> _logger;
private readonly string _apiKey;
public GeminiService(HttpClient httpClient, IConfiguration configuration, ILogger<GeminiService> logger)
{
_httpClient = httpClient;
_configuration = configuration;
_logger = logger;
_apiKey = Environment.GetEnvironmentVariable("DRAMALING_GEMINI_API_KEY")
?? _configuration["AI:GeminiApiKey"] ?? "";
}
public async Task<List<GeneratedCard>> GenerateCardsAsync(string inputText, string extractionType, int cardCount)
{
try
{
if (string.IsNullOrEmpty(_apiKey) || _apiKey == "your-gemini-api-key-here")
{
throw new InvalidOperationException("Gemini API key not configured");
}
var prompt = BuildPrompt(inputText, extractionType, cardCount);
var response = await CallGeminiApiAsync(prompt);
return ParseGeneratedCards(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating cards with Gemini API");
throw;
}
}
public async Task<ValidationResult> ValidateCardAsync(Flashcard card)
{
try
{
if (string.IsNullOrEmpty(_apiKey) || _apiKey == "your-gemini-api-key-here")
{
throw new InvalidOperationException("Gemini API key not configured");
}
var prompt = BuildValidationPrompt(card);
var response = await CallGeminiApiAsync(prompt);
return ParseValidationResult(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error validating card with Gemini API");
throw;
}
}
private string BuildPrompt(string inputText, string extractionType, int cardCount)
{
var template = extractionType == "vocabulary" ? VocabularyExtractionPrompt : SmartExtractionPrompt;
return template
.Replace("{cardCount}", cardCount.ToString())
.Replace("{inputText}", inputText);
}
private string BuildValidationPrompt(Flashcard card)
{
return CardValidationPrompt
.Replace("{word}", card.Word)
.Replace("{translation}", card.Translation)
.Replace("{definition}", card.Definition)
.Replace("{partOfSpeech}", card.PartOfSpeech ?? "")
.Replace("{pronunciation}", card.Pronunciation ?? "")
.Replace("{example}", card.Example ?? "");
}
private async Task<string> CallGeminiApiAsync(string prompt)
{
var requestBody = new
{
contents = new[]
{
new
{
parts = new[]
{
new { text = prompt }
}
}
}
};
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-latest:generateContent?key={_apiKey}",
content);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
_logger.LogError("Gemini API error: {StatusCode} - {Content}", response.StatusCode, errorContent);
throw new HttpRequestException($"Gemini API request failed: {response.StatusCode}");
}
var responseContent = await response.Content.ReadAsStringAsync();
var geminiResponse = JsonSerializer.Deserialize<JsonElement>(responseContent);
if (geminiResponse.TryGetProperty("candidates", out var candidates) &&
candidates.GetArrayLength() > 0 &&
candidates[0].TryGetProperty("content", out var contentElement) &&
contentElement.TryGetProperty("parts", out var parts) &&
parts.GetArrayLength() > 0 &&
parts[0].TryGetProperty("text", out var textElement))
{
return textElement.GetString() ?? "";
}
throw new InvalidOperationException("Invalid response format from Gemini API");
}
private List<GeneratedCard> ParseGeneratedCards(string response)
{
try
{
// 清理回應文本
var cleanText = response.Trim();
cleanText = cleanText.Replace("```json", "").Replace("```", "").Trim();
// 如果不是以 { 開始,嘗試找到 JSON 部分
if (!cleanText.StartsWith("{"))
{
var jsonStart = cleanText.IndexOf("{");
if (jsonStart >= 0)
{
cleanText = cleanText[jsonStart..];
}
}
var jsonResponse = JsonSerializer.Deserialize<JsonElement>(cleanText);
if (!jsonResponse.TryGetProperty("cards", out var cardsElement) || cardsElement.ValueKind != JsonValueKind.Array)
{
throw new InvalidOperationException("Response does not contain cards array");
}
var cards = new List<GeneratedCard>();
foreach (var cardElement in cardsElement.EnumerateArray())
{
var card = new GeneratedCard
{
Word = GetStringProperty(cardElement, "word"),
PartOfSpeech = GetStringProperty(cardElement, "part_of_speech"),
Pronunciation = GetStringProperty(cardElement, "pronunciation"),
Translation = GetStringProperty(cardElement, "translation"),
Definition = GetStringProperty(cardElement, "definition"),
Synonyms = GetArrayProperty(cardElement, "synonyms"),
Example = GetStringProperty(cardElement, "example"),
ExampleTranslation = GetStringProperty(cardElement, "example_translation"),
DifficultyLevel = GetStringProperty(cardElement, "difficulty_level")
};
if (!string.IsNullOrEmpty(card.Word) && !string.IsNullOrEmpty(card.Translation))
{
cards.Add(card);
}
}
return cards;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing generated cards response: {Response}", response);
throw new InvalidOperationException($"Failed to parse AI response: {ex.Message}");
}
}
private ValidationResult ParseValidationResult(string response)
{
try
{
var cleanText = response.Trim().Replace("```json", "").Replace("```", "").Trim();
var jsonResponse = JsonSerializer.Deserialize<JsonElement>(cleanText);
var issues = new List<ValidationIssue>();
if (jsonResponse.TryGetProperty("issues", out var issuesElement))
{
foreach (var issueElement in issuesElement.EnumerateArray())
{
issues.Add(new ValidationIssue
{
Field = GetStringProperty(issueElement, "field"),
Original = GetStringProperty(issueElement, "original"),
Corrected = GetStringProperty(issueElement, "corrected"),
Reason = GetStringProperty(issueElement, "reason"),
Severity = GetStringProperty(issueElement, "severity")
});
}
}
var suggestions = new List<string>();
if (jsonResponse.TryGetProperty("suggestions", out var suggestionsElement))
{
foreach (var suggestion in suggestionsElement.EnumerateArray())
{
suggestions.Add(suggestion.GetString() ?? "");
}
}
return new ValidationResult
{
Issues = issues,
Suggestions = suggestions,
OverallScore = jsonResponse.TryGetProperty("overall_score", out var scoreElement)
? scoreElement.GetInt32() : 85,
Confidence = jsonResponse.TryGetProperty("confidence", out var confidenceElement)
? confidenceElement.GetDouble() : 0.9
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing validation result: {Response}", response);
throw new InvalidOperationException($"Failed to parse validation response: {ex.Message}");
}
}
private static string GetStringProperty(JsonElement element, string propertyName)
{
return element.TryGetProperty(propertyName, out var prop) ? prop.GetString() ?? "" : "";
}
private static List<string> GetArrayProperty(JsonElement element, string propertyName)
{
var result = new List<string>();
if (element.TryGetProperty(propertyName, out var arrayElement) && arrayElement.ValueKind == JsonValueKind.Array)
{
foreach (var item in arrayElement.EnumerateArray())
{
result.Add(item.GetString() ?? "");
}
}
return result;
}
// Prompt 模板
private const string VocabularyExtractionPrompt = @"
從以下英文文本中萃取 {cardCount} 個最重要的詞彙,為每個詞彙生成詞卡資料。
輸入文本:
{inputText}
請按照以下 JSON 格式回應,不要包含任何其他文字或代碼塊標記:
{
""cards"": [
{
""word"": ""單字原型"",
""part_of_speech"": ""詞性(n./v./adj./adv.等)"",
""pronunciation"": ""IPA音標"",
""translation"": ""繁體中文翻譯"",
""definition"": ""英文定義(保持A1-A2程度)"",
""synonyms"": [""同義詞1"", ""同義詞2""],
""example"": ""例句(使用原文中的句子或生成新句子)"",
""example_translation"": ""例句中文翻譯"",
""difficulty_level"": ""CEFR等級(A1/A2/B1/B2/C1/C2)""
}
]
}
要求:
1. 選擇最有學習價值的詞彙
2. 定義要簡單易懂,適合英語學習者
3. 例句要實用且符合語境
4. 確保 JSON 格式正確
5. 同義詞最多2個選擇常用的";
private const string SmartExtractionPrompt = @"
分析以下英文文本,識別片語、俚語和常用表達,生成 {cardCount} 個學習卡片:
輸入文本:
{inputText}
重點關注:
1. 片語和俚語
2. 文化相關表達
3. 語境特定用法
4. 慣用語和搭配
請按照相同的 JSON 格式回應...";
private const string CardValidationPrompt = @"
請檢查以下詞卡內容的準確性:
單字: {word}
翻譯: {translation}
定義: {definition}
詞性: {partOfSpeech}
發音: {pronunciation}
例句: {example}
請按照以下 JSON 格式回應:
{
""issues"": [],
""suggestions"": [],
""overall_score"": 85,
""confidence"": 0.9
}";
}
// 支援類型
public class GeneratedCard
{
public string Word { get; set; } = string.Empty;
public string PartOfSpeech { get; set; } = string.Empty;
public string Pronunciation { get; set; } = string.Empty;
public string Translation { get; set; } = string.Empty;
public string Definition { get; set; } = string.Empty;
public List<string> Synonyms { get; set; } = new();
public string Example { get; set; } = string.Empty;
public string ExampleTranslation { get; set; } = string.Empty;
public string DifficultyLevel { get; set; } = string.Empty;
}
public class ValidationResult
{
public List<ValidationIssue> Issues { get; set; } = new();
public List<string> Suggestions { get; set; } = new();
public int OverallScore { get; set; }
public double Confidence { get; set; }
}
public class ValidationIssue
{
public string Field { get; set; } = string.Empty;
public string Original { get; set; } = string.Empty;
public string Corrected { get; set; } = string.Empty;
public string Reason { get; set; } = string.Empty;
public string Severity { get; set; } = string.Empty;
}