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. Выделить 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;
|
||
}
|
||
}
|
||
}
|
||
} |