daily_digest/Services/GoogleGeminiSummarizationService.cs

164 lines
9.2 KiB
C#
Raw Normal View History

using DailyDigestWorker.Configuration;
using DailyDigestWorker.Models.Gemini; // Наши DTO
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System.Text; // Для StringBuilder и Encoding
namespace DailyDigestWorker.Services
{
public class GoogleGeminiSummarizationService : ISummarizationService
{
private readonly ILogger<GoogleGeminiSummarizationService> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ApiKeys _apiKeys;
private readonly GeminiSettings _geminiSettings;
private readonly string _apiKey;
// Внедряем зависимости
public GoogleGeminiSummarizationService(
ILogger<GoogleGeminiSummarizationService> logger,
IHttpClientFactory httpClientFactory,
IOptions<ApiKeys> apiKeysOptions,
IOptions<GeminiSettings> geminiSettingsOptions)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_apiKeys = apiKeysOptions.Value;
_geminiSettings = geminiSettingsOptions.Value;
_apiKey = _apiKeys.Gemini; // Сохраняем ключ для удобства
}
public async Task<string?> SummarizeTextAsync(string textToSummarize, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(_apiKey))
{
_logger.LogError("Ключ API для Google Gemini не сконфигурирован.");
return null;
}
if (string.IsNullOrWhiteSpace(textToSummarize))
{
_logger.LogWarning("Получен пустой текст для суммаризации.");
return null; // Нечего суммаризировать
}
// Формируем URL
string requestUrl = string.Format(_geminiSettings.ApiEndpointFormat, _geminiSettings.ModelName, _apiKey);
_logger.LogInformation("Запрос саммари к Google Gemini API (Модель: {Model})...", _geminiSettings.ModelName);
// Формируем УЛУЧШЕННЫЙ промпт
var prompt = new StringBuilder();
prompt.AppendLine("Ты - редактор, готовящий краткую новостную сводку по городу Новокузнецку для Telegram-дайджеста.")
.AppendLine("Проанализируй следующие тексты новостей, появившихся за последние 24 часа:")
.AppendLine("--- ТЕКСТЫ НОВОСТЕЙ НАЧАЛО ---")
.AppendLine(textToSummarize)
.AppendLine("--- ТЕКСТЫ НОВОСТЕЙ КОНЕЦ ---")
.AppendLine("\nТвоя задача:")
.AppendLine("1. Выделить 10 САМЫХ ВАЖНЫХ и интересных событий.")
.AppendLine("2. Для каждого события сформулировать ОЧЕНЬ КРАТКОЕ описание (1-2 предложения максимум).")
.AppendLine("3. Представить результат в виде НЕНУМЕРОВАННОГО списка, используя эмоджи в качестве маркера для каждого пункта.")
.AppendLine("4. Использовать нейтральный и информативный тон. Без эмоций, оценок, приветствий и заключений.")
.AppendLine("5. Не добавляй заголовок к списку новостей, только сам список.")
.AppendLine("\nПример желаемого формата вывода:")
.AppendLine("- Краткое описание первого события.")
.AppendLine("- Краткое описание второго события.")
.AppendLine("\nСгенерируй сводку:")
;
// Создаем тело запроса с помощью DTO
var requestDto = new GeminiRequestDto
{
Contents = new List<ContentDto>
{
new ContentDto { Parts = new List<PartDto> { new PartDto { Text = prompt.ToString() } } }
},
GenerationConfig = new GenerationConfigDto
{
Temperature = _geminiSettings.Temperature,
MaxOutputTokens = _geminiSettings.MaxOutputTokens
}
// Можно добавить SafetySettings, если нужно
};
// Сериализуем DTO в JSON
string jsonPayload = JsonConvert.SerializeObject(requestDto);
_logger.LogDebug("Тело запроса Gemini JSON: {Payload}", jsonPayload); // Осторожно, может быть большим
var client = _httpClientFactory.CreateClient();
try
{
// Создаем и отправляем POST-запрос
using var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
request.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.SendAsync(request, cancellationToken);
string responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
if (!response.IsSuccessStatusCode)
{
_logger.LogError("Ошибка при запросе к Gemini API. Статус: {StatusCode}. Ответ: {ResponseBody}", response.StatusCode, responseBody);
return null;
}
_logger.LogDebug("Ответ от Gemini API: {ResponseBody}", responseBody);
// Десериализуем ответ
var responseDto = JsonConvert.DeserializeObject<GeminiResponseDto>(responseBody);
// Проверяем наличие кандидатов и текста
var candidate = responseDto?.Candidates?.FirstOrDefault();
var generatedText = candidate?.Content?.Parts?.FirstOrDefault()?.Text;
// Проверяем причину завершения (на случай блокировки по безопасности)
if (candidate?.FinishReason != null && candidate.FinishReason != "STOP")
{
_logger.LogWarning("Генерация Gemini завершилась с причиной: {FinishReason}", candidate.FinishReason);
// Дополнительно логируем safety ratings, если они есть
if (candidate.SafetyRatings != null && candidate.SafetyRatings.Any(sr => sr.Blocked ?? false))
{
_logger.LogWarning("Генерация заблокирована по безопасности: {@SafetyRatings}", candidate.SafetyRatings);
}
if (responseDto?.PromptFeedback?.BlockReason != null)
{
_logger.LogWarning("Промпт заблокирован по безопасности: {BlockReason}", responseDto.PromptFeedback.BlockReason);
}
// В зависимости от причины, можно вернуть null или специальное сообщение
// return $"Не удалось сгенерировать саммари (причина: {candidate.FinishReason})";
return null; // Возвращаем null, если генерация не завершилась нормально
}
if (string.IsNullOrWhiteSpace(generatedText))
{
_logger.LogWarning("Gemini API вернул пустой результат или не удалось извлечь текст.");
return null;
}
_logger.LogInformation("Саммари успешно получено от Gemini API.");
return generatedText.Trim(); // Возвращаем полученный текст, убрав лишние пробелы
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Ошибка HTTP при запросе к Gemini API.");
return null;
}
catch (JsonException ex)
{
_logger.LogError(ex, "Ошибка парсинга JSON ответа Gemini API.");
return null;
}
catch (OperationCanceledException)
{
_logger.LogInformation("Запрос саммари был отменен.");
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Неожиданная ошибка при получении саммари от Gemini API.");
return null;
}
}
}
}