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 _logger; private readonly IHttpClientFactory _httpClientFactory; private readonly ApiKeys _apiKeys; private readonly GeminiSettings _geminiSettings; private readonly string _apiKey; // Внедряем зависимости public GoogleGeminiSummarizationService( ILogger logger, IHttpClientFactory httpClientFactory, IOptions apiKeysOptions, IOptions geminiSettingsOptions) { _logger = logger; _httpClientFactory = httpClientFactory; _apiKeys = apiKeysOptions.Value; _geminiSettings = geminiSettingsOptions.Value; _apiKey = _apiKeys.Gemini; // Сохраняем ключ для удобства } public async Task 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 { new ContentDto { Parts = new List { 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(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; } } } }