dramaling-vocab-learning/backend/DramaLing.Api/Services/AI/Gemini/GeminiClient.cs

148 lines
5.3 KiB
C#

using DramaLing.Api.Models.Configuration;
using Microsoft.Extensions.Options;
using System.Text.Json;
using System.Text;
namespace DramaLing.Api.Services.AI.Gemini;
/// <summary>
/// Gemini API HTTP 客戶端實作
/// </summary>
public class GeminiClient : IGeminiClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<GeminiClient> _logger;
private readonly GeminiOptions _options;
public GeminiClient(
HttpClient httpClient,
IOptions<GeminiOptions> options,
ILogger<GeminiClient> logger)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
_logger.LogInformation("GeminiClient initialized with model: {Model}, timeout: {Timeout}s",
_options.Model, _options.TimeoutSeconds);
_httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds);
_httpClient.DefaultRequestHeaders.Add("User-Agent", "DramaLing/1.0");
}
public async Task<string> 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.LogInformation("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;
}
}
public async Task<bool> TestConnectionAsync()
{
try
{
await CallGeminiAPIAsync("Test connection");
return true;
}
catch
{
return false;
}
}
private string ExtractTextFromResponse(string responseJson)
{
using var document = JsonDocument.Parse(responseJson);
var root = document.RootElement;
string aiText = string.Empty;
if (root.TryGetProperty("candidates", out var candidatesElement) &&
candidatesElement.ValueKind == JsonValueKind.Array)
{
_logger.LogInformation("Found candidates array with {Count} items",
candidatesElement.GetArrayLength());
var firstCandidate = candidatesElement.EnumerateArray().FirstOrDefault();
if (firstCandidate.ValueKind != JsonValueKind.Undefined)
{
if (firstCandidate.TryGetProperty("content", out var contentElement))
{
if (contentElement.TryGetProperty("parts", out var partsElement) &&
partsElement.ValueKind == JsonValueKind.Array)
{
var firstPart = partsElement.EnumerateArray().FirstOrDefault();
if (firstPart.TryGetProperty("text", out var textElement))
{
aiText = textElement.GetString() ?? string.Empty;
_logger.LogInformation("Successfully extracted text: {Length} characters",
aiText.Length);
}
}
}
}
}
// 檢查是否有安全過濾
if (root.TryGetProperty("promptFeedback", out var feedbackElement))
{
_logger.LogWarning("Gemini prompt feedback received: {Feedback}",
feedbackElement.ToString());
}
_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 (string.IsNullOrWhiteSpace(aiText) && root.TryGetProperty("promptFeedback", out _))
{
return "The content analysis is temporarily unavailable due to safety filtering. Please try with different content.";
}
return aiText;
}
}