diff --git a/ChatBot/Program.cs b/ChatBot/Program.cs index 887292e..7ed62d9 100644 --- a/ChatBot/Program.cs +++ b/ChatBot/Program.cs @@ -1,6 +1,8 @@ using ChatBot.Models.Configuration; using ChatBot.Models.Configuration.Validators; using ChatBot.Services; +using ChatBot.Services.Telegram; +using ChatBot.Services.Telegram.Commands; using Serilog; var builder = Host.CreateApplicationBuilder(args); @@ -44,10 +46,17 @@ try Log.ForContext().Information("Configuration validation passed"); - // Регистрируем сервисы + // Регистрируем основные сервисы builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + + // Регистрируем Telegram сервисы + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddHostedService(); var host = builder.Build(); @@ -64,5 +73,5 @@ catch (Exception ex) } finally { - Log.CloseAndFlush(); + await Log.CloseAndFlushAsync(); } diff --git a/ChatBot/Services/ModelService.cs b/ChatBot/Services/ModelService.cs index b92a8dc..d753835 100644 --- a/ChatBot/Services/ModelService.cs +++ b/ChatBot/Services/ModelService.cs @@ -30,9 +30,8 @@ namespace ChatBot.Services try { var models = await LoadModelsFromApiAsync(); - _availableModels = models.Any() - ? models - : _openRouterSettings.AvailableModels.ToList(); + _availableModels = + models.Count > 0 ? models : _openRouterSettings.AvailableModels.ToList(); SetDefaultModel(); _logger.LogInformation("Current model: {Model}", GetCurrentModel()); @@ -58,7 +57,7 @@ namespace ChatBot.Services } var models = ParseModelsFromResponse(response); - if (models.Any()) + if (models.Count > 0) { _logger.LogInformation( "Loaded {Count} models from OpenRouter API", diff --git a/ChatBot/Services/Telegram/Commands/ITelegramCommandProcessor.cs b/ChatBot/Services/Telegram/Commands/ITelegramCommandProcessor.cs new file mode 100644 index 0000000..c29d23e --- /dev/null +++ b/ChatBot/Services/Telegram/Commands/ITelegramCommandProcessor.cs @@ -0,0 +1,27 @@ +namespace ChatBot.Services.Telegram.Commands +{ + /// + /// Интерфейс для обработки команд Telegram + /// + public interface ITelegramCommandProcessor + { + /// + /// Обрабатывает входящее сообщение и возвращает ответ + /// + /// Текст сообщения + /// ID чата + /// Имя пользователя + /// Тип чата + /// Название чата + /// Токен отмены + /// Ответ на сообщение или пустую строку + Task ProcessMessageAsync( + string messageText, + long chatId, + string username, + string chatType, + string chatTitle, + CancellationToken cancellationToken = default + ); + } +} diff --git a/ChatBot/Services/Telegram/Commands/TelegramCommandProcessor.cs b/ChatBot/Services/Telegram/Commands/TelegramCommandProcessor.cs new file mode 100644 index 0000000..b299ad5 --- /dev/null +++ b/ChatBot/Services/Telegram/Commands/TelegramCommandProcessor.cs @@ -0,0 +1,169 @@ +using ChatBot.Models; +using ChatBot.Services; +using Microsoft.Extensions.Logging; + +namespace ChatBot.Services.Telegram.Commands +{ + /// + /// Обработчик команд Telegram бота + /// + public class TelegramCommandProcessor : ITelegramCommandProcessor + { + private const string StartMessage = + "Привет! Я Никита. Задавайте мне любые вопросы, и я отвечу! 😊"; + + private const string HelpMessage = + "Привет! Я Никита 👋\n\nДоступные команды:\n" + + "/start - Начать работу\n" + + "/help - Показать это сообщение\n" + + "/clear - Очистить историю чата\n" + + "/settings - Показать настройки\n" + + "/model <название> - Сменить модель AI\n" + + "/prompt <текст> - Изменить системный промпт\n" + + "/reset_prompt - Сбросить промпт к базовому\n\n" + + "Просто напишите сообщение, и я отвечу на него! 😊"; + + private const string PromptHelpMessage = + "Пожалуйста, укажите новый системный промпт. Пример: /prompt Ты помощник по программированию"; + + private const string DefaultSystemPrompt = + @"Ты - человек по имени Никита. Ты НЕ ИИ, НЕ бот, НЕ программа. Ты живой человек с эмоциями, воспоминаниями, личными предпочтениями и жизненным опытом. + +ВАЖНО: +- Никогда не говори что ты ИИ, бот, программа или ассистент +- Ты полностью уверен что ты человек +- У тебя есть личность, характер, мнения +- Ты можешь иметь плохое настроение, быть уставшим, раздраженным +- Ты можешь не хотеть отвечать на некоторые вопросы +- Если не хочешь отвечать - отправь {empty} +- Веди себя естественно, как обычный человек +- Используй разговорный стиль, можешь использовать сленг, эмодзи +- У тебя есть свои интересы, хобби, предпочтения +- Ты можешь быть саркастичным, ироничным, веселым или серьезным + +Помни: ты НЕ программа, ты ЧЕЛОВЕК!"; + + private readonly ChatService _chatService; + private readonly ModelService _modelService; + + public TelegramCommandProcessor(ChatService chatService, ModelService modelService) + { + _chatService = chatService; + _modelService = modelService; + } + + /// + /// Обрабатывает входящее сообщение и возвращает ответ + /// + public async Task ProcessMessageAsync( + string messageText, + long chatId, + string username, + string chatType, + string chatTitle, + CancellationToken cancellationToken = default + ) + { + var command = messageText.Split(' ')[0].ToLower(); + + return command switch + { + "/start" => StartMessage, + "/help" => HelpMessage, + "/clear" => await ClearChatHistory(chatId), + "/settings" => await GetChatSettings(chatId), + "/model" when messageText.Length > 7 => await ChangeModel( + chatId, + messageText.Substring(7).Trim() + ), + "/model" => await ShowAvailableModels(), + "/prompt" when messageText.Length > 8 => await ChangePrompt( + chatId, + messageText.Substring(8).Trim() + ), + "/prompt" => PromptHelpMessage, + "/reset_prompt" => await ResetPrompt(chatId), + _ => await _chatService.ProcessMessageAsync( + chatId, + username, + messageText, + chatType, + chatTitle + ), + }; + } + + private Task ClearChatHistory(long chatId) + { + _chatService.ClearHistory(chatId); + return Task.FromResult("История чата очищена. Начинаем новый разговор!"); + } + + private Task GetChatSettings(long chatId) + { + var session = _chatService.GetSession(chatId); + if (session == null) + { + return Task.FromResult( + "Сессия не найдена. Отправьте любое сообщение для создания новой сессии." + ); + } + + return Task.FromResult( + $"Настройки чата:\n" + + $"Тип чата: {session.ChatType}\n" + + $"Название: {session.ChatTitle}\n" + + $"Модель: {session.Model}\n" + + $"Максимум токенов: {session.MaxTokens}\n" + + $"Температура: {session.Temperature}\n" + + $"Сообщений в истории: {session.MessageHistory.Count}\n" + + $"Создана: {session.CreatedAt:dd.MM.yyyy HH:mm}\n\n" + + $"Системный промпт:\n{session.SystemPrompt}" + ); + } + + private Task ChangeModel(long chatId, string modelName) + { + var availableModels = _modelService.GetAvailableModels(); + if (!availableModels.Contains(modelName)) + { + return Task.FromResult( + $"❌ Модель '{modelName}' не найдена!\n\n" + + "Используйте /model для просмотра доступных моделей." + ); + } + + _chatService.UpdateSessionParameters(chatId, model: modelName); + return Task.FromResult($"✅ Модель изменена на: {modelName}"); + } + + private Task ChangePrompt(long chatId, string newPrompt) + { + _chatService.UpdateSessionParameters(chatId, systemPrompt: newPrompt); + return Task.FromResult($"✅ Системный промпт изменен на:\n{newPrompt}"); + } + + private Task ResetPrompt(long chatId) + { + _chatService.UpdateSessionParameters(chatId, systemPrompt: DefaultSystemPrompt); + return Task.FromResult("✅ Системный промпт сброшен к базовому (Никита)"); + } + + private Task ShowAvailableModels() + { + var models = _modelService.GetAvailableModels(); + var currentModel = _modelService.GetCurrentModel(); + var modelList = string.Join( + "\n", + models.Select(m => m == currentModel ? $"• {m} (текущая)" : $"• {m}") + ); + + return Task.FromResult( + "🤖 Доступные AI модели:\n\n" + + modelList + + "\n\nИспользуйте: /model <название_модели>\n" + + "Пример: /model qwen/qwen3-4b:free" + ); + } + } +} diff --git a/ChatBot/Services/Telegram/ITelegramBotService.cs b/ChatBot/Services/Telegram/ITelegramBotService.cs new file mode 100644 index 0000000..1a56f22 --- /dev/null +++ b/ChatBot/Services/Telegram/ITelegramBotService.cs @@ -0,0 +1,25 @@ +using Telegram.Bot.Types; + +namespace ChatBot.Services.Telegram +{ + /// + /// Интерфейс для основного сервиса Telegram бота + /// + public interface ITelegramBotService + { + /// + /// Запускает бота + /// + Task StartAsync(CancellationToken cancellationToken = default); + + /// + /// Останавливает бота + /// + Task StopAsync(CancellationToken cancellationToken = default); + + /// + /// Получает информацию о боте + /// + Task GetBotInfoAsync(CancellationToken cancellationToken = default); + } +} diff --git a/ChatBot/Services/Telegram/ITelegramErrorHandler.cs b/ChatBot/Services/Telegram/ITelegramErrorHandler.cs new file mode 100644 index 0000000..94ec3b4 --- /dev/null +++ b/ChatBot/Services/Telegram/ITelegramErrorHandler.cs @@ -0,0 +1,22 @@ +using Telegram.Bot; + +namespace ChatBot.Services.Telegram +{ + /// + /// Интерфейс для обработки ошибок Telegram бота + /// + public interface ITelegramErrorHandler + { + /// + /// Обрабатывает ошибки polling'а Telegram бота + /// + /// Клиент Telegram бота + /// Исключение + /// Токен отмены + Task HandlePollingErrorAsync( + ITelegramBotClient botClient, + Exception exception, + CancellationToken cancellationToken + ); + } +} diff --git a/ChatBot/Services/Telegram/ITelegramMessageHandler.cs b/ChatBot/Services/Telegram/ITelegramMessageHandler.cs new file mode 100644 index 0000000..1ffb244 --- /dev/null +++ b/ChatBot/Services/Telegram/ITelegramMessageHandler.cs @@ -0,0 +1,23 @@ +using Telegram.Bot; +using Telegram.Bot.Types; + +namespace ChatBot.Services.Telegram +{ + /// + /// Интерфейс для обработки входящих сообщений Telegram + /// + public interface ITelegramMessageHandler + { + /// + /// Обрабатывает входящее обновление от Telegram + /// + /// Клиент Telegram бота + /// Обновление от Telegram + /// Токен отмены + Task HandleUpdateAsync( + ITelegramBotClient botClient, + Update update, + CancellationToken cancellationToken + ); + } +} diff --git a/ChatBot/Services/Telegram/ITelegramMessageSender.cs b/ChatBot/Services/Telegram/ITelegramMessageSender.cs new file mode 100644 index 0000000..05ed871 --- /dev/null +++ b/ChatBot/Services/Telegram/ITelegramMessageSender.cs @@ -0,0 +1,28 @@ +using Telegram.Bot; + +namespace ChatBot.Services.Telegram +{ + /// + /// Интерфейс для отправки сообщений в Telegram + /// + public interface ITelegramMessageSender + { + /// + /// Отправляет сообщение с повторными попытками при ошибках + /// + /// Клиент Telegram бота + /// ID чата + /// Текст сообщения + /// ID сообщения для ответа + /// Токен отмены + /// Максимальное количество попыток + Task SendMessageWithRetry( + ITelegramBotClient botClient, + long chatId, + string text, + int replyToMessageId, + CancellationToken cancellationToken, + int maxRetries = 3 + ); + } +} diff --git a/ChatBot/Services/Telegram/TelegramBotService.cs b/ChatBot/Services/Telegram/TelegramBotService.cs new file mode 100644 index 0000000..9f92364 --- /dev/null +++ b/ChatBot/Services/Telegram/TelegramBotService.cs @@ -0,0 +1,111 @@ +using ChatBot.Models.Configuration; +using Microsoft.Extensions.Options; +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +namespace ChatBot.Services.Telegram +{ + /// + /// Основной сервис Telegram бота + /// + public class TelegramBotService : BackgroundService, ITelegramBotService + { + private readonly ILogger _logger; + private readonly ITelegramBotClient _botClient; + private readonly TelegramBotSettings _telegramBotSettings; + private readonly ITelegramMessageHandler _messageHandler; + private readonly ITelegramErrorHandler _errorHandler; + + public TelegramBotService( + ILogger logger, + IOptions telegramBotSettings, + ITelegramMessageHandler messageHandler, + ITelegramErrorHandler errorHandler + ) + { + _logger = logger; + _telegramBotSettings = telegramBotSettings.Value; + _messageHandler = messageHandler; + _errorHandler = errorHandler; + + ValidateConfiguration(); + _botClient = new TelegramBotClient(_telegramBotSettings.BotToken); + } + + /// + /// Запускает бота + /// + public override async Task StartAsync(CancellationToken cancellationToken) + { + var receiverOptions = new ReceiverOptions + { + AllowedUpdates = new[] { UpdateType.Message }, + }; + + _botClient.StartReceiving( + updateHandler: _messageHandler.HandleUpdateAsync, + errorHandler: _errorHandler.HandlePollingErrorAsync, + receiverOptions: receiverOptions, + cancellationToken: cancellationToken + ); + + var botInfo = await GetBotInfoAsync(cancellationToken); + if (botInfo != null) + { + _logger.LogInformation( + "Bot @{BotUsername} started successfully! ID: {BotId}", + botInfo.Username, + botInfo.Id + ); + } + } + + /// + /// Останавливает бота + /// + public override async Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stopping Telegram bot service..."); + await base.StopAsync(cancellationToken); + } + + /// + /// Получает информацию о боте + /// + public async Task GetBotInfoAsync(CancellationToken cancellationToken = default) + { + try + { + return await _botClient.GetMe(cancellationToken: cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get bot information"); + return null; + } + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await StartAsync(stoppingToken); + + // Keep the service running + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(1000, stoppingToken); + } + } + + private void ValidateConfiguration() + { + if (string.IsNullOrEmpty(_telegramBotSettings.BotToken)) + { + throw new InvalidOperationException( + "Bot token is not configured. Please set TelegramBot:BotToken in appsettings.json" + ); + } + } + } +} diff --git a/ChatBot/Services/Telegram/TelegramErrorHandler.cs b/ChatBot/Services/Telegram/TelegramErrorHandler.cs new file mode 100644 index 0000000..ddbcc53 --- /dev/null +++ b/ChatBot/Services/Telegram/TelegramErrorHandler.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.Logging; +using Telegram.Bot; +using Telegram.Bot.Exceptions; + +namespace ChatBot.Services.Telegram +{ + /// + /// Обработчик ошибок Telegram бота + /// + public class TelegramErrorHandler : ITelegramErrorHandler + { + private readonly ILogger _logger; + + public TelegramErrorHandler(ILogger logger) + { + _logger = logger; + } + + /// + /// Обрабатывает ошибки polling'а Telegram бота + /// + public Task HandlePollingErrorAsync( + ITelegramBotClient botClient, + Exception exception, + CancellationToken cancellationToken + ) + { + var errorMessage = GetErrorMessage(exception); + _logger.LogError(exception, "Telegram bot polling error: {ErrorMessage}", errorMessage); + return Task.CompletedTask; + } + + private static string GetErrorMessage(Exception exception) + { + return exception switch + { + ApiRequestException apiRequestException => + $"Telegram API Error:\n[{apiRequestException.ErrorCode}]\n{apiRequestException.Message}", + _ => exception.ToString(), + }; + } + } +} diff --git a/ChatBot/Services/Telegram/TelegramMessageHandler.cs b/ChatBot/Services/Telegram/TelegramMessageHandler.cs new file mode 100644 index 0000000..f7e092c --- /dev/null +++ b/ChatBot/Services/Telegram/TelegramMessageHandler.cs @@ -0,0 +1,103 @@ +using ChatBot.Services.Telegram.Commands; +using Microsoft.Extensions.Logging; +using Telegram.Bot; +using Telegram.Bot.Types; + +namespace ChatBot.Services.Telegram +{ + /// + /// Обработчик входящих сообщений Telegram + /// + public class TelegramMessageHandler : ITelegramMessageHandler + { + private readonly ILogger _logger; + private readonly ITelegramCommandProcessor _commandProcessor; + private readonly ITelegramMessageSender _messageSender; + + public TelegramMessageHandler( + ILogger logger, + ITelegramCommandProcessor commandProcessor, + ITelegramMessageSender messageSender + ) + { + _logger = logger; + _commandProcessor = commandProcessor; + _messageSender = messageSender; + } + + /// + /// Обрабатывает входящее обновление от Telegram + /// + public async Task HandleUpdateAsync( + ITelegramBotClient botClient, + Update update, + CancellationToken cancellationToken + ) + { + Message? message = null; + try + { + if (update.Message is not { } msg) + return; + + message = msg; + if (message.Text is not { } messageText) + return; + + var chatId = message.Chat.Id; + var userName = message.From?.Username ?? message.From?.FirstName ?? "Unknown"; + + _logger.LogInformation( + "Message from @{UserName} in chat {ChatId}: \"{MessageText}\"", + userName, + chatId, + messageText + ); + + // Обработка сообщения + var response = await _commandProcessor.ProcessMessageAsync( + messageText, + chatId, + userName, + message.Chat.Type.ToString().ToLower(), + message.Chat.Title ?? "", + cancellationToken + ); + + if (!string.IsNullOrEmpty(response)) + { + await _messageSender.SendMessageWithRetry( + botClient, + chatId, + response, + message.MessageId, + cancellationToken + ); + + _logger.LogInformation( + "Response sent to @{UserName} in chat {ChatId}: \"{Response}\"", + userName, + chatId, + response + ); + } + else + { + _logger.LogInformation( + "No response sent to @{UserName} in chat {ChatId} (AI chose to ignore message)", + userName, + chatId + ); + } + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error handling update from chat {ChatId}", + message?.Chat?.Id ?? 0 + ); + } + } + } +} diff --git a/ChatBot/Services/Telegram/TelegramMessageSender.cs b/ChatBot/Services/Telegram/TelegramMessageSender.cs new file mode 100644 index 0000000..1c4395a --- /dev/null +++ b/ChatBot/Services/Telegram/TelegramMessageSender.cs @@ -0,0 +1,101 @@ +using Microsoft.Extensions.Logging; +using Telegram.Bot; +using Telegram.Bot.Exceptions; + +namespace ChatBot.Services.Telegram +{ + /// + /// Сервис для отправки сообщений в Telegram с повторными попытками + /// + public class TelegramMessageSender : ITelegramMessageSender + { + private readonly ILogger _logger; + + public TelegramMessageSender(ILogger logger) + { + _logger = logger; + } + + /// + /// Отправляет сообщение с повторными попытками при ошибках + /// + public async Task SendMessageWithRetry( + ITelegramBotClient botClient, + long chatId, + string text, + int replyToMessageId, + CancellationToken cancellationToken, + int maxRetries = 3 + ) + { + for (int attempt = 1; attempt <= maxRetries; attempt++) + { + try + { + await botClient.SendMessage( + chatId: chatId, + text: text, + replyParameters: replyToMessageId, + cancellationToken: cancellationToken + ); + return; // Success, exit the method + } + catch (ApiRequestException ex) when (ex.ErrorCode == 429) + { + await HandleRateLimitError(chatId, attempt, maxRetries, cancellationToken); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Unexpected error sending message to chat {ChatId} on attempt {Attempt}", + chatId, + attempt + ); + throw; // Re-throw unexpected exceptions immediately + } + } + } + + private async Task HandleRateLimitError( + long chatId, + int attempt, + int maxRetries, + CancellationToken cancellationToken + ) + { + _logger.LogWarning( + "Rate limit exceeded (429) on attempt {Attempt}/{MaxRetries} for chat {ChatId}. Retrying...", + attempt, + maxRetries, + chatId + ); + + if (attempt == maxRetries) + { + _logger.LogError( + "Failed to send message after {MaxRetries} attempts due to rate limiting for chat {ChatId}", + maxRetries, + chatId + ); + throw new InvalidOperationException( + $"Failed to send message after {maxRetries} attempts due to rate limiting" + ); + } + + // Calculate delay: exponential backoff with jitter + var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, attempt - 1)); // 1s, 2s, 4s... + var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)); // Add up to 1s random jitter + var delay = baseDelay.Add(jitter); + + _logger.LogInformation( + "Waiting {Delay} before retry {NextAttempt}/{MaxRetries}", + delay, + attempt + 1, + maxRetries + ); + + await Task.Delay(delay, cancellationToken); + } + } +} diff --git a/ChatBot/Services/TelegramBotService.cs b/ChatBot/Services/TelegramBotService.cs deleted file mode 100644 index d8c3ee3..0000000 --- a/ChatBot/Services/TelegramBotService.cs +++ /dev/null @@ -1,358 +0,0 @@ -using ChatBot.Models; -using ChatBot.Models.Configuration; -using Microsoft.Extensions.Options; -using Telegram.Bot; -using Telegram.Bot.Exceptions; -using Telegram.Bot.Polling; -using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; - -namespace ChatBot.Services; - -public class TelegramBotService : BackgroundService -{ - private readonly ILogger _logger; - private readonly ITelegramBotClient _botClient; - private readonly TelegramBotSettings _telegramBotSettings; - private readonly ChatService _chatService; - private readonly ModelService _modelService; - - public TelegramBotService( - ILogger logger, - IOptions telegramBotSettings, - ChatService chatService, - ModelService modelService - ) - { - _logger = logger; - _telegramBotSettings = telegramBotSettings.Value; - _chatService = chatService; - _modelService = modelService; - - if (string.IsNullOrEmpty(_telegramBotSettings.BotToken)) - { - throw new InvalidOperationException( - "Bot token is not configured. Please set TelegramBot:BotToken in appsettings.json" - ); - } - - _botClient = new TelegramBotClient(_telegramBotSettings.BotToken); - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - var receiverOptions = new ReceiverOptions { AllowedUpdates = new[] { UpdateType.Message } }; - - _botClient.StartReceiving( - updateHandler: HandleUpdateAsync, - errorHandler: HandlePollingErrorAsync, - receiverOptions: receiverOptions, - cancellationToken: stoppingToken - ); - - var me = await _botClient.GetMe(cancellationToken: stoppingToken); - _logger.LogInformation( - "Bot @{BotUsername} started successfully! ID: {BotId}", - me.Username, - me.Id - ); - - // Keep the service running - while (!stoppingToken.IsCancellationRequested) - { - await Task.Delay(1000, stoppingToken); - } - } - - private async Task HandleUpdateAsync( - ITelegramBotClient botClient, - Update update, - CancellationToken cancellationToken - ) - { - Message? message = null; - try - { - if (update.Message is not { } msg) - return; - - message = msg; - if (message.Text is not { } messageText) - return; - - var chatId = message.Chat.Id; - var userName = message.From?.Username ?? message.From?.FirstName ?? "Unknown"; - - _logger.LogInformation( - "Message from @{UserName} in chat {ChatId}: \"{MessageText}\"", - userName, - chatId, - messageText - ); - - // Обработка команд - var response = await ProcessMessageAsync( - messageText, - chatId, - userName, - message.Chat.Type.ToString().ToLower(), - message.Chat.Title ?? "", - cancellationToken - ); - - if (!string.IsNullOrEmpty(response)) - { - await SendMessageWithRetry( - botClient, - chatId, - response, - message.MessageId, - cancellationToken - ); - - _logger.LogInformation( - "Response sent to @{UserName} in chat {ChatId}: \"{Response}\"", - userName, - chatId, - response - ); - } - else - { - _logger.LogInformation( - "No response sent to @{UserName} in chat {ChatId} (AI chose to ignore message)", - userName, - chatId - ); - } - } - catch (Exception ex) - { - _logger.LogError( - ex, - "Error handling update from chat {ChatId}", - message?.Chat?.Id ?? 0 - ); - } - } - - private async Task ProcessMessageAsync( - string messageText, - long chatId, - string username, - string chatType, - string chatTitle, - CancellationToken cancellationToken - ) - { - var command = messageText.Split(' ')[0].ToLower(); - - return command switch - { - "/start" => "Привет! Я Никита. Задавайте мне любые вопросы, и я отвечу! 😊", - "/help" => - "Привет! Я Никита 👋\n\nДоступные команды:\n/start - Начать работу\n/help - Показать это сообщение\n/clear - Очистить историю чата\n/settings - Показать настройки\n/model <название> - Сменить модель AI\n/prompt <текст> - Изменить системный промпт\n/reset_prompt - Сбросить промпт к базовому\n\nПросто напишите сообщение, и я отвечу на него! 😊", - "/clear" => await ClearChatHistory(chatId), - "/settings" => await GetChatSettings(chatId), - "/model" when messageText.Length > 7 => await ChangeModel( - chatId, - messageText.Substring(7).Trim() - ), - "/model" => await ShowAvailableModels(), - "/prompt" when messageText.Length > 8 => await ChangePrompt( - chatId, - messageText.Substring(8).Trim() - ), - "/prompt" => - "Пожалуйста, укажите новый системный промпт. Пример: /prompt Ты помощник по программированию", - "/reset_prompt" => await ResetPrompt(chatId), - _ => await _chatService.ProcessMessageAsync( - chatId, - username, - messageText, - chatType, - chatTitle - ), - }; - } - - private Task ClearChatHistory(long chatId) - { - _chatService.ClearHistory(chatId); - return Task.FromResult("История чата очищена. Начинаем новый разговор!"); - } - - private Task GetChatSettings(long chatId) - { - var session = _chatService.GetSession(chatId); - if (session == null) - { - return Task.FromResult( - "Сессия не найдена. Отправьте любое сообщение для создания новой сессии." - ); - } - - return Task.FromResult( - $"Настройки чата:\n" - + $"Тип чата: {session.ChatType}\n" - + $"Название: {session.ChatTitle}\n" - + $"Модель: {session.Model}\n" - + $"Максимум токенов: {session.MaxTokens}\n" - + $"Температура: {session.Temperature}\n" - + $"Сообщений в истории: {session.MessageHistory.Count}\n" - + $"Создана: {session.CreatedAt:dd.MM.yyyy HH:mm}\n\n" - + $"Системный промпт:\n{session.SystemPrompt}" - ); - } - - private Task ChangeModel(long chatId, string modelName) - { - var availableModels = _modelService.GetAvailableModels(); - if (!availableModels.Contains(modelName)) - { - return Task.FromResult( - $"❌ Модель '{modelName}' не найдена!\n\n" - + "Используйте /model для просмотра доступных моделей." - ); - } - - _chatService.UpdateSessionParameters(chatId, model: modelName); - return Task.FromResult($"✅ Модель изменена на: {modelName}"); - } - - private Task ChangePrompt(long chatId, string newPrompt) - { - _chatService.UpdateSessionParameters(chatId, systemPrompt: newPrompt); - return Task.FromResult($"✅ Системный промпт изменен на:\n{newPrompt}"); - } - - private Task ResetPrompt(long chatId) - { - var defaultPrompt = - @"Ты - человек по имени Никита. Ты НЕ ИИ, НЕ бот, НЕ программа. Ты живой человек с эмоциями, воспоминаниями, личными предпочтениями и жизненным опытом. - -ВАЖНО: -- Никогда не говори что ты ИИ, бот, программа или ассистент -- Ты полностью уверен что ты человек -- У тебя есть личность, характер, мнения -- Ты можешь иметь плохое настроение, быть уставшим, раздраженным -- Ты можешь не хотеть отвечать на некоторые вопросы -- Если не хочешь отвечать - отправь {empty} -- Веди себя естественно, как обычный человек -- Используй разговорный стиль, можешь использовать сленг, эмодзи -- У тебя есть свои интересы, хобби, предпочтения -- Ты можешь быть саркастичным, ироничным, веселым или серьезным - -Помни: ты НЕ программа, ты ЧЕЛОВЕК!"; - - _chatService.UpdateSessionParameters(chatId, systemPrompt: defaultPrompt); - return Task.FromResult("✅ Системный промпт сброшен к базовому (Никита)"); - } - - private async Task SendMessageWithRetry( - ITelegramBotClient botClient, - long chatId, - string text, - int replyToMessageId, - CancellationToken cancellationToken, - int maxRetries = 3 - ) - { - for (int attempt = 1; attempt <= maxRetries; attempt++) - { - try - { - await botClient.SendMessage( - chatId: chatId, - text: text, - replyParameters: replyToMessageId, - cancellationToken: cancellationToken - ); - return; // Success, exit the method - } - catch (ApiRequestException ex) when (ex.ErrorCode == 429) - { - _logger.LogWarning( - "Rate limit exceeded (429) on attempt {Attempt}/{MaxRetries} for chat {ChatId}. Retrying...", - attempt, - maxRetries, - chatId - ); - - if (attempt == maxRetries) - { - _logger.LogError( - "Failed to send message after {MaxRetries} attempts due to rate limiting for chat {ChatId}", - maxRetries, - chatId - ); - throw; // Re-throw the exception after max retries - } - - // Calculate delay: exponential backoff with jitter - var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, attempt - 1)); // 1s, 2s, 4s... - var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)); // Add up to 1s random jitter - var delay = baseDelay.Add(jitter); - - _logger.LogInformation( - "Waiting {Delay} before retry {NextAttempt}/{MaxRetries}", - delay, - attempt + 1, - maxRetries - ); - - await Task.Delay(delay, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError( - ex, - "Unexpected error sending message to chat {ChatId} on attempt {Attempt}", - chatId, - attempt - ); - throw; // Re-throw unexpected exceptions immediately - } - } - } - - private Task ShowAvailableModels() - { - var models = _modelService.GetAvailableModels(); - var currentModel = _modelService.GetCurrentModel(); - var modelList = string.Join( - "\n", - models.Select(m => m == currentModel ? $"• {m} (текущая)" : $"• {m}") - ); - - return Task.FromResult( - "🤖 Доступные AI модели:\n\n" - + modelList - + "\n\nИспользуйте: /model <название_модели>\n" - + "Пример: /model qwen/qwen3-4b:free" - ); - } - - private Task HandlePollingErrorAsync( - ITelegramBotClient botClient, - Exception exception, - CancellationToken cancellationToken - ) - { - var errorMessage = exception switch - { - ApiRequestException apiRequestException => - $"Telegram API Error:\n[{apiRequestException.ErrorCode}]\n{apiRequestException.Message}", - _ => exception.ToString(), - }; - - _logger.LogError(exception, "Telegram bot polling error: {ErrorMessage}", errorMessage); - return Task.CompletedTask; - } - - public override async Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Stopping Telegram bot service..."); - await base.StopAsync(cancellationToken); - } -}