Add promt fix tests
All checks were successful
SonarQube / Build and analyze (push) Successful in 2m54s

This commit is contained in:
Leonid Pershin
2025-10-21 12:07:56 +03:00
parent ef71568579
commit 1996fec14f
18 changed files with 398 additions and 333 deletions

View File

@@ -10,6 +10,7 @@ namespace ChatBot.Models
public class ChatSession
{
private readonly object _lock = new object();
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
private readonly List<ChatMessage> _messageHistory = new List<ChatMessage>();
private IHistoryCompressionService? _compressionService;
@@ -110,35 +111,41 @@ namespace ChatBot.Models
int compressionTarget
)
{
lock (_lock)
await _semaphore.WaitAsync();
try
{
_messageHistory.Add(message);
LastUpdatedAt = DateTime.UtcNow;
}
// Check if compression is needed and perform it asynchronously
if (
_compressionService != null
&& _compressionService.ShouldCompress(_messageHistory.Count, compressionThreshold)
)
{
await CompressHistoryAsync(compressionTarget);
// Check if compression is needed and perform it asynchronously
if (
_compressionService != null
&& _compressionService.ShouldCompress(_messageHistory.Count, compressionThreshold)
)
{
await CompressHistoryAsync(compressionTarget);
}
else if (_messageHistory.Count > MaxHistoryLength)
{
// Fallback to simple trimming if compression is not available
await TrimHistoryAsync();
}
}
else if (_messageHistory.Count > MaxHistoryLength)
finally
{
// Fallback to simple trimming if compression is not available
await TrimHistoryAsync();
_semaphore.Release();
}
}
/// <summary>
/// Compress message history using the compression service
/// Note: This method should be called within a semaphore lock
/// </summary>
private async Task CompressHistoryAsync(int targetCount)
{
if (_compressionService == null)
{
await TrimHistoryAsync();
TrimHistoryInternal();
return;
}
@@ -149,50 +156,52 @@ namespace ChatBot.Models
targetCount
);
lock (_lock)
{
_messageHistory.Clear();
_messageHistory.AddRange(compressedMessages);
LastUpdatedAt = DateTime.UtcNow;
}
_messageHistory.Clear();
_messageHistory.AddRange(compressedMessages);
LastUpdatedAt = DateTime.UtcNow;
}
catch (Exception)
{
// Log error and fallback to simple trimming
// Note: We can't inject ILogger here, so we'll just fallback
await TrimHistoryAsync();
TrimHistoryInternal();
}
}
/// <summary>
/// Simple history trimming without compression
/// Simple history trimming without compression (async wrapper)
/// Note: This method should be called within a semaphore lock
/// </summary>
private async Task TrimHistoryAsync()
private Task TrimHistoryAsync()
{
await Task.Run(() =>
{
lock (_lock)
{
if (_messageHistory.Count > MaxHistoryLength)
{
var systemMessage = _messageHistory.FirstOrDefault(m =>
m.Role == ChatRole.System
);
var recentMessages = _messageHistory
.Where(m => m.Role != ChatRole.System)
.TakeLast(MaxHistoryLength - (systemMessage != null ? 1 : 0))
.ToList();
TrimHistoryInternal();
return Task.CompletedTask;
}
_messageHistory.Clear();
if (systemMessage != null)
{
_messageHistory.Add(systemMessage);
}
_messageHistory.AddRange(recentMessages);
LastUpdatedAt = DateTime.UtcNow;
}
/// <summary>
/// Internal method to trim history without async overhead
/// Note: This method should be called within a semaphore lock
/// </summary>
private void TrimHistoryInternal()
{
if (_messageHistory.Count > MaxHistoryLength)
{
var systemMessage = _messageHistory.FirstOrDefault(m =>
m.Role == ChatRole.System
);
var recentMessages = _messageHistory
.Where(m => m.Role != ChatRole.System)
.TakeLast(MaxHistoryLength - (systemMessage != null ? 1 : 0))
.ToList();
_messageHistory.Clear();
if (systemMessage != null)
{
_messageHistory.Add(systemMessage);
}
});
_messageHistory.AddRange(recentMessages);
LastUpdatedAt = DateTime.UtcNow;
}
}
/// <summary>

View File

@@ -35,13 +35,13 @@ namespace ChatBot.Services
/// <summary>
/// Get or create a chat session for the given chat ID
/// </summary>
public ChatSession GetOrCreateSession(
public async Task<ChatSession> GetOrCreateSessionAsync(
long chatId,
string chatType = ChatTypes.Private,
string chatTitle = ""
)
{
var session = _sessionStorage.GetOrCreate(chatId, chatType, chatTitle);
var session = await _sessionStorage.GetOrCreateAsync(chatId, chatType, chatTitle);
// Set compression service if compression is enabled
if (_aiSettings.EnableHistoryCompression)
@@ -66,7 +66,7 @@ namespace ChatBot.Services
{
try
{
var session = GetOrCreateSession(chatId, chatType, chatTitle);
var session = await GetOrCreateSessionAsync(chatId, chatType, chatTitle);
// Add user message to history with username
if (_aiSettings.EnableHistoryCompression)
@@ -157,7 +157,7 @@ namespace ChatBot.Services
/// </summary>
public async Task UpdateSessionParametersAsync(long chatId, string? model = null)
{
var session = _sessionStorage.Get(chatId);
var session = await _sessionStorage.GetAsync(chatId);
if (session != null)
{
if (!string.IsNullOrEmpty(model))
@@ -177,7 +177,7 @@ namespace ChatBot.Services
/// </summary>
public virtual async Task ClearHistoryAsync(long chatId)
{
var session = _sessionStorage.Get(chatId);
var session = await _sessionStorage.GetAsync(chatId);
if (session != null)
{
session.ClearHistory();
@@ -192,33 +192,33 @@ namespace ChatBot.Services
/// <summary>
/// Get session information
/// </summary>
public virtual ChatSession? GetSession(long chatId)
public virtual async Task<ChatSession?> GetSessionAsync(long chatId)
{
return _sessionStorage.Get(chatId);
return await _sessionStorage.GetAsync(chatId);
}
/// <summary>
/// Remove a session
/// </summary>
public bool RemoveSession(long chatId)
public async Task<bool> RemoveSessionAsync(long chatId)
{
return _sessionStorage.Remove(chatId);
return await _sessionStorage.RemoveAsync(chatId);
}
/// <summary>
/// Get all active sessions count
/// </summary>
public int GetActiveSessionsCount()
public async Task<int> GetActiveSessionsCountAsync()
{
return _sessionStorage.GetActiveSessionsCount();
return await _sessionStorage.GetActiveSessionsCountAsync();
}
/// <summary>
/// Clean up old sessions (older than specified hours)
/// </summary>
public int CleanupOldSessions(int hoursOld = 24)
public async Task<int> CleanupOldSessionsAsync(int hoursOld = 24)
{
return _sessionStorage.CleanupOldSessions(hoursOld);
return await _sessionStorage.CleanupOldSessionsAsync(hoursOld);
}
}
}

View File

@@ -1,3 +1,4 @@
using ChatBot.Data;
using ChatBot.Data.Interfaces;
using ChatBot.Models;
using ChatBot.Models.Dto;
@@ -15,19 +16,22 @@ namespace ChatBot.Services
private readonly IChatSessionRepository _repository;
private readonly ILogger<DatabaseSessionStorage> _logger;
private readonly IHistoryCompressionService? _compressionService;
private readonly ChatBotDbContext _context;
public DatabaseSessionStorage(
IChatSessionRepository repository,
ILogger<DatabaseSessionStorage> logger,
ChatBotDbContext context,
IHistoryCompressionService? compressionService = null
)
{
_repository = repository;
_logger = logger;
_context = context;
_compressionService = compressionService;
}
public ChatSession GetOrCreate(
public async Task<ChatSession> GetOrCreateAsync(
long chatId,
string chatType = "private",
string chatTitle = ""
@@ -35,10 +39,7 @@ namespace ChatBot.Services
{
try
{
var sessionEntity = _repository
.GetOrCreateAsync(chatId, chatType, chatTitle)
.GetAwaiter()
.GetResult();
var sessionEntity = await _repository.GetOrCreateAsync(chatId, chatType, chatTitle);
return ConvertToChatSession(sessionEntity);
}
catch (Exception ex)
@@ -51,11 +52,11 @@ namespace ChatBot.Services
}
}
public ChatSession? Get(long chatId)
public async Task<ChatSession?> GetAsync(long chatId)
{
try
{
var sessionEntity = _repository.GetByChatIdAsync(chatId).GetAwaiter().GetResult();
var sessionEntity = await _repository.GetByChatIdAsync(chatId);
return sessionEntity != null ? ConvertToChatSession(sessionEntity) : null;
}
catch (Exception ex)
@@ -65,11 +66,11 @@ namespace ChatBot.Services
}
}
public bool Remove(long chatId)
public async Task<bool> RemoveAsync(long chatId)
{
try
{
return _repository.DeleteAsync(chatId).GetAwaiter().GetResult();
return await _repository.DeleteAsync(chatId);
}
catch (Exception ex)
{
@@ -78,11 +79,11 @@ namespace ChatBot.Services
}
}
public int GetActiveSessionsCount()
public async Task<int> GetActiveSessionsCountAsync()
{
try
{
return _repository.GetActiveSessionsCountAsync().GetAwaiter().GetResult();
return await _repository.GetActiveSessionsCountAsync();
}
catch (Exception ex)
{
@@ -91,11 +92,11 @@ namespace ChatBot.Services
}
}
public int CleanupOldSessions(int hoursOld = 24)
public async Task<int> CleanupOldSessionsAsync(int hoursOld = 24)
{
try
{
return _repository.CleanupOldSessionsAsync(hoursOld).GetAwaiter().GetResult();
return await _repository.CleanupOldSessionsAsync(hoursOld);
}
catch (Exception ex)
{
@@ -105,10 +106,11 @@ namespace ChatBot.Services
}
/// <summary>
/// Save session changes to database
/// Save session changes to database with transaction support
/// </summary>
public async Task SaveSessionAsync(ChatSession session)
{
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var sessionEntity = await _repository.GetByChatIdAsync(session.ChatId);
@@ -126,7 +128,7 @@ namespace ChatBot.Services
sessionEntity.MaxHistoryLength = session.MaxHistoryLength;
sessionEntity.LastUpdatedAt = DateTime.UtcNow;
// Clear existing messages and add new ones
// Clear existing messages and add new ones in a transaction
await _repository.ClearMessagesAsync(sessionEntity.Id);
var messages = session.GetAllMessages();
@@ -141,9 +143,14 @@ namespace ChatBot.Services
}
await _repository.UpdateAsync(sessionEntity);
// Commit transaction if all operations succeeded
await transaction.CommitAsync();
}
catch (Exception ex)
{
// Transaction will be automatically rolled back on exception
await transaction.RollbackAsync();
_logger.LogError(ex, "Failed to save session for chat {ChatId}", session.ChatId);
throw new InvalidOperationException(
$"Failed to save session for chat {session.ChatId}",

View File

@@ -17,7 +17,7 @@ namespace ChatBot.Services
_logger = logger;
}
public ChatSession GetOrCreate(
public Task<ChatSession> GetOrCreateAsync(
long chatId,
string chatType = "private",
string chatTitle = ""
@@ -54,31 +54,31 @@ namespace ChatBot.Services
}
}
return session;
return Task.FromResult(session);
}
public ChatSession? Get(long chatId)
public Task<ChatSession?> GetAsync(long chatId)
{
_sessions.TryGetValue(chatId, out var session);
return session;
return Task.FromResult(session);
}
public bool Remove(long chatId)
public Task<bool> RemoveAsync(long chatId)
{
var removed = _sessions.TryRemove(chatId, out _);
if (removed)
{
_logger.LogInformation("Removed session for chat {ChatId}", chatId);
}
return removed;
return Task.FromResult(removed);
}
public int GetActiveSessionsCount()
public Task<int> GetActiveSessionsCountAsync()
{
return _sessions.Count;
return Task.FromResult(_sessions.Count);
}
public int CleanupOldSessions(int hoursOld = 24)
public Task<int> CleanupOldSessionsAsync(int hoursOld = 24)
{
var cutoffTime = DateTime.UtcNow.AddHours(-hoursOld);
var sessionsToRemove = _sessions
@@ -97,7 +97,7 @@ namespace ChatBot.Services
}
_logger.LogInformation("Cleaned up {DeletedCount} old sessions", deletedCount);
return deletedCount;
return Task.FromResult(deletedCount);
}
public Task SaveSessionAsync(ChatSession session)

View File

@@ -10,27 +10,27 @@ namespace ChatBot.Services.Interfaces
/// <summary>
/// Get or create a chat session
/// </summary>
ChatSession GetOrCreate(long chatId, string chatType = "private", string chatTitle = "");
Task<ChatSession> GetOrCreateAsync(long chatId, string chatType = "private", string chatTitle = "");
/// <summary>
/// Get a session by chat ID
/// </summary>
ChatSession? Get(long chatId);
Task<ChatSession?> GetAsync(long chatId);
/// <summary>
/// Remove a session
/// </summary>
bool Remove(long chatId);
Task<bool> RemoveAsync(long chatId);
/// <summary>
/// Get count of active sessions
/// </summary>
int GetActiveSessionsCount();
Task<int> GetActiveSessionsCountAsync();
/// <summary>
/// Clean up old sessions
/// </summary>
int CleanupOldSessions(int hoursOld = 24);
Task<int> CleanupOldSessionsAsync(int hoursOld = 24);
/// <summary>
/// Save session changes to storage (for database implementations)

View File

@@ -24,17 +24,15 @@ namespace ChatBot.Services.Telegram.Commands
public override string CommandName => "/settings";
public override string Description => "Показать настройки чата";
public override Task<string> ExecuteAsync(
public override async Task<string> ExecuteAsync(
TelegramCommandContext context,
CancellationToken cancellationToken = default
)
{
var session = _chatService.GetSession(context.ChatId);
var session = await _chatService.GetSessionAsync(context.ChatId);
if (session == null)
{
return Task.FromResult(
"Сессия не найдена. Отправьте любое сообщение для создания новой сессии."
);
return "Сессия не найдена. Отправьте любое сообщение для создания новой сессии.";
}
var compressionStatus = _aiSettings.EnableHistoryCompression ? "Включено" : "Отключено";
@@ -42,15 +40,13 @@ namespace ChatBot.Services.Telegram.Commands
? $"\nСжатие истории: {compressionStatus}\nПорог сжатия: {_aiSettings.CompressionThreshold} сообщений\nЦелевое количество: {_aiSettings.CompressionTarget} сообщений"
: $"\nСжатие истории: {compressionStatus}";
return Task.FromResult(
$"Настройки чата:\n"
+ $"Тип чата: {session.ChatType}\n"
+ $"Название: {session.ChatTitle}\n"
+ $"Модель: {session.Model}\n"
+ $"Сообщений в истории: {session.GetMessageCount()}\n"
+ $"Создана: {session.CreatedAt:dd.MM.yyyy HH:mm}"
+ compressionInfo
);
return $"Настройки чата:\n"
+ $"Тип чата: {session.ChatType}\n"
+ $"Название: {session.ChatTitle}\n"
+ $"Модель: {session.Model}\n"
+ $"Сообщений в истории: {session.GetMessageCount()}\n"
+ $"Создана: {session.CreatedAt:dd.MM.yyyy HH:mm}"
+ compressionInfo;
}
}
}

View File

@@ -40,7 +40,7 @@ namespace ChatBot.Services.Telegram.Commands
statusBuilder.AppendLine();
// Информация о сессии
var session = _chatService.GetSession(context.ChatId);
var session = await _chatService.GetSessionAsync(context.ChatId);
if (session != null)
{
statusBuilder.AppendLine($"📊 **Сессия:**");

View File

@@ -39,6 +39,23 @@ namespace ChatBot.Services.Telegram.Commands
CancellationToken cancellationToken = default
)
{
// Input validation
if (string.IsNullOrWhiteSpace(messageText))
{
_logger.LogWarning("Empty message received for chat {ChatId}", chatId);
return string.Empty;
}
if (chatId == 0)
{
_logger.LogError("Invalid chatId (0) received");
throw new ArgumentException("ChatId cannot be 0", nameof(chatId));
}
username = username ?? "Unknown";
chatType = chatType ?? "private";
chatTitle = chatTitle ?? string.Empty;
try
{
// Получаем информацию о боте
@@ -121,10 +138,25 @@ namespace ChatBot.Services.Telegram.Commands
cancellationToken
);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Network error processing message for chat {ChatId}", chatId);
return "Ошибка сети при обработке сообщения. Проверьте подключение к AI сервису.";
}
catch (TaskCanceledException ex)
{
_logger.LogWarning(ex, "Request timeout for chat {ChatId}", chatId);
return "Превышено время ожидания ответа. Попробуйте еще раз.";
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "Invalid operation for chat {ChatId}", chatId);
return "Ошибка в работе системы. Попробуйте позже.";
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing message for chat {ChatId}", chatId);
return "Произошла ошибка при обработке сообщения. Попробуйте еще раз.";
_logger.LogError(ex, "Unexpected error processing message for chat {ChatId}", chatId);
return "Произошла непредвиденная ошибка. Попробуйте еще раз.";
}
}
}