148 lines
5.3 KiB
C#
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;
|
|
}
|
|
} |