This commit is contained in:
Leonid Pershin
2025-10-15 21:28:33 +03:00
parent bdf31cc73c
commit fd68fb4cba
13 changed files with 666 additions and 364 deletions

View File

@@ -1,6 +1,8 @@
using ChatBot.Models.Configuration; using ChatBot.Models.Configuration;
using ChatBot.Models.Configuration.Validators; using ChatBot.Models.Configuration.Validators;
using ChatBot.Services; using ChatBot.Services;
using ChatBot.Services.Telegram;
using ChatBot.Services.Telegram.Commands;
using Serilog; using Serilog;
var builder = Host.CreateApplicationBuilder(args); var builder = Host.CreateApplicationBuilder(args);
@@ -44,10 +46,17 @@ try
Log.ForContext<Program>().Information("Configuration validation passed"); Log.ForContext<Program>().Information("Configuration validation passed");
// Регистрируем сервисы // Регистрируем основные сервисы
builder.Services.AddSingleton<ModelService>(); builder.Services.AddSingleton<ModelService>();
builder.Services.AddSingleton<AIService>(); builder.Services.AddSingleton<AIService>();
builder.Services.AddSingleton<ChatService>(); builder.Services.AddSingleton<ChatService>();
// Регистрируем Telegram сервисы
builder.Services.AddSingleton<ITelegramMessageSender, TelegramMessageSender>();
builder.Services.AddSingleton<ITelegramErrorHandler, TelegramErrorHandler>();
builder.Services.AddSingleton<ITelegramCommandProcessor, TelegramCommandProcessor>();
builder.Services.AddSingleton<ITelegramMessageHandler, TelegramMessageHandler>();
builder.Services.AddSingleton<ITelegramBotService, TelegramBotService>();
builder.Services.AddHostedService<TelegramBotService>(); builder.Services.AddHostedService<TelegramBotService>();
var host = builder.Build(); var host = builder.Build();
@@ -64,5 +73,5 @@ catch (Exception ex)
} }
finally finally
{ {
Log.CloseAndFlush(); await Log.CloseAndFlushAsync();
} }

View File

@@ -30,9 +30,8 @@ namespace ChatBot.Services
try try
{ {
var models = await LoadModelsFromApiAsync(); var models = await LoadModelsFromApiAsync();
_availableModels = models.Any() _availableModels =
? models models.Count > 0 ? models : _openRouterSettings.AvailableModels.ToList();
: _openRouterSettings.AvailableModels.ToList();
SetDefaultModel(); SetDefaultModel();
_logger.LogInformation("Current model: {Model}", GetCurrentModel()); _logger.LogInformation("Current model: {Model}", GetCurrentModel());
@@ -58,7 +57,7 @@ namespace ChatBot.Services
} }
var models = ParseModelsFromResponse(response); var models = ParseModelsFromResponse(response);
if (models.Any()) if (models.Count > 0)
{ {
_logger.LogInformation( _logger.LogInformation(
"Loaded {Count} models from OpenRouter API", "Loaded {Count} models from OpenRouter API",

View File

@@ -0,0 +1,27 @@
namespace ChatBot.Services.Telegram.Commands
{
/// <summary>
/// Интерфейс для обработки команд Telegram
/// </summary>
public interface ITelegramCommandProcessor
{
/// <summary>
/// Обрабатывает входящее сообщение и возвращает ответ
/// </summary>
/// <param name="messageText">Текст сообщения</param>
/// <param name="chatId">ID чата</param>
/// <param name="username">Имя пользователя</param>
/// <param name="chatType">Тип чата</param>
/// <param name="chatTitle">Название чата</param>
/// <param name="cancellationToken">Токен отмены</param>
/// <returns>Ответ на сообщение или пустую строку</returns>
Task<string> ProcessMessageAsync(
string messageText,
long chatId,
string username,
string chatType,
string chatTitle,
CancellationToken cancellationToken = default
);
}
}

View File

@@ -0,0 +1,169 @@
using ChatBot.Models;
using ChatBot.Services;
using Microsoft.Extensions.Logging;
namespace ChatBot.Services.Telegram.Commands
{
/// <summary>
/// Обработчик команд Telegram бота
/// </summary>
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;
}
/// <summary>
/// Обрабатывает входящее сообщение и возвращает ответ
/// </summary>
public async Task<string> 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<string> ClearChatHistory(long chatId)
{
_chatService.ClearHistory(chatId);
return Task.FromResult("История чата очищена. Начинаем новый разговор!");
}
private Task<string> 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<string> 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<string> ChangePrompt(long chatId, string newPrompt)
{
_chatService.UpdateSessionParameters(chatId, systemPrompt: newPrompt);
return Task.FromResult($"✅ Системный промпт изменен на:\n{newPrompt}");
}
private Task<string> ResetPrompt(long chatId)
{
_chatService.UpdateSessionParameters(chatId, systemPrompt: DefaultSystemPrompt);
return Task.FromResult("✅ Системный промпт сброшен к базовому (Никита)");
}
private Task<string> 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"
);
}
}
}

View File

@@ -0,0 +1,25 @@
using Telegram.Bot.Types;
namespace ChatBot.Services.Telegram
{
/// <summary>
/// Интерфейс для основного сервиса Telegram бота
/// </summary>
public interface ITelegramBotService
{
/// <summary>
/// Запускает бота
/// </summary>
Task StartAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Останавливает бота
/// </summary>
Task StopAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Получает информацию о боте
/// </summary>
Task<User?> GetBotInfoAsync(CancellationToken cancellationToken = default);
}
}

View File

@@ -0,0 +1,22 @@
using Telegram.Bot;
namespace ChatBot.Services.Telegram
{
/// <summary>
/// Интерфейс для обработки ошибок Telegram бота
/// </summary>
public interface ITelegramErrorHandler
{
/// <summary>
/// Обрабатывает ошибки polling'а Telegram бота
/// </summary>
/// <param name="botClient">Клиент Telegram бота</param>
/// <param name="exception">Исключение</param>
/// <param name="cancellationToken">Токен отмены</param>
Task HandlePollingErrorAsync(
ITelegramBotClient botClient,
Exception exception,
CancellationToken cancellationToken
);
}
}

View File

@@ -0,0 +1,23 @@
using Telegram.Bot;
using Telegram.Bot.Types;
namespace ChatBot.Services.Telegram
{
/// <summary>
/// Интерфейс для обработки входящих сообщений Telegram
/// </summary>
public interface ITelegramMessageHandler
{
/// <summary>
/// Обрабатывает входящее обновление от Telegram
/// </summary>
/// <param name="botClient">Клиент Telegram бота</param>
/// <param name="update">Обновление от Telegram</param>
/// <param name="cancellationToken">Токен отмены</param>
Task HandleUpdateAsync(
ITelegramBotClient botClient,
Update update,
CancellationToken cancellationToken
);
}
}

View File

@@ -0,0 +1,28 @@
using Telegram.Bot;
namespace ChatBot.Services.Telegram
{
/// <summary>
/// Интерфейс для отправки сообщений в Telegram
/// </summary>
public interface ITelegramMessageSender
{
/// <summary>
/// Отправляет сообщение с повторными попытками при ошибках
/// </summary>
/// <param name="botClient">Клиент Telegram бота</param>
/// <param name="chatId">ID чата</param>
/// <param name="text">Текст сообщения</param>
/// <param name="replyToMessageId">ID сообщения для ответа</param>
/// <param name="cancellationToken">Токен отмены</param>
/// <param name="maxRetries">Максимальное количество попыток</param>
Task SendMessageWithRetry(
ITelegramBotClient botClient,
long chatId,
string text,
int replyToMessageId,
CancellationToken cancellationToken,
int maxRetries = 3
);
}
}

View File

@@ -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
{
/// <summary>
/// Основной сервис Telegram бота
/// </summary>
public class TelegramBotService : BackgroundService, ITelegramBotService
{
private readonly ILogger<TelegramBotService> _logger;
private readonly ITelegramBotClient _botClient;
private readonly TelegramBotSettings _telegramBotSettings;
private readonly ITelegramMessageHandler _messageHandler;
private readonly ITelegramErrorHandler _errorHandler;
public TelegramBotService(
ILogger<TelegramBotService> logger,
IOptions<TelegramBotSettings> telegramBotSettings,
ITelegramMessageHandler messageHandler,
ITelegramErrorHandler errorHandler
)
{
_logger = logger;
_telegramBotSettings = telegramBotSettings.Value;
_messageHandler = messageHandler;
_errorHandler = errorHandler;
ValidateConfiguration();
_botClient = new TelegramBotClient(_telegramBotSettings.BotToken);
}
/// <summary>
/// Запускает бота
/// </summary>
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
);
}
}
/// <summary>
/// Останавливает бота
/// </summary>
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping Telegram bot service...");
await base.StopAsync(cancellationToken);
}
/// <summary>
/// Получает информацию о боте
/// </summary>
public async Task<User?> 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"
);
}
}
}
}

View File

@@ -0,0 +1,43 @@
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Exceptions;
namespace ChatBot.Services.Telegram
{
/// <summary>
/// Обработчик ошибок Telegram бота
/// </summary>
public class TelegramErrorHandler : ITelegramErrorHandler
{
private readonly ILogger<TelegramErrorHandler> _logger;
public TelegramErrorHandler(ILogger<TelegramErrorHandler> logger)
{
_logger = logger;
}
/// <summary>
/// Обрабатывает ошибки polling'а Telegram бота
/// </summary>
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(),
};
}
}
}

View File

@@ -0,0 +1,103 @@
using ChatBot.Services.Telegram.Commands;
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Types;
namespace ChatBot.Services.Telegram
{
/// <summary>
/// Обработчик входящих сообщений Telegram
/// </summary>
public class TelegramMessageHandler : ITelegramMessageHandler
{
private readonly ILogger<TelegramMessageHandler> _logger;
private readonly ITelegramCommandProcessor _commandProcessor;
private readonly ITelegramMessageSender _messageSender;
public TelegramMessageHandler(
ILogger<TelegramMessageHandler> logger,
ITelegramCommandProcessor commandProcessor,
ITelegramMessageSender messageSender
)
{
_logger = logger;
_commandProcessor = commandProcessor;
_messageSender = messageSender;
}
/// <summary>
/// Обрабатывает входящее обновление от Telegram
/// </summary>
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
);
}
}
}
}

View File

@@ -0,0 +1,101 @@
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Exceptions;
namespace ChatBot.Services.Telegram
{
/// <summary>
/// Сервис для отправки сообщений в Telegram с повторными попытками
/// </summary>
public class TelegramMessageSender : ITelegramMessageSender
{
private readonly ILogger<TelegramMessageSender> _logger;
public TelegramMessageSender(ILogger<TelegramMessageSender> logger)
{
_logger = logger;
}
/// <summary>
/// Отправляет сообщение с повторными попытками при ошибках
/// </summary>
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);
}
}
}

View File

@@ -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<TelegramBotService> _logger;
private readonly ITelegramBotClient _botClient;
private readonly TelegramBotSettings _telegramBotSettings;
private readonly ChatService _chatService;
private readonly ModelService _modelService;
public TelegramBotService(
ILogger<TelegramBotService> logger,
IOptions<TelegramBotSettings> 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<string> 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<string> ClearChatHistory(long chatId)
{
_chatService.ClearHistory(chatId);
return Task.FromResult("История чата очищена. Начинаем новый разговор!");
}
private Task<string> 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<string> 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<string> ChangePrompt(long chatId, string newPrompt)
{
_chatService.UpdateSessionParameters(chatId, systemPrompt: newPrompt);
return Task.FromResult($"✅ Системный промпт изменен на:\n{newPrompt}");
}
private Task<string> 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<string> 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);
}
}