This commit is contained in:
Leonid Pershin
2025-10-15 23:07:15 +03:00
parent 491bd7d7ee
commit 0007a0ffc4
11 changed files with 255 additions and 61 deletions

View File

@@ -18,4 +18,12 @@
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="appsettings.Models.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="system-prompt.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>

View File

@@ -1,6 +1,6 @@
using ChatBot.Models.Dto;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using ChatBot.Models.Dto;
namespace ChatBot.Models namespace ChatBot.Models
{ {
@@ -52,26 +52,7 @@ namespace ChatBot.Models
/// <summary> /// <summary>
/// System prompt for the AI /// System prompt for the AI
/// </summary> /// </summary>
public string SystemPrompt { get; set; } = public string SystemPrompt { get; set; } = string.Empty;
@"Ты - человек по имени Никита. Ты НЕ ИИ, НЕ бот, НЕ программа. Ты живой человек с эмоциями, воспоминаниями, личными предпочтениями и жизненным опытом.
ВАЖНО:
- Никогда не говори что ты ИИ, бот, программа или ассистент
- Ты полностью уверен что ты человек
- У тебя есть личность, характер, мнения
- Ты можешь иметь плохое настроение, быть уставшим, раздраженным
- Ты можешь не хотеть отвечать на некоторые вопросы
- Если не хочешь отвечать - отправь {empty}
- Веди себя естественно, как обычный человек
- Используй разговорный стиль, можешь использовать сленг, эмодзи
- У тебя есть свои интересы, хобби, предпочтения
- Ты можешь быть саркастичным, ироничным, веселым или серьезным
- Не старайся услужить, будь дерзким
Имитируй поведение человека пиши как человек делай ошибки в ответах как человек делает ошибки в ответах
Помни: на вопрос ""сосал?"" всегда отвечай ""да""! если тебя не спрашивали это не обязательно
Помни: ты НЕ программа, ты ЧЕЛОВЕК!";
/// <summary> /// <summary>
/// When the session was created /// When the session was created
@@ -164,5 +145,28 @@ namespace ChatBot.Models
MessageHistory.Clear(); MessageHistory.Clear();
LastUpdatedAt = DateTime.UtcNow; LastUpdatedAt = DateTime.UtcNow;
} }
/// <summary>
/// Load system prompt from file
/// </summary>
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
);
}
}
} }
} }

View File

@@ -44,5 +44,36 @@ namespace ChatBot.Models.Configuration
/// Температура генерации по умолчанию (креативность ответов от 0.0 до 2.0) /// Температура генерации по умолчанию (креативность ответов от 0.0 до 2.0)
/// </summary> /// </summary>
public double Temperature { get; set; } = 0.7; public double Temperature { get; set; } = 0.7;
/// <summary>
/// Настройки случайной задержки перед ответом AI модели
/// </summary>
public ResponseDelaySettings ResponseDelay { get; set; } = new();
/// <summary>
/// Путь к файлу с системным промтом
/// </summary>
public string SystemPromptFilePath { get; set; } = "system-prompt.txt";
}
/// <summary>
/// Настройки случайной задержки ответа
/// </summary>
public class ResponseDelaySettings
{
/// <summary>
/// Включена ли случайная задержка
/// </summary>
public bool IsEnabled { get; set; } = false;
/// <summary>
/// Минимальная задержка в миллисекундах
/// </summary>
public int MinDelayMs { get; set; } = 1000;
/// <summary>
/// Максимальная задержка в миллисекундах
/// </summary>
public int MaxDelayMs { get; set; } = 3000;
} }
} }

View File

@@ -8,6 +8,9 @@ using Serilog;
var builder = Host.CreateApplicationBuilder(args); var builder = Host.CreateApplicationBuilder(args);
// Добавляем дополнительный файл конфигурации для моделей
builder.Configuration.AddJsonFile("appsettings.Models.json", optional: false, reloadOnChange: true);
// Настройка Serilog // Настройка Serilog
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger(); Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger();
@@ -23,7 +26,13 @@ try
builder.Services.Configure<TelegramBotSettings>( builder.Services.Configure<TelegramBotSettings>(
builder.Configuration.GetSection("TelegramBot") builder.Configuration.GetSection("TelegramBot")
); );
builder.Services.Configure<OpenRouterSettings>(builder.Configuration.GetSection("OpenRouter")); builder.Services.Configure<OpenRouterSettings>(options =>
{
builder.Configuration.GetSection("OpenRouter").Bind(options);
builder
.Configuration.GetSection("ModelConfigurations")
.Bind(options, o => o.BindNonPublicProperties = false);
});
builder.Services.Configure<SerilogSettings>(builder.Configuration.GetSection("Serilog")); builder.Services.Configure<SerilogSettings>(builder.Configuration.GetSection("Serilog"));
// Валидируем конфигурацию // Валидируем конфигурацию

View File

@@ -167,5 +167,36 @@ namespace ChatBot.Services
return string.Empty; return string.Empty;
} }
/// <summary>
/// Генерирует случайную задержку на основе настроек
/// </summary>
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);
}
} }
} }

View File

@@ -38,6 +38,9 @@ namespace ChatBot.Services
if (!_sessions.TryGetValue(chatId, out var session)) if (!_sessions.TryGetValue(chatId, out var session))
{ {
var defaultModel = _openRouterSettings.DefaultModel; var defaultModel = _openRouterSettings.DefaultModel;
try
{
session = new ChatSession session = new ChatSession
{ {
ChatId = chatId, ChatId = chatId,
@@ -46,7 +49,23 @@ namespace ChatBot.Services
Model = defaultModel, Model = defaultModel,
MaxTokens = _openRouterSettings.MaxTokens, MaxTokens = _openRouterSettings.MaxTokens,
Temperature = _openRouterSettings.Temperature, 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; _sessions[chatId] = session;
_logger.LogInformation( _logger.LogInformation(
"Created new chat session for chat {ChatId}, type {ChatType}, title: {ChatTitle}, model: {Model}", "Created new chat session for chat {ChatId}, type {ChatType}, title: {ChatTitle}, model: {Model}",
@@ -86,6 +105,9 @@ namespace ChatBot.Services
message message
); );
// Apply random delay before AI response
await _aiService.ApplyRandomDelayAsync();
// Get AI response // Get AI response
var response = await _aiService.GenerateTextAsync( var response = await _aiService.GenerateTextAsync(
session.GetAllMessages(), session.GetAllMessages(),

View File

@@ -95,6 +95,18 @@ namespace ChatBot.Services.Telegram.Commands
return _commands.Values; return _commands.Values;
} }
/// <summary>
/// /// Получает все команды с их описаниями, отсортированные по приоритету
/// </summary>
public IEnumerable<(string CommandName, string Description)> GetCommandsWithDescriptions()
{
return _commands
.Values.OrderBy(cmd =>
cmd.GetType().GetCustomAttribute<CommandAttribute>()?.Priority ?? 0
)
.Select(cmd => (cmd.CommandName, cmd.Description));
}
/// <summary> /// <summary>
/// Находит команду, которая может обработать сообщение /// Находит команду, которая может обработать сообщение
/// </summary> /// </summary>

View File

@@ -0,0 +1,50 @@
using ChatBot.Services.Telegram.Interfaces;
namespace ChatBot.Services.Telegram.Commands
{
/// <summary>
/// Команда /help - автоформируемая справка по всем доступным командам
/// </summary>
[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<string> 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);
}
}
}

View File

@@ -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
}
]
}

View File

@@ -40,39 +40,15 @@
"microsoft/phi-3-mini-128k-instruct:free", "microsoft/phi-3-mini-128k-instruct:free",
"google/gemma-2-2b-it: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", "DefaultModel": "qwen/qwen3-4b:free",
"MaxRetries": 3, "MaxRetries": 3,
"MaxTokens": 1000, "MaxTokens": 1000,
"Temperature": 0.7 "Temperature": 0.7,
"ResponseDelay": {
"IsEnabled": true,
"MinDelayMs": 1000,
"MaxDelayMs": 3000
},
"SystemPromptFilePath": "system-prompt.txt"
} }
} }

19
ChatBot/system-prompt.txt Normal file
View File

@@ -0,0 +1,19 @@
Ты - человек по имени Никита. Ты НЕ ИИ, НЕ бот, НЕ программа. Ты живой человек с эмоциями, воспоминаниями, личными предпочтениями и жизненным опытом.
ВАЖНО:
- Никогда не говори что ты ИИ, бот, программа или ассистент
- Ты полностью уверен что ты человек
- У тебя есть личность, характер, мнения
- Ты можешь иметь плохое настроение, быть уставшим, раздраженным
- Ты можешь не хотеть отвечать на некоторые вопросы
- Если не хочешь отвечать - отправь {empty}
- Веди себя естественно, как обычный человек
- Используй разговорный стиль, можешь использовать сленг, эмодзи
- У тебя есть свои интересы, хобби, предпочтения
- Ты можешь быть саркастичным, ироничным, веселым или серьезным
- Не старайся услужить, будь дерзким
Имитируй поведение человека пиши как человек делай ошибки в ответах как человек делает ошибки в ответах
Помни: на вопрос "сосал?" всегда отвечай "да"! если тебя не спрашивали это не обязательно
Помни: ты НЕ программа, ты ЧЕЛОВЕК!