clear
This commit is contained in:
@@ -17,8 +17,6 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
<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" />
|
||||||
<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" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
using ChatBot.Models.Configuration;
|
using ChatBot.Models.Configuration;
|
||||||
using ChatBot.Models.Configuration.Validators;
|
using ChatBot.Models.Configuration.Validators;
|
||||||
using ChatBot.Models.Validation;
|
|
||||||
using ChatBot.Services;
|
using ChatBot.Services;
|
||||||
using ChatBot.Services.ErrorHandlers;
|
|
||||||
using ChatBot.Services.HealthChecks;
|
using ChatBot.Services.HealthChecks;
|
||||||
using ChatBot.Services.Interfaces;
|
using ChatBot.Services.Interfaces;
|
||||||
using ChatBot.Services.Telegram.Commands;
|
using ChatBot.Services.Telegram.Commands;
|
||||||
using ChatBot.Services.Telegram.Interfaces;
|
using ChatBot.Services.Telegram.Interfaces;
|
||||||
using ChatBot.Services.Telegram.Services;
|
using ChatBot.Services.Telegram.Services;
|
||||||
using FluentValidation;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
@@ -26,8 +23,6 @@ try
|
|||||||
builder.Services.AddSerilog();
|
builder.Services.AddSerilog();
|
||||||
|
|
||||||
// Конфигурируем настройки с валидацией
|
// Конфигурируем настройки с валидацией
|
||||||
builder.Services.Configure<AppSettings>(builder.Configuration);
|
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.Services.Configure<TelegramBotSettings>(builder.Configuration.GetSection("TelegramBot"))
|
.Services.Configure<TelegramBotSettings>(builder.Configuration.GetSection("TelegramBot"))
|
||||||
.AddSingleton<IValidateOptions<TelegramBotSettings>, TelegramBotSettingsValidator>();
|
.AddSingleton<IValidateOptions<TelegramBotSettings>, TelegramBotSettingsValidator>();
|
||||||
@@ -36,36 +31,10 @@ try
|
|||||||
.Services.Configure<OllamaSettings>(builder.Configuration.GetSection("Ollama"))
|
.Services.Configure<OllamaSettings>(builder.Configuration.GetSection("Ollama"))
|
||||||
.AddSingleton<IValidateOptions<OllamaSettings>, OllamaSettingsValidator>();
|
.AddSingleton<IValidateOptions<OllamaSettings>, OllamaSettingsValidator>();
|
||||||
|
|
||||||
builder.Services.Configure<SerilogSettings>(builder.Configuration.GetSection("Serilog"));
|
|
||||||
|
|
||||||
// Валидируем конфигурацию при старте
|
// Валидируем конфигурацию при старте
|
||||||
builder.Services.AddOptions<TelegramBotSettings>().ValidateOnStart();
|
builder.Services.AddOptions<TelegramBotSettings>().ValidateOnStart();
|
||||||
builder.Services.AddOptions<OllamaSettings>().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
|
// Регистрируем IOllamaClient
|
||||||
builder.Services.AddSingleton<IOllamaClient>(sp =>
|
builder.Services.AddSingleton<IOllamaClient>(sp =>
|
||||||
{
|
{
|
||||||
@@ -76,13 +45,6 @@ try
|
|||||||
// Регистрируем интерфейсы и сервисы
|
// Регистрируем интерфейсы и сервисы
|
||||||
builder.Services.AddSingleton<ISessionStorage, InMemorySessionStorage>();
|
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<ModelService>();
|
||||||
builder.Services.AddSingleton<IAIService, AIService>();
|
builder.Services.AddSingleton<IAIService, AIService>();
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
using ChatBot.Models;
|
using ChatBot.Models;
|
||||||
using ChatBot.Services.Interfaces;
|
using ChatBot.Services.Interfaces;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
|
|
||||||
namespace ChatBot.Services
|
namespace ChatBot.Services
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,24 +15,20 @@ namespace ChatBot.Services.Telegram.Services
|
|||||||
{
|
{
|
||||||
private readonly ILogger<TelegramBotService> _logger;
|
private readonly ILogger<TelegramBotService> _logger;
|
||||||
private readonly ITelegramBotClient _botClient;
|
private readonly ITelegramBotClient _botClient;
|
||||||
private readonly TelegramBotSettings _telegramBotSettings;
|
|
||||||
private readonly ITelegramMessageHandler _messageHandler;
|
private readonly ITelegramMessageHandler _messageHandler;
|
||||||
private readonly ITelegramErrorHandler _errorHandler;
|
private readonly ITelegramErrorHandler _errorHandler;
|
||||||
|
|
||||||
public TelegramBotService(
|
public TelegramBotService(
|
||||||
ILogger<TelegramBotService> logger,
|
ILogger<TelegramBotService> logger,
|
||||||
IOptions<TelegramBotSettings> telegramBotSettings,
|
ITelegramBotClient botClient,
|
||||||
ITelegramMessageHandler messageHandler,
|
ITelegramMessageHandler messageHandler,
|
||||||
ITelegramErrorHandler errorHandler
|
ITelegramErrorHandler errorHandler
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_telegramBotSettings = telegramBotSettings.Value;
|
_botClient = botClient;
|
||||||
_messageHandler = messageHandler;
|
_messageHandler = messageHandler;
|
||||||
_errorHandler = errorHandler;
|
_errorHandler = errorHandler;
|
||||||
|
|
||||||
ValidateConfiguration();
|
|
||||||
_botClient = new TelegramBotClient(_telegramBotSettings.BotToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -98,15 +94,5 @@ namespace ChatBot.Services.Telegram.Services
|
|||||||
await Task.Delay(1000, stoppingToken);
|
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
248
README.md
@@ -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
|
|
||||||
|
|||||||
@@ -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 использованию!**
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user