From 5ce721970356795fee7f7dfc1c9305d4014af780 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 17 Oct 2025 02:36:47 +0300 Subject: [PATCH] fix errors --- .../20251016214154_InitialCreate.cs | 127 ++++++++++----- ChatBot/Services/HistoryCompressionService.cs | 152 ++++++++++-------- .../Telegram/Commands/TelegramCommandBase.cs | 4 +- 3 files changed, 179 insertions(+), 104 deletions(-) diff --git a/ChatBot/Migrations/20251016214154_InitialCreate.cs b/ChatBot/Migrations/20251016214154_InitialCreate.cs index 5d1b90e..1f433e2 100644 --- a/ChatBot/Migrations/20251016214154_InitialCreate.cs +++ b/ChatBot/Migrations/20251016214154_InitialCreate.cs @@ -9,40 +9,88 @@ namespace ChatBot.Migrations /// public partial class InitialCreate : Migration { + private const string ChatSessionsTableName = "chat_sessions"; + private const string ChatMessagesTableName = "chat_messages"; + private const string ChatSessionsIdColumn = "id"; + private const string ChatMessagesSessionIdColumn = "session_id"; + private const string IntegerType = "integer"; + /// protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( - name: "chat_sessions", + name: ChatSessionsTableName, columns: table => new { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - session_id = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + id = table + .Column(type: IntegerType, nullable: false) + .Annotation( + "Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.IdentityByDefaultColumn + ), + session_id = table.Column( + type: "character varying(50)", + maxLength: 50, + nullable: false + ), chat_id = table.Column(type: "bigint", nullable: false), - chat_type = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - chat_title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - model = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - created_at = table.Column(type: "timestamp with time zone", nullable: false), - last_updated_at = table.Column(type: "timestamp with time zone", nullable: false), - max_history_length = table.Column(type: "integer", nullable: false) + chat_type = table.Column( + type: "character varying(20)", + maxLength: 20, + nullable: false + ), + chat_title = table.Column( + type: "character varying(200)", + maxLength: 200, + nullable: false + ), + model = table.Column( + type: "character varying(100)", + maxLength: 100, + nullable: false + ), + created_at = table.Column( + type: "timestamp with time zone", + nullable: false + ), + last_updated_at = table.Column( + type: "timestamp with time zone", + nullable: false + ), + max_history_length = table.Column(type: IntegerType, nullable: false), }, constraints: table => { table.PrimaryKey("PK_chat_sessions", x => x.id); - }); + } + ); migrationBuilder.CreateTable( - name: "chat_messages", + name: ChatMessagesTableName, columns: table => new { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - session_id = table.Column(type: "integer", nullable: false), - content = table.Column(type: "character varying(10000)", maxLength: 10000, nullable: false), - role = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - created_at = table.Column(type: "timestamp with time zone", nullable: false), - message_order = table.Column(type: "integer", nullable: false) + id = table + .Column(type: IntegerType, nullable: false) + .Annotation( + "Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.IdentityByDefaultColumn + ), + session_id = table.Column(type: IntegerType, nullable: false), + content = table.Column( + type: "character varying(10000)", + maxLength: 10000, + nullable: false + ), + role = table.Column( + type: "character varying(20)", + maxLength: 20, + nullable: false + ), + created_at = table.Column( + type: "timestamp with time zone", + nullable: false + ), + message_order = table.Column(type: IntegerType, nullable: false), }, constraints: table => { @@ -50,46 +98,51 @@ namespace ChatBot.Migrations table.ForeignKey( name: "FK_chat_messages_chat_sessions_session_id", column: x => x.session_id, - principalTable: "chat_sessions", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); + principalTable: ChatSessionsTableName, + principalColumn: ChatSessionsIdColumn, + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateIndex( name: "IX_chat_messages_created_at", - table: "chat_messages", - column: "created_at"); + table: ChatMessagesTableName, + column: "created_at" + ); migrationBuilder.CreateIndex( name: "IX_chat_messages_session_id", - table: "chat_messages", - column: "session_id"); + table: ChatMessagesTableName, + column: ChatMessagesSessionIdColumn + ); migrationBuilder.CreateIndex( name: "IX_chat_messages_session_id_message_order", - table: "chat_messages", - columns: new[] { "session_id", "message_order" }); + table: ChatMessagesTableName, + columns: new[] { ChatMessagesSessionIdColumn, "message_order" } + ); migrationBuilder.CreateIndex( name: "IX_chat_sessions_chat_id", - table: "chat_sessions", - column: "chat_id"); + table: ChatSessionsTableName, + column: "chat_id" + ); migrationBuilder.CreateIndex( name: "IX_chat_sessions_session_id", - table: "chat_sessions", + table: ChatSessionsTableName, column: "session_id", - unique: true); + unique: true + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "chat_messages"); + migrationBuilder.DropTable(name: ChatMessagesTableName); - migrationBuilder.DropTable( - name: "chat_sessions"); + migrationBuilder.DropTable(name: ChatSessionsTableName); } } } diff --git a/ChatBot/Services/HistoryCompressionService.cs b/ChatBot/Services/HistoryCompressionService.cs index 797b41b..ac50c91 100644 --- a/ChatBot/Services/HistoryCompressionService.cs +++ b/ChatBot/Services/HistoryCompressionService.cs @@ -288,94 +288,116 @@ namespace ChatBot.Services CancellationToken cancellationToken ) { - const int maxRetries = 2; // Fewer retries for summarization to avoid delays + const int maxRetries = 2; for (int attempt = 1; attempt <= maxRetries; attempt++) { try { - var chatRequest = new OllamaSharp.Models.Chat.ChatRequest - { - Messages = messages - .Select(m => new OllamaSharp.Models.Chat.Message(m.Role, m.Content)) - .ToList(), - Stream = false, - Options = new OllamaSharp.Models.RequestOptions - { - Temperature = 0.3f, // Lower temperature for more focused summaries - }, - }; - - // Create timeout cancellation token for compression operations - using var timeoutCts = new CancellationTokenSource( - TimeSpan.FromSeconds(_aiSettings.CompressionTimeoutSeconds) - ); - using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( - cancellationToken, - timeoutCts.Token - ); - - var response = new StringBuilder(); - await foreach ( - var chatResponse in _ollamaClient - .ChatAsync(chatRequest) - .WithCancellation(combinedCts.Token) - ) - { - if (chatResponse?.Message?.Content != null) - { - response.Append(chatResponse.Message.Content); - } - } - - var result = response.ToString().Trim(); - return result.Length > _aiSettings.MaxSummarizedMessageLength - ? result.Substring(0, _aiSettings.MaxSummarizedMessageLength) + "..." - : result; + return await TryGenerateSummaryAsync(messages, cancellationToken); } catch (OperationCanceledException ex) when (ex.InnerException is TaskCanceledException) { - _logger.LogWarning( - ex, - "Compression operation timed out after {TimeoutSeconds} seconds on attempt {Attempt}", - _aiSettings.CompressionTimeoutSeconds, - attempt - ); - if (attempt == maxRetries) - { + if (HandleTimeoutAsync(attempt, maxRetries, ex)) return string.Empty; - } } catch (HttpRequestException ex) when (attempt < maxRetries) { - var delay = 1000 * attempt; // Simple linear backoff for summarization - _logger.LogWarning( - ex, - "Failed to generate AI summary on attempt {Attempt}/{MaxAttempts}. Retrying in {DelayMs}ms...", - attempt, - maxRetries, - delay - ); - await Task.Delay(delay, cancellationToken); + await HandleHttpExceptionAsync(attempt, maxRetries, ex, cancellationToken); } catch (Exception ex) { - _logger.LogWarning( - ex, - "Failed to generate AI summary on attempt {Attempt}", - attempt - ); - if (attempt == maxRetries) - { + if (HandleGenericExceptionAsync(attempt, maxRetries, ex)) return string.Empty; - } } } return string.Empty; } + private async Task TryGenerateSummaryAsync( + List messages, + CancellationToken cancellationToken + ) + { + var chatRequest = CreateSummaryRequest(messages); + using var timeoutCts = new CancellationTokenSource( + TimeSpan.FromSeconds(_aiSettings.CompressionTimeoutSeconds) + ); + using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeoutCts.Token + ); + + var response = new StringBuilder(); + await foreach ( + var chatResponse in _ollamaClient + .ChatAsync(chatRequest) + .WithCancellation(combinedCts.Token) + ) + { + if (chatResponse?.Message?.Content != null) + { + response.Append(chatResponse.Message.Content); + } + } + + var result = response.ToString().Trim(); + return result.Length > _aiSettings.MaxSummarizedMessageLength + ? result.Substring(0, _aiSettings.MaxSummarizedMessageLength) + "..." + : result; + } + + private static OllamaSharp.Models.Chat.ChatRequest CreateSummaryRequest( + List messages + ) + { + return new OllamaSharp.Models.Chat.ChatRequest + { + Messages = messages + .Select(m => new OllamaSharp.Models.Chat.Message(m.Role, m.Content)) + .ToList(), + Stream = false, + Options = new OllamaSharp.Models.RequestOptions { Temperature = 0.3f }, + }; + } + + private bool HandleTimeoutAsync(int attempt, int maxRetries, OperationCanceledException ex) + { + _logger.LogWarning( + ex, + "Compression operation timed out after {TimeoutSeconds} seconds on attempt {Attempt}", + _aiSettings.CompressionTimeoutSeconds, + attempt + ); + return attempt == maxRetries; + } + + private async Task HandleHttpExceptionAsync( + int attempt, + int maxRetries, + HttpRequestException ex, + CancellationToken cancellationToken + ) + { + var delay = 1000 * attempt; + _logger.LogWarning( + ex, + "Failed to generate AI summary on attempt {Attempt}/{MaxAttempts}. Retrying in {DelayMs}ms...", + attempt, + maxRetries, + delay + ); + await Task.Delay(delay, cancellationToken); + } + + private bool HandleGenericExceptionAsync(int attempt, int maxRetries, Exception ex) + { + _logger.LogWarning(ex, "Failed to generate AI summary on attempt {Attempt}", attempt); + return attempt == maxRetries; + } + /// /// Check if a single message should be kept (not too short, not too long) /// diff --git a/ChatBot/Services/Telegram/Commands/TelegramCommandBase.cs b/ChatBot/Services/Telegram/Commands/TelegramCommandBase.cs index 1b7e40d..8264add 100644 --- a/ChatBot/Services/Telegram/Commands/TelegramCommandBase.cs +++ b/ChatBot/Services/Telegram/Commands/TelegramCommandBase.cs @@ -53,7 +53,7 @@ namespace ChatBot.Services.Telegram.Commands /// /// Проверяет, есть ли аргументы у команды /// - protected bool HasArguments(TelegramCommandContext context) + protected static bool HasArguments(TelegramCommandContext context) { return !string.IsNullOrWhiteSpace(context.Arguments); } @@ -61,7 +61,7 @@ namespace ChatBot.Services.Telegram.Commands /// /// Получает аргументы команды /// - protected string GetArguments(TelegramCommandContext context) + protected static string GetArguments(TelegramCommandContext context) { return context.Arguments; }