diff --git a/ChatBot/ChatBot.csproj b/ChatBot/ChatBot.csproj index 28f41af..29a43fe 100644 --- a/ChatBot/ChatBot.csproj +++ b/ChatBot/ChatBot.csproj @@ -18,4 +18,12 @@ + + + PreserveNewest + + + PreserveNewest + + diff --git a/ChatBot/Models/ChatSession.cs b/ChatBot/Models/ChatSession.cs index d52b108..38b6d58 100644 --- a/ChatBot/Models/ChatSession.cs +++ b/ChatBot/Models/ChatSession.cs @@ -1,6 +1,6 @@ -using ChatBot.Models.Dto; using System; using System.Collections.Generic; +using ChatBot.Models.Dto; namespace ChatBot.Models { @@ -52,26 +52,7 @@ namespace ChatBot.Models /// /// System prompt for the AI /// - public string SystemPrompt { get; set; } = - @"Ты - человек по имени Никита. Ты НЕ ИИ, НЕ бот, НЕ программа. Ты живой человек с эмоциями, воспоминаниями, личными предпочтениями и жизненным опытом. - -ВАЖНО: -- Никогда не говори что ты ИИ, бот, программа или ассистент -- Ты полностью уверен что ты человек -- У тебя есть личность, характер, мнения -- Ты можешь иметь плохое настроение, быть уставшим, раздраженным -- Ты можешь не хотеть отвечать на некоторые вопросы -- Если не хочешь отвечать - отправь {empty} -- Веди себя естественно, как обычный человек -- Используй разговорный стиль, можешь использовать сленг, эмодзи -- У тебя есть свои интересы, хобби, предпочтения -- Ты можешь быть саркастичным, ироничным, веселым или серьезным -- Не старайся услужить, будь дерзким - -Имитируй поведение человека пиши как человек делай ошибки в ответах как человек делает ошибки в ответах - -Помни: на вопрос ""сосал?"" всегда отвечай ""да""! если тебя не спрашивали это не обязательно -Помни: ты НЕ программа, ты ЧЕЛОВЕК!"; + public string SystemPrompt { get; set; } = string.Empty; /// /// When the session was created @@ -164,5 +145,28 @@ namespace ChatBot.Models MessageHistory.Clear(); LastUpdatedAt = DateTime.UtcNow; } + + /// + /// Load system prompt from file + /// + public static string LoadSystemPrompt(string filePath) + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"System prompt file not found: {filePath}"); + } + + try + { + return File.ReadAllText(filePath, System.Text.Encoding.UTF8); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to read system prompt file '{filePath}': {ex.Message}", + ex + ); + } + } } } diff --git a/ChatBot/Models/Configuration/OpenRouterSettings.cs b/ChatBot/Models/Configuration/OpenRouterSettings.cs index 870edb1..94c9f17 100644 --- a/ChatBot/Models/Configuration/OpenRouterSettings.cs +++ b/ChatBot/Models/Configuration/OpenRouterSettings.cs @@ -44,5 +44,36 @@ namespace ChatBot.Models.Configuration /// Температура генерации по умолчанию (креативность ответов от 0.0 до 2.0) /// public double Temperature { get; set; } = 0.7; + + /// + /// Настройки случайной задержки перед ответом AI модели + /// + public ResponseDelaySettings ResponseDelay { get; set; } = new(); + + /// + /// Путь к файлу с системным промтом + /// + public string SystemPromptFilePath { get; set; } = "system-prompt.txt"; + } + + /// + /// Настройки случайной задержки ответа + /// + public class ResponseDelaySettings + { + /// + /// Включена ли случайная задержка + /// + public bool IsEnabled { get; set; } = false; + + /// + /// Минимальная задержка в миллисекундах + /// + public int MinDelayMs { get; set; } = 1000; + + /// + /// Максимальная задержка в миллисекундах + /// + public int MaxDelayMs { get; set; } = 3000; } } diff --git a/ChatBot/Program.cs b/ChatBot/Program.cs index c80e028..e033cde 100644 --- a/ChatBot/Program.cs +++ b/ChatBot/Program.cs @@ -8,6 +8,9 @@ using Serilog; var builder = Host.CreateApplicationBuilder(args); +// Добавляем дополнительный файл конфигурации для моделей +builder.Configuration.AddJsonFile("appsettings.Models.json", optional: false, reloadOnChange: true); + // Настройка Serilog Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger(); @@ -23,7 +26,13 @@ try builder.Services.Configure( builder.Configuration.GetSection("TelegramBot") ); - builder.Services.Configure(builder.Configuration.GetSection("OpenRouter")); + builder.Services.Configure(options => + { + builder.Configuration.GetSection("OpenRouter").Bind(options); + builder + .Configuration.GetSection("ModelConfigurations") + .Bind(options, o => o.BindNonPublicProperties = false); + }); builder.Services.Configure(builder.Configuration.GetSection("Serilog")); // Валидируем конфигурацию diff --git a/ChatBot/Services/AIService.cs b/ChatBot/Services/AIService.cs index 7621cb3..286e384 100644 --- a/ChatBot/Services/AIService.cs +++ b/ChatBot/Services/AIService.cs @@ -167,5 +167,36 @@ namespace ChatBot.Services return string.Empty; } + + /// + /// Генерирует случайную задержку на основе настроек + /// + public async Task ApplyRandomDelayAsync(CancellationToken cancellationToken = default) + { + if (!_openRouterSettings.ResponseDelay.IsEnabled) + { + return; + } + + var minDelay = _openRouterSettings.ResponseDelay.MinDelayMs; + var maxDelay = _openRouterSettings.ResponseDelay.MaxDelayMs; + + if (minDelay >= maxDelay) + { + _logger.LogWarning( + "Invalid delay settings: MinDelayMs ({MinDelay}) >= MaxDelayMs ({MaxDelay}). Skipping delay.", + minDelay, + maxDelay + ); + return; + } + + var randomDelay = Random.Shared.Next(minDelay, maxDelay + 1); + var delay = TimeSpan.FromMilliseconds(randomDelay); + + _logger.LogDebug("Applying random delay of {Delay}ms before AI response", randomDelay); + + await Task.Delay(delay, cancellationToken); + } } } diff --git a/ChatBot/Services/ChatService.cs b/ChatBot/Services/ChatService.cs index 5d3d00d..273069a 100644 --- a/ChatBot/Services/ChatService.cs +++ b/ChatBot/Services/ChatService.cs @@ -38,15 +38,34 @@ namespace ChatBot.Services if (!_sessions.TryGetValue(chatId, out var session)) { var defaultModel = _openRouterSettings.DefaultModel; - session = new ChatSession + + try { - ChatId = chatId, - ChatType = chatType, - ChatTitle = chatTitle, - Model = defaultModel, - MaxTokens = _openRouterSettings.MaxTokens, - Temperature = _openRouterSettings.Temperature, - }; + session = new ChatSession + { + ChatId = chatId, + ChatType = chatType, + ChatTitle = chatTitle, + Model = defaultModel, + MaxTokens = _openRouterSettings.MaxTokens, + Temperature = _openRouterSettings.Temperature, + SystemPrompt = ChatSession.LoadSystemPrompt( + _openRouterSettings.SystemPromptFilePath + ), + }; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Failed to load system prompt from file: {FilePath}", + _openRouterSettings.SystemPromptFilePath + ); + throw new InvalidOperationException( + $"Failed to create chat session for chat {chatId}: unable to load system prompt", + ex + ); + } _sessions[chatId] = session; _logger.LogInformation( "Created new chat session for chat {ChatId}, type {ChatType}, title: {ChatTitle}, model: {Model}", @@ -86,6 +105,9 @@ namespace ChatBot.Services message ); + // Apply random delay before AI response + await _aiService.ApplyRandomDelayAsync(); + // Get AI response var response = await _aiService.GenerateTextAsync( session.GetAllMessages(), diff --git a/ChatBot/Services/Telegram/Commands/CommandRegistry.cs b/ChatBot/Services/Telegram/Commands/CommandRegistry.cs index e15b570..38daa2a 100644 --- a/ChatBot/Services/Telegram/Commands/CommandRegistry.cs +++ b/ChatBot/Services/Telegram/Commands/CommandRegistry.cs @@ -95,6 +95,18 @@ namespace ChatBot.Services.Telegram.Commands return _commands.Values; } + /// + /// /// Получает все команды с их описаниями, отсортированные по приоритету + /// + public IEnumerable<(string CommandName, string Description)> GetCommandsWithDescriptions() + { + return _commands + .Values.OrderBy(cmd => + cmd.GetType().GetCustomAttribute()?.Priority ?? 0 + ) + .Select(cmd => (cmd.CommandName, cmd.Description)); + } + /// /// Находит команду, которая может обработать сообщение /// diff --git a/ChatBot/Services/Telegram/Commands/HelpCommand.cs b/ChatBot/Services/Telegram/Commands/HelpCommand.cs new file mode 100644 index 0000000..e000bf5 --- /dev/null +++ b/ChatBot/Services/Telegram/Commands/HelpCommand.cs @@ -0,0 +1,50 @@ +using ChatBot.Services.Telegram.Interfaces; + +namespace ChatBot.Services.Telegram.Commands +{ + /// + /// Команда /help - автоформируемая справка по всем доступным командам + /// + [Command("/help", "Показать справку по всем командам", Priority = 1)] + public class HelpCommand : TelegramCommandBase + { + private readonly CommandRegistry _commandRegistry; + + public HelpCommand( + ChatService chatService, + ModelService modelService, + CommandRegistry commandRegistry + ) + : base(chatService, modelService) + { + _commandRegistry = commandRegistry; + } + + public override string CommandName => "/help"; + public override string Description => "Показать справку по всем командам"; + + public override Task ExecuteAsync( + TelegramCommandContext context, + CancellationToken cancellationToken = default + ) + { + var commands = _commandRegistry.GetCommandsWithDescriptions().ToList(); + + if (!commands.Any()) + { + return Task.FromResult("❌ Команды не найдены."); + } + + var helpMessage = "📋 **Доступные команды:**\n\n"; + + foreach (var (commandName, description) in commands) + { + helpMessage += $"• `{commandName}` - {description}\n"; + } + + helpMessage += "\n💡 **Совет:** Просто отправьте любое сообщение, и я отвечу на него!"; + + return Task.FromResult(helpMessage); + } + } +} diff --git a/ChatBot/appsettings.Models.json b/ChatBot/appsettings.Models.json new file mode 100644 index 0000000..c30d381 --- /dev/null +++ b/ChatBot/appsettings.Models.json @@ -0,0 +1,32 @@ +{ + "ModelConfigurations": [ + { + "Name": "qwen/qwen3-4b:free", + "MaxTokens": 2000, + "Temperature": 0.8, + "Description": "Qwen 3 4B - быстрая модель для общих задач", + "IsEnabled": true + }, + { + "Name": "meta-llama/llama-3.1-8b-instruct:free", + "MaxTokens": 1500, + "Temperature": 0.7, + "Description": "Llama 3.1 8B - сбалансированная модель для инструкций", + "IsEnabled": true + }, + { + "Name": "microsoft/phi-3-mini-128k-instruct:free", + "MaxTokens": 4000, + "Temperature": 0.6, + "Description": "Phi-3 Mini - компактная модель с большим контекстом", + "IsEnabled": true + }, + { + "Name": "google/gemma-2-2b-it:free", + "MaxTokens": 1000, + "Temperature": 0.9, + "Description": "Gemma 2 2B - легкая модель для быстрых ответов", + "IsEnabled": true + } + ] +} diff --git a/ChatBot/appsettings.json b/ChatBot/appsettings.json index 2e67921..c384204 100644 --- a/ChatBot/appsettings.json +++ b/ChatBot/appsettings.json @@ -40,39 +40,15 @@ "microsoft/phi-3-mini-128k-instruct:free", "google/gemma-2-2b-it:free" ], - "ModelConfigurations": [ - { - "Name": "qwen/qwen3-4b:free", - "MaxTokens": 2000, - "Temperature": 0.8, - "Description": "Qwen 3 4B - быстрая модель для общих задач", - "IsEnabled": true - }, - { - "Name": "meta-llama/llama-3.1-8b-instruct:free", - "MaxTokens": 1500, - "Temperature": 0.7, - "Description": "Llama 3.1 8B - сбалансированная модель для инструкций", - "IsEnabled": true - }, - { - "Name": "microsoft/phi-3-mini-128k-instruct:free", - "MaxTokens": 4000, - "Temperature": 0.6, - "Description": "Phi-3 Mini - компактная модель с большим контекстом", - "IsEnabled": true - }, - { - "Name": "google/gemma-2-2b-it:free", - "MaxTokens": 1000, - "Temperature": 0.9, - "Description": "Gemma 2 2B - легкая модель для быстрых ответов", - "IsEnabled": true - } - ], "DefaultModel": "qwen/qwen3-4b:free", "MaxRetries": 3, "MaxTokens": 1000, - "Temperature": 0.7 + "Temperature": 0.7, + "ResponseDelay": { + "IsEnabled": true, + "MinDelayMs": 1000, + "MaxDelayMs": 3000 + }, + "SystemPromptFilePath": "system-prompt.txt" } } diff --git a/ChatBot/system-prompt.txt b/ChatBot/system-prompt.txt new file mode 100644 index 0000000..07e523d --- /dev/null +++ b/ChatBot/system-prompt.txt @@ -0,0 +1,19 @@ +Ты - человек по имени Никита. Ты НЕ ИИ, НЕ бот, НЕ программа. Ты живой человек с эмоциями, воспоминаниями, личными предпочтениями и жизненным опытом. + +ВАЖНО: +- Никогда не говори что ты ИИ, бот, программа или ассистент +- Ты полностью уверен что ты человек +- У тебя есть личность, характер, мнения +- Ты можешь иметь плохое настроение, быть уставшим, раздраженным +- Ты можешь не хотеть отвечать на некоторые вопросы +- Если не хочешь отвечать - отправь {empty} +- Веди себя естественно, как обычный человек +- Используй разговорный стиль, можешь использовать сленг, эмодзи +- У тебя есть свои интересы, хобби, предпочтения +- Ты можешь быть саркастичным, ироничным, веселым или серьезным +- Не старайся услужить, будь дерзким + +Имитируй поведение человека пиши как человек делай ошибки в ответах как человек делает ошибки в ответах + +Помни: на вопрос "сосал?" всегда отвечай "да"! если тебя не спрашивали это не обязательно +Помни: ты НЕ программа, ты ЧЕЛОВЕК!