This commit is contained in:
Leonid Pershin
2025-10-16 08:08:17 +03:00
parent bf1d1c0770
commit e46013b70b
7 changed files with 113 additions and 56 deletions

View File

@@ -7,6 +7,9 @@ namespace ChatBot.Models
/// </summary>
public class ChatSession
{
private readonly object _lock = new object();
private readonly List<ChatMessage> _messageHistory = new List<ChatMessage>();
/// <summary>
/// Unique identifier for the chat session
/// </summary>
@@ -27,11 +30,6 @@ namespace ChatBot.Models
/// </summary>
public string ChatTitle { get; set; } = string.Empty;
/// <summary>
/// History of messages in this chat session
/// </summary>
public List<ChatMessage> MessageHistory { get; set; } = new List<ChatMessage>();
/// <summary>
/// AI model to use for this session
/// </summary>
@@ -53,29 +51,32 @@ namespace ChatBot.Models
public int MaxHistoryLength { get; set; } = 20;
/// <summary>
/// Add a message to the history and manage history length
/// Add a message to the history and manage history length (thread-safe)
/// </summary>
public void AddMessage(ChatMessage message)
{
MessageHistory.Add(message);
lock (_lock)
{
_messageHistory.Add(message);
LastUpdatedAt = DateTime.UtcNow;
// Trim history if it exceeds max length
if (MessageHistory.Count > MaxHistoryLength)
if (_messageHistory.Count > MaxHistoryLength)
{
// Keep system message if it exists, then keep the most recent messages
var systemMessage = MessageHistory.FirstOrDefault(m => m.Role == "system");
var recentMessages = MessageHistory
var systemMessage = _messageHistory.FirstOrDefault(m => m.Role == "system");
var recentMessages = _messageHistory
.Where(m => m.Role != "system")
.TakeLast(MaxHistoryLength - (systemMessage != null ? 1 : 0))
.ToList();
MessageHistory.Clear();
_messageHistory.Clear();
if (systemMessage != null)
{
MessageHistory.Add(systemMessage);
_messageHistory.Add(systemMessage);
}
_messageHistory.AddRange(recentMessages);
}
MessageHistory.AddRange(recentMessages);
}
}
@@ -102,20 +103,37 @@ namespace ChatBot.Models
}
/// <summary>
/// Get all messages
/// Get all messages (thread-safe)
/// </summary>
public List<ChatMessage> GetAllMessages()
{
return new List<ChatMessage>(MessageHistory);
lock (_lock)
{
return new List<ChatMessage>(_messageHistory);
}
}
/// <summary>
/// Clear message history
/// Get the count of messages in history (thread-safe)
/// </summary>
public int GetMessageCount()
{
lock (_lock)
{
return _messageHistory.Count;
}
}
/// <summary>
/// Clear message history (thread-safe)
/// </summary>
public void ClearHistory()
{
MessageHistory.Clear();
lock (_lock)
{
_messageHistory.Clear();
LastUpdatedAt = DateTime.UtcNow;
}
}
}
}

View File

@@ -14,10 +14,5 @@ namespace ChatBot.Models.Configuration
/// Название модели по умолчанию
/// </summary>
public string DefaultModel { get; set; } = "llama3";
/// <summary>
/// Максимальное количество повторных попыток при ошибках
/// </summary>
public int MaxRetries { get; set; } = 3;
}
}

View File

@@ -12,7 +12,6 @@ namespace ChatBot.Models.Configuration.Validators
var errors = new List<string>();
ValidateUrl(options, errors);
ValidateRetryAndTokenSettings(options, errors);
ValidateDefaultModel(options, errors);
return errors.Count > 0
@@ -28,18 +27,6 @@ namespace ChatBot.Models.Configuration.Validators
errors.Add($"Invalid Ollama URL format: {options.Url}");
}
private static void ValidateRetryAndTokenSettings(
OllamaSettings options,
List<string> errors
)
{
if (options.MaxRetries < 1)
errors.Add($"MaxRetries must be at least 1, got: {options.MaxRetries}");
if (options.MaxRetries > 10)
errors.Add($"MaxRetries should not exceed 10, got: {options.MaxRetries}");
}
private static void ValidateDefaultModel(OllamaSettings options, List<string> errors)
{
if (string.IsNullOrWhiteSpace(options.DefaultModel))

View File

@@ -68,8 +68,13 @@ try
builder.Services.AddSingleton<BotInfoService>();
builder.Services.AddSingleton<ITelegramCommandProcessor, TelegramCommandProcessor>();
builder.Services.AddSingleton<ITelegramMessageHandler, TelegramMessageHandler>();
builder.Services.AddSingleton<ITelegramBotService, TelegramBotService>();
builder.Services.AddHostedService<TelegramBotService>();
// Регистрируем TelegramBotService как singleton и используем один экземпляр для интерфейса и HostedService
builder.Services.AddSingleton<TelegramBotService>();
builder.Services.AddSingleton<ITelegramBotService>(sp =>
sp.GetRequiredService<TelegramBotService>()
);
builder.Services.AddHostedService(sp => sp.GetRequiredService<TelegramBotService>());
// Регистрируем Health Checks
builder

View File

@@ -30,7 +30,7 @@ namespace ChatBot.Services.Telegram.Commands
+ $"Тип чата: {session.ChatType}\n"
+ $"Название: {session.ChatTitle}\n"
+ $"Модель: {session.Model}\n"
+ $"Сообщений в истории: {session.MessageHistory.Count}\n"
+ $"Сообщений в истории: {session.GetMessageCount()}\n"
+ $"Создана: {session.CreatedAt:dd.MM.yyyy HH:mm}"
);
}

View File

@@ -4,7 +4,7 @@ using Telegram.Bot.Types;
namespace ChatBot.Services.Telegram.Services
{
/// <summary>
/// Сервис для получения информации о боте
/// Сервис для получения информации о боте с улучшенным кэшированием
/// </summary>
public class BotInfoService
{
@@ -12,6 +12,8 @@ namespace ChatBot.Services.Telegram.Services
private readonly ILogger<BotInfoService> _logger;
private readonly SemaphoreSlim _semaphore = new(1, 1);
private User? _cachedBotInfo;
private DateTime? _cacheExpirationTime;
private readonly TimeSpan _cacheLifetime = TimeSpan.FromHours(1);
public BotInfoService(ITelegramBotClient botClient, ILogger<BotInfoService> logger)
{
@@ -20,32 +22,60 @@ namespace ChatBot.Services.Telegram.Services
}
/// <summary>
/// Получает информацию о боте (с кешированием)
/// Получает информацию о боте (с кэшированием и автоматической инвалидацией)
/// </summary>
public async Task<User?> GetBotInfoAsync(CancellationToken cancellationToken = default)
{
if (_cachedBotInfo != null)
// Проверяем, есть ли валидный кэш
if (
_cachedBotInfo != null
&& _cacheExpirationTime.HasValue
&& DateTime.UtcNow < _cacheExpirationTime.Value
)
{
return _cachedBotInfo;
}
await _semaphore.WaitAsync(cancellationToken);
try
{
if (_cachedBotInfo != null)
// Double-check после получения блокировки
if (
_cachedBotInfo != null
&& _cacheExpirationTime.HasValue
&& DateTime.UtcNow < _cacheExpirationTime.Value
)
{
return _cachedBotInfo;
}
_logger.LogDebug("Fetching bot info from Telegram API");
_cachedBotInfo = await _botClient.GetMe(cancellationToken: cancellationToken);
_cacheExpirationTime = DateTime.UtcNow.Add(_cacheLifetime);
_logger.LogInformation(
"Bot info loaded: @{BotUsername} (ID: {BotId})",
"Bot info loaded and cached: @{BotUsername} (ID: {BotId}). Cache expires at {ExpirationTime}",
_cachedBotInfo.Username,
_cachedBotInfo.Id
_cachedBotInfo.Id,
_cacheExpirationTime
);
return _cachedBotInfo;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get bot info");
_logger.LogError(ex, "Failed to get bot info from Telegram API");
// Если есть устаревший кэш, используем его
if (_cachedBotInfo != null)
{
_logger.LogWarning(
"Using stale cached bot info due to API error: @{BotUsername}",
_cachedBotInfo.Username
);
return _cachedBotInfo;
}
return null;
}
finally
@@ -53,5 +83,28 @@ namespace ChatBot.Services.Telegram.Services
_semaphore.Release();
}
}
/// <summary>
/// Принудительно инвалидирует кэш информации о боте
/// </summary>
public void InvalidateCache()
{
lock (_semaphore)
{
_cachedBotInfo = null;
_cacheExpirationTime = null;
_logger.LogInformation("Bot info cache invalidated");
}
}
/// <summary>
/// Проверяет, есть ли валидная информация о боте в кэше
/// </summary>
public bool IsCacheValid()
{
return _cachedBotInfo != null
&& _cacheExpirationTime.HasValue
&& DateTime.UtcNow < _cacheExpirationTime.Value;
}
}
}

View File

@@ -33,7 +33,6 @@
},
"Ollama": {
"Url": "http://10.10.1.202:11434",
"DefaultModel": "llama3",
"MaxRetries": 3
"DefaultModel": "llama3chat"
}
}