This commit is contained in:
Leonid Pershin
2025-10-16 07:57:22 +03:00
parent b4f8df6816
commit bf1d1c0770
17 changed files with 3 additions and 1320 deletions

View File

@@ -17,8 +17,6 @@
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="FluentValidation" Version="11.10.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.10.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@@ -1,12 +0,0 @@
namespace ChatBot.Common.Constants
{
/// <summary>
/// Constants for retry logic
/// </summary>
public static class RetryConstants
{
public const int DefaultMaxRetries = 3;
public const int DefaultBaseDelaySeconds = 1;
public const int DefaultMaxJitterMs = 2000;
}
}

View File

@@ -1,39 +0,0 @@
namespace ChatBot.Common.Results
{
/// <summary>
/// Represents the result of an operation that can succeed or fail
/// </summary>
public class Result
{
public bool IsSuccess { get; }
public string Error { get; }
protected Result(bool isSuccess, string error)
{
IsSuccess = isSuccess;
Error = error;
}
public static Result Success() => new(true, string.Empty);
public static Result Failure(string error) => new(false, error);
}
/// <summary>
/// Represents the result of an operation that returns a value
/// </summary>
public class Result<T> : Result
{
public T? Value { get; }
private Result(T? value, bool isSuccess, string error)
: base(isSuccess, error)
{
Value = value;
}
public static Result<T> Success(T value) => new(value, true, string.Empty);
public static new Result<T> Failure(string error) => new(default, false, error);
}
}

View File

@@ -1,23 +0,0 @@
namespace ChatBot.Models.Configuration
{
/// <summary>
/// Основные настройки приложения
/// </summary>
public class AppSettings
{
/// <summary>
/// Настройки Telegram бота
/// </summary>
public TelegramBotSettings TelegramBot { get; set; } = new();
/// <summary>
/// Настройки Ollama API
/// </summary>
public OllamaSettings Ollama { get; set; } = new();
/// <summary>
/// Настройки логирования Serilog
/// </summary>
public SerilogSettings Serilog { get; set; } = new();
}
}

View File

@@ -1,60 +0,0 @@
namespace ChatBot.Models.Configuration
{
/// <summary>
/// Настройки логирования Serilog
/// </summary>
public class SerilogSettings
{
/// <summary>
/// Список используемых sink'ов для логирования
/// </summary>
public List<string> Using { get; set; } = new();
/// <summary>
/// Настройки минимального уровня логирования
/// </summary>
public MinimumLevelSettings MinimumLevel { get; set; } = new();
/// <summary>
/// Настройки получателей логов (куда писать логи)
/// </summary>
public List<WriteToSettings> WriteTo { get; set; } = new();
/// <summary>
/// Список обогатителей логов (дополнительная информация)
/// </summary>
public List<string> Enrich { get; set; } = new();
}
/// <summary>
/// Настройки минимального уровня логирования
/// </summary>
public class MinimumLevelSettings
{
/// <summary>
/// Уровень логирования по умолчанию
/// </summary>
public string Default { get; set; } = "Information";
/// <summary>
/// Переопределения уровня логирования для конкретных пространств имен
/// </summary>
public Dictionary<string, string> Override { get; set; } = new();
}
/// <summary>
/// Настройки получателя логов
/// </summary>
public class WriteToSettings
{
/// <summary>
/// Название sink'а для записи логов
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Аргументы для настройки sink'а
/// </summary>
public Dictionary<string, object> Args { get; set; } = new();
}
}

View File

@@ -1,129 +0,0 @@
namespace ChatBot.Models.Configuration.Validators
{
/// <summary>
/// Валидатор конфигурации приложения
/// </summary>
public static class ConfigurationValidator
{
/// <summary>
/// Валидирует все настройки приложения
/// </summary>
/// <param name="settings">Настройки приложения</param>
/// <returns>Результат валидации</returns>
public static ValidationResult ValidateAppSettings(AppSettings settings)
{
var errors = new List<string>();
// Валидация настроек Telegram бота
var telegramResult = ValidateTelegramBotSettings(settings.TelegramBot);
errors.AddRange(telegramResult.Errors);
// Валидация настроек Ollama
var ollamaResult = ValidateOllamaSettings(settings.Ollama);
errors.AddRange(ollamaResult.Errors);
return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
}
/// <summary>
/// Валидирует настройки Telegram бота
/// </summary>
/// <param name="settings">Настройки Telegram бота</param>
/// <returns>Результат валидации</returns>
public static ValidationResult ValidateTelegramBotSettings(TelegramBotSettings settings)
{
var errors = new List<string>();
// Проверка наличия токена бота
if (string.IsNullOrWhiteSpace(settings.BotToken))
{
errors.Add("TelegramBot:BotToken is required");
}
// Проверка формата токена (должен содержать ':' или начинаться с 'bot')
else if (
!settings.BotToken.StartsWith("bot", StringComparison.OrdinalIgnoreCase)
&& !settings.BotToken.Contains(":")
)
{
errors.Add(
"TelegramBot:BotToken appears to be invalid (should contain ':' or start with 'bot')"
);
}
return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
}
/// <summary>
/// Валидирует настройки Ollama
/// </summary>
/// <param name="settings">Настройки Ollama</param>
/// <returns>Результат валидации</returns>
public static ValidationResult ValidateOllamaSettings(OllamaSettings settings)
{
var errors = new List<string>();
// Валидация основных компонентов настроек Ollama
ValidateUrl(settings.Url, errors);
ValidateNumericSettings(settings, errors);
if (string.IsNullOrWhiteSpace(settings.DefaultModel))
{
errors.Add("Ollama:DefaultModel is required");
}
return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
}
/// <summary>
/// Валидирует URL Ollama
/// </summary>
/// <param name="url">URL для проверки</param>
/// <param name="errors">Список ошибок валидации</param>
private static void ValidateUrl(string url, List<string> errors)
{
// Проверка наличия URL
if (string.IsNullOrWhiteSpace(url))
{
errors.Add("Ollama:Url is required");
}
// Проверка корректности URL (должен быть валидным HTTP/HTTPS URL)
else if (
!Uri.TryCreate(url, UriKind.Absolute, out var uri)
|| (uri.Scheme != "http" && uri.Scheme != "https")
)
{
errors.Add("Ollama:Url must be a valid HTTP/HTTPS URL");
}
}
/// <summary>
/// Валидирует числовые параметры настроек Ollama
/// </summary>
/// <param name="settings">Настройки Ollama</param>
/// <param name="errors">Список ошибок валидации</param>
private static void ValidateNumericSettings(OllamaSettings settings, List<string> errors)
{
// Проверка количества повторных попыток (1-10)
if (settings.MaxRetries < 1 || settings.MaxRetries > 10)
{
errors.Add("Ollama:MaxRetries must be between 1 and 10");
}
}
}
/// <summary>
/// Результат валидации конфигурации
/// </summary>
public class ValidationResult
{
/// <summary>
/// Указывает, прошла ли валидация успешно
/// </summary>
public bool IsValid { get; set; }
/// <summary>
/// Список ошибок валидации
/// </summary>
public List<string> Errors { get; set; } = new();
}
}

View File

@@ -1,33 +0,0 @@
using ChatBot.Common.Constants;
using ChatBot.Models.Dto;
using FluentValidation;
namespace ChatBot.Models.Validation
{
/// <summary>
/// Validator for ChatMessage
/// </summary>
public class ChatMessageValidator : AbstractValidator<ChatMessage>
{
public ChatMessageValidator()
{
RuleFor(x => x.Content)
.NotEmpty()
.WithMessage("Message content cannot be empty")
.MaximumLength(10000)
.WithMessage("Message content is too long (max 10000 characters)");
RuleFor(x => x.Role)
.NotEmpty()
.WithMessage("Message role cannot be empty")
.Must(role =>
role == ChatRoles.System
|| role == ChatRoles.User
|| role == ChatRoles.Assistant
)
.WithMessage(
$"Invalid message role. Must be one of: {ChatRoles.System}, {ChatRoles.User}, {ChatRoles.Assistant}"
);
}
}
}

View File

@@ -1,14 +1,11 @@
using ChatBot.Models.Configuration;
using ChatBot.Models.Configuration.Validators;
using ChatBot.Models.Validation;
using ChatBot.Services;
using ChatBot.Services.ErrorHandlers;
using ChatBot.Services.HealthChecks;
using ChatBot.Services.Interfaces;
using ChatBot.Services.Telegram.Commands;
using ChatBot.Services.Telegram.Interfaces;
using ChatBot.Services.Telegram.Services;
using FluentValidation;
using Microsoft.Extensions.Options;
using Serilog;
using Telegram.Bot;
@@ -26,8 +23,6 @@ try
builder.Services.AddSerilog();
// Конфигурируем настройки с валидацией
builder.Services.Configure<AppSettings>(builder.Configuration);
builder
.Services.Configure<TelegramBotSettings>(builder.Configuration.GetSection("TelegramBot"))
.AddSingleton<IValidateOptions<TelegramBotSettings>, TelegramBotSettingsValidator>();
@@ -36,36 +31,10 @@ try
.Services.Configure<OllamaSettings>(builder.Configuration.GetSection("Ollama"))
.AddSingleton<IValidateOptions<OllamaSettings>, OllamaSettingsValidator>();
builder.Services.Configure<SerilogSettings>(builder.Configuration.GetSection("Serilog"));
// Валидируем конфигурацию при старте
builder.Services.AddOptions<TelegramBotSettings>().ValidateOnStart();
builder.Services.AddOptions<OllamaSettings>().ValidateOnStart();
// Валидируем конфигурацию (старый способ для совместимости)
var appSettings = builder.Configuration.Get<AppSettings>();
if (appSettings == null)
{
Log.ForContext<Program>().Fatal("Failed to load configuration");
return;
}
var validationResult = ConfigurationValidator.ValidateAppSettings(appSettings);
if (!validationResult.IsValid)
{
Log.ForContext<Program>().Fatal("Configuration validation failed:");
foreach (var error in validationResult.Errors)
{
Log.ForContext<Program>().Fatal(" - {Error}", error);
}
return;
}
Log.ForContext<Program>().Debug("Configuration validation passed");
// Регистрируем FluentValidation валидаторы
builder.Services.AddValidatorsFromAssemblyContaining<ChatMessageValidator>();
// Регистрируем IOllamaClient
builder.Services.AddSingleton<IOllamaClient>(sp =>
{
@@ -76,13 +45,6 @@ try
// Регистрируем интерфейсы и сервисы
builder.Services.AddSingleton<ISessionStorage, InMemorySessionStorage>();
// Регистрируем error handlers
builder.Services.AddSingleton<IErrorHandler, RateLimitErrorHandler>();
builder.Services.AddSingleton<IErrorHandler, NetworkErrorHandler>();
// Регистрируем retry policy (использует error handlers)
builder.Services.AddSingleton<IRetryPolicy, ExponentialBackoffRetryPolicy>();
// Регистрируем основные сервисы
builder.Services.AddSingleton<ModelService>();
builder.Services.AddSingleton<IAIService, AIService>();

View File

@@ -1,49 +0,0 @@
using ChatBot.Services.Interfaces;
namespace ChatBot.Services.ErrorHandlers
{
/// <summary>
/// Error handler for network-related errors
/// </summary>
public class NetworkErrorHandler : IErrorHandler
{
private readonly ILogger<NetworkErrorHandler> _logger;
public NetworkErrorHandler(ILogger<NetworkErrorHandler> logger)
{
_logger = logger;
}
public bool CanHandle(Exception exception)
{
return exception is HttpRequestException
|| exception is TaskCanceledException
|| exception.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase)
|| exception.Message.Contains("connection", StringComparison.OrdinalIgnoreCase);
}
public async Task<ErrorHandlingResult> HandleAsync(
Exception exception,
int attempt,
string currentModel,
CancellationToken cancellationToken = default
)
{
_logger.LogWarning(
exception,
"Network error on attempt {Attempt} for model {Model}",
attempt,
currentModel
);
// Apply exponential backoff for network errors
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt - 1));
_logger.LogInformation("Waiting {Delay} before retry due to network error", delay);
await Task.Delay(delay, cancellationToken);
return ErrorHandlingResult.Retry();
}
}
}

View File

@@ -1,52 +0,0 @@
using ChatBot.Services.Interfaces;
namespace ChatBot.Services.ErrorHandlers
{
/// <summary>
/// Error handler for rate limit errors (HTTP 429)
/// </summary>
public class RateLimitErrorHandler : IErrorHandler
{
private readonly ILogger<RateLimitErrorHandler> _logger;
public RateLimitErrorHandler(ILogger<RateLimitErrorHandler> logger)
{
_logger = logger;
}
public bool CanHandle(Exception exception)
{
return exception.Message.Contains("429")
|| exception.Message.Contains("Too Many Requests")
|| exception.Message.Contains("rate limit", StringComparison.OrdinalIgnoreCase);
}
public async Task<ErrorHandlingResult> HandleAsync(
Exception exception,
int attempt,
string currentModel,
CancellationToken cancellationToken = default
)
{
_logger.LogWarning(
exception,
"Rate limit exceeded on attempt {Attempt} for model {Model}",
attempt,
currentModel
);
// Apply exponential backoff for rate limiting
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt - 1));
var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, 2000));
_logger.LogInformation(
"Rate limit hit, waiting {Delay} before retry",
delay.Add(jitter)
);
await Task.Delay(delay.Add(jitter), cancellationToken);
return ErrorHandlingResult.Retry();
}
}
}

View File

@@ -1,111 +0,0 @@
using ChatBot.Common.Constants;
using ChatBot.Models.Configuration;
using ChatBot.Services.Interfaces;
using Microsoft.Extensions.Options;
namespace ChatBot.Services
{
/// <summary>
/// Retry policy with exponential backoff and jitter
/// </summary>
public class ExponentialBackoffRetryPolicy : IRetryPolicy
{
private readonly int _maxRetries;
private readonly ILogger<ExponentialBackoffRetryPolicy> _logger;
private readonly IEnumerable<IErrorHandler> _errorHandlers;
public ExponentialBackoffRetryPolicy(
IOptions<OllamaSettings> settings,
ILogger<ExponentialBackoffRetryPolicy> logger,
IEnumerable<IErrorHandler> errorHandlers
)
{
_maxRetries = settings.Value.MaxRetries;
_logger = logger;
_errorHandlers = errorHandlers;
}
public async Task<T> ExecuteAsync<T>(
Func<Task<T>> action,
CancellationToken cancellationToken = default
)
{
Exception? lastException = null;
for (int attempt = 1; attempt <= _maxRetries; attempt++)
{
try
{
return await action();
}
catch (Exception ex) when (attempt < _maxRetries)
{
lastException = ex;
LogAttemptFailure(ex, attempt);
if (!await HandleErrorAndDecideRetry(ex, attempt, cancellationToken))
break;
}
catch (Exception ex)
{
lastException = ex;
_logger.LogError(ex, "All {MaxRetries} attempts failed", _maxRetries);
}
}
throw new InvalidOperationException(
$"Failed after {_maxRetries} attempts",
lastException
);
}
private void LogAttemptFailure(Exception ex, int attempt)
{
_logger.LogWarning(ex, "Attempt {Attempt}/{MaxRetries} failed", attempt, _maxRetries);
}
private async Task<bool> HandleErrorAndDecideRetry(
Exception ex,
int attempt,
CancellationToken cancellationToken
)
{
var handler = _errorHandlers.FirstOrDefault(h => h.CanHandle(ex));
if (handler == null)
{
await DelayWithBackoff(attempt, cancellationToken);
return true;
}
var result = await handler.HandleAsync(ex, attempt, string.Empty, cancellationToken);
if (result.IsFatal)
{
_logger.LogError("Fatal error occurred: {ErrorMessage}", result.ErrorMessage);
return false;
}
return result.ShouldRetry;
}
private async Task DelayWithBackoff(int attempt, CancellationToken cancellationToken)
{
var baseDelay = TimeSpan.FromSeconds(
Math.Pow(2, attempt - 1) * RetryConstants.DefaultBaseDelaySeconds
);
var jitter = TimeSpan.FromMilliseconds(
Random.Shared.Next(0, RetryConstants.DefaultMaxJitterMs)
);
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,6 +1,6 @@
using System.Collections.Concurrent;
using ChatBot.Models;
using ChatBot.Services.Interfaces;
using System.Collections.Concurrent;
namespace ChatBot.Services
{

View File

@@ -1,42 +0,0 @@
namespace ChatBot.Services.Interfaces
{
/// <summary>
/// Interface for error handling strategy
/// </summary>
public interface IErrorHandler
{
/// <summary>
/// Check if this handler can handle the exception
/// </summary>
bool CanHandle(Exception exception);
/// <summary>
/// Handle the exception and return result
/// </summary>
Task<ErrorHandlingResult> HandleAsync(
Exception exception,
int attempt,
string currentModel,
CancellationToken cancellationToken = default
);
}
/// <summary>
/// Result of error handling
/// </summary>
public class ErrorHandlingResult
{
public bool ShouldRetry { get; set; }
public string? NewModel { get; set; }
public bool IsFatal { get; set; }
public string? ErrorMessage { get; set; }
public static ErrorHandlingResult Retry(string? newModel = null) =>
new() { ShouldRetry = true, NewModel = newModel };
public static ErrorHandlingResult Fatal(string errorMessage) =>
new() { IsFatal = true, ErrorMessage = errorMessage };
public static ErrorHandlingResult NoRetry() => new() { ShouldRetry = false };
}
}

View File

@@ -1,16 +0,0 @@
namespace ChatBot.Services.Interfaces
{
/// <summary>
/// Interface for retry policy
/// </summary>
public interface IRetryPolicy
{
/// <summary>
/// Execute an action with retry logic
/// </summary>
Task<T> ExecuteAsync<T>(
Func<Task<T>> action,
CancellationToken cancellationToken = default
);
}
}

View File

@@ -15,24 +15,20 @@ namespace ChatBot.Services.Telegram.Services
{
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,
ITelegramBotClient botClient,
ITelegramMessageHandler messageHandler,
ITelegramErrorHandler errorHandler
)
{
_logger = logger;
_telegramBotSettings = telegramBotSettings.Value;
_botClient = botClient;
_messageHandler = messageHandler;
_errorHandler = errorHandler;
ValidateConfiguration();
_botClient = new TelegramBotClient(_telegramBotSettings.BotToken);
}
/// <summary>
@@ -98,15 +94,5 @@ namespace ChatBot.Services.Telegram.Services
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"
);
}
}
}
}

248
README.md
View File

@@ -1,248 +0,0 @@
# Telegram ChatBot with Ollama AI
A high-quality, production-ready Telegram chatbot powered by Ollama AI models. This bot provides natural conversation experiences using local AI models.
## 🎯 Features
- **Ollama Integration**: Uses OllamaSharp library for efficient AI model communication
- **Multiple Model Support**: Automatically manages and switches between multiple AI models
- **Session Management**: Maintains conversation history for each chat
- **Command System**: Extensible command architecture for bot commands
- **Smart Retry Logic**: Exponential backoff with jitter for failed requests
- **Rate Limit Handling**: Automatic model switching on rate limits
- **Natural Conversation**: Configurable response delays for human-like interactions
- **Group Chat Support**: Works in both private and group conversations
- **Robust Logging**: Comprehensive logging with Serilog
## 📋 Prerequisites
- .NET 9.0 or later
- Ollama server running locally or remotely
- Telegram Bot Token (from [@BotFather](https://t.me/botfather))
## 🚀 Getting Started
### 1. Install Ollama
Download and install Ollama from [ollama.ai](https://ollama.ai)
### 2. Pull an AI Model
```bash
ollama pull llama3
```
### 3. Configure the Bot
Edit `appsettings.json`:
```json
{
"TelegramBot": {
"BotToken": "YOUR_BOT_TOKEN_HERE"
},
"Ollama": {
"Url": "http://localhost:11434",
"MaxRetries": 3,
"MaxTokens": 1000,
"Temperature": 0.7,
"ResponseDelay": {
"IsEnabled": true,
"MinDelayMs": 1000,
"MaxDelayMs": 3000
},
"SystemPromptFilePath": "system-prompt.txt"
}
}
```
Edit `appsettings.Models.json` to configure your models:
```json
{
"ModelConfigurations": [
{
"Name": "llama3",
"MaxTokens": 2000,
"Temperature": 0.8,
"Description": "Llama 3 Model",
"IsEnabled": true
}
]
}
```
### 4. Customize System Prompt
Edit `system-prompt.txt` to define your bot's personality and behavior.
### 5. Run the Bot
```bash
cd ChatBot
dotnet run
```
## 🏗️ Architecture
### Core Services
- **AIService**: Handles AI model communication and text generation
- **ChatService**: Manages chat sessions and message history
- **ModelService**: Handles model selection and switching
- **TelegramBotService**: Main Telegram bot service
### Command System
Commands are automatically registered using attributes:
```csharp
[Command("start", "Start conversation with the bot")]
public class StartCommand : TelegramCommandBase
{
// Implementation
}
```
Available commands:
- `/start` - Start conversation
- `/help` - Show help information
- `/clear` - Clear conversation history
- `/settings` - View current settings
## ⚙️ Configuration
### Ollama Settings
- **Url**: Ollama server URL
- **MaxRetries**: Maximum retry attempts for failed requests
- **MaxTokens**: Default maximum tokens for responses
- **Temperature**: AI creativity level (0.0 - 2.0)
- **ResponseDelay**: Add human-like delays before responses
- **SystemPromptFilePath**: Path to system prompt file
### Model Configuration
Each model can have custom settings:
- **Name**: Model name (must match Ollama model name)
- **MaxTokens**: Maximum tokens for this model
- **Temperature**: Temperature for this model
- **Description**: Human-readable description
- **IsEnabled**: Whether the model is available for use
## 🔧 Advanced Features
### Automatic Model Switching
The bot automatically switches to alternative models when:
- Rate limits are encountered
- Current model becomes unavailable
### Session Management
- Automatic session creation per chat
- Configurable message history length
- Old session cleanup (default: 24 hours)
### Error Handling
- Exponential backoff with jitter for retries
- Graceful degradation on failures
- Comprehensive error logging
## 📝 Development
### Project Structure
```
ChatBot/
├── Models/
│ ├── Configuration/ # Configuration models
│ │ └── Validators/ # Configuration validation
│ └── Dto/ # Data transfer objects
├── Services/
│ ├── Telegram/ # Telegram-specific services
│ │ ├── Commands/ # Bot commands
│ │ ├── Interfaces/ # Service interfaces
│ │ └── Services/ # Service implementations
│ ├── AIService.cs # AI model communication
│ ├── ChatService.cs # Chat session management
│ └── ModelService.cs # Model management
└── Program.cs # Application entry point
```
### Adding New Commands
1. Create a new class in `Services/Telegram/Commands/`
2. Inherit from `TelegramCommandBase`
3. Add `[Command]` attribute
4. Implement `ExecuteAsync` method
Example:
```csharp
[Command("mycommand", "Description of my command")]
public class MyCommand : TelegramCommandBase
{
public override async Task ExecuteAsync(TelegramCommandContext context)
{
await context.MessageSender.SendTextMessageAsync(
context.Message.Chat.Id,
"Command executed!"
);
}
}
```
## 🐛 Troubleshooting
### Bot doesn't respond
1. Check if Ollama server is running: `ollama list`
2. Verify bot token in `appsettings.json`
3. Check logs in `logs/` directory
### Model not found
1. Pull the model: `ollama pull model-name`
2. Verify model name matches in `appsettings.Models.json`
3. Check model availability: `ollama list`
### Connection errors
1. Verify Ollama URL in configuration
2. Check firewall settings
3. Ensure Ollama server is accessible
## 📦 Dependencies
- **OllamaSharp** (v5.4.7): Ollama API client
- **Telegram.Bot** (v22.7.2): Telegram Bot API
- **Serilog** (v4.3.0): Structured logging
- **Microsoft.Extensions.Hosting** (v9.0.10): Host infrastructure
## 📄 License
This project is licensed under the terms specified in [LICENSE.txt](LICENSE.txt).
## 🤝 Contributing
Contributions are welcome! Please ensure:
- Code follows existing patterns
- All tests pass
- Documentation is updated
- Commits are descriptive
## 🔮 Future Enhancements
- [ ] Multi-language support
- [ ] Voice message handling
- [ ] Image generation support
- [ ] User preferences persistence
- [ ] Advanced conversation analytics
- [ ] Custom model fine-tuning support
---
Built with ❤️ using .NET 9.0 and Ollama

View File

@@ -1,449 +0,0 @@
# Рефакторинг проекта ChatBot - Итоги
## 📋 Выполненные улучшения
Все рекомендации по улучшению проекта были реализованы, за исключением unit-тестов (как было запрошено).
---
## ✅ Реализованные изменения
### 1. **Константы для магических строк и значений**
Созданы классы констант для улучшения читаемости и поддерживаемости:
- `ChatBot/Common/Constants/AIResponseConstants.cs` - константы для AI ответов
- `ChatBot/Common/Constants/ChatRoles.cs` - роли сообщений (system, user, assistant)
- `ChatBot/Common/Constants/ChatTypes.cs` - типы чатов
- `ChatBot/Common/Constants/RetryConstants.cs` - константы для retry логики
**Преимущества:**
- Нет магических строк в коде
- Легко изменить значения в одном месте
- IntelliSense помогает при разработке
---
### 2. **Result Pattern**
Создан класс `Result<T>` для явного представления успеха/неудачи операций:
**Файл:** `ChatBot/Common/Results/Result.cs`
```csharp
var result = Result<string>.Success("данные");
var failure = Result<string>.Failure("ошибка");
```
**Преимущества:**
- Явная обработка ошибок без exceptions
- Более функциональный подход
- Лучшая читаемость кода
---
### 3. **SOLID Principles - Интерфейсы для всех сервисов**
#### **Dependency Inversion Principle (DIP)**
Созданы интерфейсы для всех основных сервисов:
- `IAIService` - интерфейс для AI сервиса
- `ISessionStorage` - интерфейс для хранения сессий
- `IOllamaClient` - интерфейс для Ollama клиента
- `ISystemPromptProvider` - интерфейс для загрузки системного промпта
- `IRetryPolicy` - интерфейс для retry логики
- `IResponseDelayService` - интерфейс для задержек
- `IErrorHandler` - интерфейс для обработки ошибок
**Преимущества:**
- Слабая связанность компонентов
- Легко тестировать с моками
- Можно менять реализацию без изменения зависимых классов
---
### 4. **Single Responsibility Principle (SRP)**
#### **Разделение ответственностей в AIService**
**До:** AIService делал все - генерацию, retry, задержки, переключение моделей
**После:** Каждый класс отвечает за одну вещь:
- `AIService` - только генерация текста
- `ExponentialBackoffRetryPolicy` - retry логика
- `RandomResponseDelayService` - задержки ответов
- `RateLimitErrorHandler` / `NetworkErrorHandler` - обработка ошибок
- `ModelService` - управление моделями
#### **Удаление статического метода из ChatSession**
**До:** `ChatSession.LoadSystemPrompt()` - нарушал SRP
**После:** Создан `FileSystemPromptProvider` - отдельный сервис для загрузки промптов
#### **Новая структура:**
```
ChatBot/Services/
├── AIService.cs (упрощен)
├── FileSystemPromptProvider.cs
├── InMemorySessionStorage.cs
├── ExponentialBackoffRetryPolicy.cs
├── RandomResponseDelayService.cs
└── ErrorHandlers/
├── RateLimitErrorHandler.cs
└── NetworkErrorHandler.cs
```
---
### 5. **Open/Closed Principle (OCP)**
#### **Strategy Pattern для обработки ошибок**
**До:** Жестко закодированная проверка `if (ex.Message.Contains("429"))`
**После:** Расширяемая система с интерфейсом `IErrorHandler`
```csharp
public interface IErrorHandler
{
bool CanHandle(Exception exception);
Task<ErrorHandlingResult> HandleAsync(...);
}
```
**Реализации:**
- `RateLimitErrorHandler` - обработка HTTP 429
- `NetworkErrorHandler` - сетевые ошибки
**Преимущества:**
- Легко добавить новый обработчик без изменения существующего кода
- Каждый обработчик независим
- Цепочка ответственности (Chain of Responsibility)
---
### 6. **Устранение анти-паттернов**
#### **6.1. Service Locator в CommandRegistry (КРИТИЧНО)**
**До:**
```csharp
// Service Locator - анти-паттерн
var service = serviceProvider.GetService(parameterType);
var command = Activator.CreateInstance(commandType, args);
```
**После:**
```csharp
// Proper Dependency Injection
public CommandRegistry(IEnumerable<ITelegramCommand> commands)
{
foreach (var command in commands)
RegisterCommand(command);
}
```
В `Program.cs`:
```csharp
builder.Services.AddSingleton<ITelegramCommand, StartCommand>();
builder.Services.AddSingleton<ITelegramCommand, HelpCommand>();
builder.Services.AddSingleton<ITelegramCommand, ClearCommand>();
builder.Services.AddSingleton<ITelegramCommand, SettingsCommand>();
```
#### **6.2. Threading Issue в BotInfoService (КРИТИЧНО)**
**До:**
```csharp
lock (_lock) // lock с async - deadlock!
{
var task = _botClient.GetMe();
task.Wait(); // блокировка потока
}
```
**После:**
```csharp
private readonly SemaphoreSlim _semaphore = new(1, 1);
await _semaphore.WaitAsync(cancellationToken);
try
{
_cachedBotInfo = await _botClient.GetMe(...);
}
finally
{
_semaphore.Release();
}
```
**Преимущества:**
- Нет риска deadlock
- Асинхронный код работает правильно
- Поддержка CancellationToken
---
### 7. **FluentValidation**
Добавлены валидаторы для моделей данных:
**Файлы:**
- `ChatBot/Models/Validation/ChatMessageValidator.cs`
- `ChatBot/Models/Configuration/Validators/OllamaSettingsValidator.cs`
- `ChatBot/Models/Configuration/Validators/TelegramBotSettingsValidator.cs`
**Пример:**
```csharp
public class ChatMessageValidator : AbstractValidator<ChatMessage>
{
public ChatMessageValidator()
{
RuleFor(x => x.Content)
.NotEmpty()
.MaximumLength(10000);
RuleFor(x => x.Role)
.Must(role => new[] { "system", "user", "assistant" }.Contains(role));
}
}
```
---
### 8. **Options Pattern Validation**
Валидация конфигурации при старте приложения:
```csharp
builder.Services
.Configure<OllamaSettings>(...)
.AddSingleton<IValidateOptions<OllamaSettings>, OllamaSettingsValidator>()
.ValidateOnStart();
```
**Преимущества:**
- Приложение не стартует с невалидной конфигурацией
- Ошибки конфигурации обнаруживаются сразу
- Детальные сообщения об ошибках
---
### 9. **Health Checks**
Добавлены проверки работоспособности внешних зависимостей:
**Файлы:**
- `ChatBot/Services/HealthChecks/OllamaHealthCheck.cs` - проверка Ollama API
- `ChatBot/Services/HealthChecks/TelegramBotHealthCheck.cs` - проверка Telegram Bot API
**Регистрация:**
```csharp
builder.Services
.AddHealthChecks()
.AddCheck<OllamaHealthCheck>("ollama", tags: new[] { "api", "ollama" })
.AddCheck<TelegramBotHealthCheck>("telegram", tags: new[] { "api", "telegram" });
```
**Преимущества:**
- Мониторинг состояния сервисов
- Быстрое обнаружение проблем
- Интеграция с системами мониторинга
---
### 10. **CancellationToken Support**
Добавлена поддержка отмены операций во всех асинхронных методах:
```csharp
public async Task<string> GenerateChatCompletionAsync(
List<ChatMessage> messages,
int? maxTokens = null,
double? temperature = null,
CancellationToken cancellationToken = default) // ✓
```
**Преимущества:**
- Graceful shutdown приложения
- Отмена долгих операций
- Экономия ресурсов
---
### 11. **Новые пакеты**
Добавлены в `ChatBot.csproj`:
```xml
<PackageReference Include="FluentValidation" Version="11.10.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.10.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
```
---
## 📊 Сравнение "До" и "После"
### **AIService**
**До:** 237 строк, 8 ответственностей
**После:** 104 строки, 1 ответственность (генерация текста)
### **ChatService**
**До:** Зависит от конкретных реализаций
**После:** Зависит только от интерфейсов
### **Program.cs**
**До:** 101 строка, Service Locator
**После:** 149 строк, Proper DI с валидацией и Health Checks
---
## 🎯 Соблюдение SOLID Principles
### ✅ **S - Single Responsibility Principle**
- Каждый класс имеет одну ответственность
- AIService упрощен с 237 до 104 строк
- Логика вынесена в специализированные сервисы
### ✅ **O - Open/Closed Principle**
- Strategy Pattern для обработки ошибок
- Легко добавить новый ErrorHandler без изменения существующего кода
### ✅ **L - Liskov Substitution Principle**
- Все реализации интерфейсов взаимозаменяемы
- Mock-объекты для тестирования
### ✅ **I - Interface Segregation Principle**
- Интерфейсы специфичны и минимальны
- Никто не зависит от методов, которые не использует
### ✅ **D - Dependency Inversion Principle**
- Все зависимости через интерфейсы
- Высокоуровневые модули не зависят от низкоуровневых
---
## 🏗️ Паттерны проектирования
1. **Dependency Injection** - через Microsoft.Extensions.DependencyInjection
2. **Strategy Pattern** - IErrorHandler для разных типов ошибок
3. **Adapter Pattern** - OllamaClientAdapter оборачивает OllamaApiClient
4. **Provider Pattern** - ISystemPromptProvider для загрузки промптов
5. **Repository Pattern** - ISessionStorage для хранения сессий
6. **Command Pattern** - ITelegramCommand для команд бота
7. **Chain of Responsibility** - ErrorHandlingChain для обработки ошибок
---
## 📝 Структура проекта после рефакторинга
```
ChatBot/
├── Common/
│ ├── Constants/
│ │ ├── AIResponseConstants.cs
│ │ ├── ChatRoles.cs
│ │ ├── ChatTypes.cs
│ │ └── RetryConstants.cs
│ └── Results/
│ └── Result.cs
├── Models/
│ ├── Configuration/
│ │ └── Validators/
│ │ ├── OllamaSettingsValidator.cs
│ │ └── TelegramBotSettingsValidator.cs
│ └── Validation/
│ └── ChatMessageValidator.cs
├── Services/
│ ├── Interfaces/
│ │ ├── IAIService.cs
│ │ ├── IErrorHandler.cs
│ │ ├── IOllamaClient.cs
│ │ ├── IResponseDelayService.cs
│ │ ├── IRetryPolicy.cs
│ │ ├── ISessionStorage.cs
│ │ └── ISystemPromptProvider.cs
│ ├── ErrorHandlers/
│ │ ├── RateLimitErrorHandler.cs
│ │ └── NetworkErrorHandler.cs
│ ├── HealthChecks/
│ │ ├── OllamaHealthCheck.cs
│ │ └── TelegramBotHealthCheck.cs
│ ├── AIService.cs (refactored)
│ ├── ChatService.cs (refactored)
│ ├── ExponentialBackoffRetryPolicy.cs
│ ├── FileSystemPromptProvider.cs
│ ├── InMemorySessionStorage.cs
│ ├── OllamaClientAdapter.cs
│ └── RandomResponseDelayService.cs
└── Program.cs (updated)
```
---
## 🚀 Преимущества после рефакторинга
### Для разработки:
- ✅ Код легче читать и понимать
- ✅ Легко добавлять новые функции
- ✅ Проще писать unit-тесты
- ✅ Меньше дублирования кода
### Для поддержки:
- ✅ Проще находить и исправлять баги
- ✅ Изменения не влияют на другие части системы
- ✅ Логи более структурированы
### Для производительности:
- ✅ Нет риска deadlock'ов
- ✅ Правильная работа с async/await
- ✅ Поддержка отмены операций
### Для надежности:
- ✅ Валидация конфигурации при старте
- ✅ Health checks для мониторинга
- ✅ Правильная обработка ошибок
---
## 🔧 Что дальше?
### Рекомендации для дальнейшего развития:
1. **Unit-тесты** - покрыть тестами новые сервисы
2. **Integration тесты** - тестирование с реальными зависимостями
3. **Метрики** - добавить Prometheus metrics
4. **Distributed Tracing** - добавить OpenTelemetry
5. **Circuit Breaker** - для защиты от каскадных ошибок
6. **Rate Limiting** - ограничение запросов к AI
7. **Caching** - кэширование ответов AI
8. **Background Jobs** - для cleanup старых сессий
---
## ✨ Итоги
Проект был полностью отрефакторен согласно принципам SOLID и best practices .NET:
- ✅ 14 задач выполнено
- ✅ 0 критичных проблем
- ✅ Код компилируется без ошибок
- ✅ Следует принципам SOLID
- ✅ Использует современные паттерны
- ✅ Готов к масштабированию и тестированию
**Время выполнения:** ~40 минут
**Файлов создано:** 23
**Файлов изменено:** 8
**Строк кода:** +1500 / -300
🎉 **Проект готов к production использованию!**