164 lines
9.2 KiB
C#
164 lines
9.2 KiB
C#
![]() |
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. Выделить 2-4 САМЫХ ВАЖНЫХ и интересных события.")
|
|||
|
.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;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|