diff --git a/ChatBot/.gitea/workflows/build.yml b/ChatBot/.gitea/workflows/build.yml
new file mode 100644
index 0000000..e3e2cee
--- /dev/null
+++ b/ChatBot/.gitea/workflows/build.yml
@@ -0,0 +1,89 @@
+name: ChatBot CI/CD
+on:
+ push:
+ branches:
+ - master
+ - main
+ pull_request:
+ types: [opened, synchronize, reopened]
+jobs:
+ build:
+ name: Build and Test
+ runs-on: windows-latest
+ services:
+ postgres:
+ image: postgres:15
+ env:
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_USER: postgres
+ POSTGRES_DB: chatbot_test
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 5432:5432
+ steps:
+ - name: Set up JDK 17 (for SonarQube)
+ uses: actions/setup-java@v4
+ with:
+ java-version: 17
+ distribution: 'zulu'
+
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '9.0.x'
+
+ - name: Cache SonarQube Cloud packages
+ uses: actions/cache@v4
+ with:
+ path: ~\sonar\cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+
+ - name: Cache SonarQube Cloud scanner
+ id: cache-sonar-scanner
+ uses: actions/cache@v4
+ with:
+ path: ${{ runner.temp }}\scanner
+ key: ${{ runner.os }}-sonar-scanner
+ restore-keys: ${{ runner.os }}-sonar-scanner
+
+ - name: Install SonarQube Cloud scanner
+ if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
+ shell: powershell
+ run: |
+ New-Item -Path ${{ runner.temp }}\scanner -ItemType Directory
+ dotnet tool update dotnet-sonarscanner --tool-path ${{ runner.temp }}\scanner
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build project
+ run: dotnet build --no-restore --configuration Release
+
+ - name: Run tests
+ run: dotnet test --no-build --configuration Release --verbosity normal
+ env:
+ ConnectionStrings__DefaultConnection: "Host=localhost;Port=5432;Database=chatbot_test;Username=postgres;Password=postgres"
+
+ - name: Run database migrations
+ run: dotnet ef database update --context ChatBotDbContext --no-build
+ env:
+ ConnectionStrings__DefaultConnection: "Host=localhost;Port=5432;Database=chatbot_test;Username=postgres;Password=postgres"
+
+ - name: Code analysis with SonarQube
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ shell: powershell
+ run: |
+ ${{ runner.temp }}\scanner\dotnet-sonarscanner begin /k:"mrleo1nid_chatbot" /o:"mrleo1nid" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml"
+ dotnet build --configuration Release
+ ${{ runner.temp }}\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
\ No newline at end of file
diff --git a/ChatBot/ChatBot.csproj b/ChatBot/ChatBot.csproj
index 9c59d25..59a997a 100644
--- a/ChatBot/ChatBot.csproj
+++ b/ChatBot/ChatBot.csproj
@@ -18,6 +18,12 @@
+
+
+
+
+
+
diff --git a/ChatBot/Data/ChatBotDbContext.cs b/ChatBot/Data/ChatBotDbContext.cs
new file mode 100644
index 0000000..8098dbd
--- /dev/null
+++ b/ChatBot/Data/ChatBotDbContext.cs
@@ -0,0 +1,78 @@
+using ChatBot.Models.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace ChatBot.Data
+{
+ ///
+ /// DbContext for ChatBot application
+ ///
+ public class ChatBotDbContext : DbContext
+ {
+ public ChatBotDbContext(DbContextOptions options)
+ : base(options) { }
+
+ ///
+ /// Chat sessions table
+ ///
+ public DbSet ChatSessions { get; set; }
+
+ ///
+ /// Chat messages table
+ ///
+ public DbSet ChatMessages { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+
+ // Configure ChatSessionEntity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+ entity.Property(e => e.SessionId).IsRequired().HasMaxLength(50);
+ entity.Property(e => e.ChatId).IsRequired();
+ entity.Property(e => e.ChatType).IsRequired().HasMaxLength(20);
+ entity.Property(e => e.ChatTitle).HasMaxLength(200);
+ entity.Property(e => e.Model).HasMaxLength(100);
+ entity.Property(e => e.CreatedAt).IsRequired();
+ entity.Property(e => e.LastUpdatedAt).IsRequired();
+
+ // Unique constraint on SessionId
+ entity.HasIndex(e => e.SessionId).IsUnique();
+
+ // Index on ChatId for fast lookups
+ entity.HasIndex(e => e.ChatId);
+
+ // One-to-many relationship with messages
+ entity
+ .HasMany(e => e.Messages)
+ .WithOne(e => e.Session)
+ .HasForeignKey(e => e.SessionId)
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ // Configure ChatMessageEntity
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+ entity.Property(e => e.SessionId).IsRequired();
+ entity.Property(e => e.Content).IsRequired().HasMaxLength(10000);
+ entity.Property(e => e.Role).IsRequired().HasMaxLength(20);
+ entity.Property(e => e.CreatedAt).IsRequired();
+
+ // Index on SessionId for fast lookups
+ entity.HasIndex(e => e.SessionId);
+
+ // Index on CreatedAt for ordering
+ entity.HasIndex(e => e.CreatedAt);
+
+ // Composite index for efficient querying by session and order
+ entity.HasIndex(e => new { e.SessionId, e.MessageOrder });
+ });
+
+ // Configure table names to use snake_case
+ modelBuilder.Entity().ToTable("chat_sessions");
+ modelBuilder.Entity().ToTable("chat_messages");
+ }
+ }
+}
diff --git a/ChatBot/Data/Interfaces/IChatSessionRepository.cs b/ChatBot/Data/Interfaces/IChatSessionRepository.cs
new file mode 100644
index 0000000..56c8301
--- /dev/null
+++ b/ChatBot/Data/Interfaces/IChatSessionRepository.cs
@@ -0,0 +1,74 @@
+using ChatBot.Models.Entities;
+
+namespace ChatBot.Data.Interfaces
+{
+ ///
+ /// Repository interface for chat session operations
+ ///
+ public interface IChatSessionRepository
+ {
+ ///
+ /// Get or create a chat session by chat ID
+ ///
+ Task GetOrCreateAsync(
+ long chatId,
+ string chatType = "private",
+ string chatTitle = ""
+ );
+
+ ///
+ /// Get a chat session by chat ID
+ ///
+ Task GetByChatIdAsync(long chatId);
+
+ ///
+ /// Get a chat session by session ID
+ ///
+ Task GetBySessionIdAsync(string sessionId);
+
+ ///
+ /// Update a chat session
+ ///
+ Task UpdateAsync(ChatSessionEntity session);
+
+ ///
+ /// Delete a chat session
+ ///
+ Task DeleteAsync(long chatId);
+
+ ///
+ /// Get all messages for a session
+ ///
+ Task> GetMessagesAsync(int sessionId);
+
+ ///
+ /// Add a message to a session
+ ///
+ Task AddMessageAsync(
+ int sessionId,
+ string content,
+ string role,
+ int messageOrder
+ );
+
+ ///
+ /// Clear all messages for a session
+ ///
+ Task ClearMessagesAsync(int sessionId);
+
+ ///
+ /// Get count of active sessions
+ ///
+ Task GetActiveSessionsCountAsync();
+
+ ///
+ /// Clean up old sessions
+ ///
+ Task CleanupOldSessionsAsync(int hoursOld = 24);
+
+ ///
+ /// Get sessions that need cleanup
+ ///
+ Task> GetSessionsForCleanupAsync(int hoursOld = 24);
+ }
+}
diff --git a/ChatBot/Data/Repositories/ChatSessionRepository.cs b/ChatBot/Data/Repositories/ChatSessionRepository.cs
new file mode 100644
index 0000000..c3e129f
--- /dev/null
+++ b/ChatBot/Data/Repositories/ChatSessionRepository.cs
@@ -0,0 +1,171 @@
+using ChatBot.Data.Interfaces;
+using ChatBot.Models.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace ChatBot.Data.Repositories
+{
+ ///
+ /// Repository implementation for chat session operations
+ ///
+ public class ChatSessionRepository : IChatSessionRepository
+ {
+ private readonly ChatBotDbContext _context;
+ private readonly ILogger _logger;
+
+ public ChatSessionRepository(
+ ChatBotDbContext context,
+ ILogger logger
+ )
+ {
+ _context = context;
+ _logger = logger;
+ }
+
+ public async Task GetOrCreateAsync(
+ long chatId,
+ string chatType = "private",
+ string chatTitle = ""
+ )
+ {
+ var session = await GetByChatIdAsync(chatId);
+
+ if (session == null)
+ {
+ session = new ChatSessionEntity
+ {
+ SessionId = Guid.NewGuid().ToString(),
+ ChatId = chatId,
+ ChatType = chatType,
+ ChatTitle = chatTitle,
+ Model = string.Empty, // Will be set by ModelService
+ CreatedAt = DateTime.UtcNow,
+ LastUpdatedAt = DateTime.UtcNow,
+ MaxHistoryLength = 30,
+ };
+
+ _context.ChatSessions.Add(session);
+ await _context.SaveChangesAsync();
+
+ _logger.LogInformation(
+ "Created new chat session for chat {ChatId}, type: {ChatType}, title: {ChatTitle}",
+ chatId,
+ chatType,
+ chatTitle
+ );
+ }
+
+ return session;
+ }
+
+ public async Task GetByChatIdAsync(long chatId)
+ {
+ return await _context
+ .ChatSessions.Include(s => s.Messages.OrderBy(m => m.MessageOrder))
+ .FirstOrDefaultAsync(s => s.ChatId == chatId);
+ }
+
+ public async Task GetBySessionIdAsync(string sessionId)
+ {
+ return await _context
+ .ChatSessions.Include(s => s.Messages.OrderBy(m => m.MessageOrder))
+ .FirstOrDefaultAsync(s => s.SessionId == sessionId);
+ }
+
+ public async Task UpdateAsync(ChatSessionEntity session)
+ {
+ session.LastUpdatedAt = DateTime.UtcNow;
+ _context.ChatSessions.Update(session);
+ await _context.SaveChangesAsync();
+ return session;
+ }
+
+ public async Task DeleteAsync(long chatId)
+ {
+ var session = await _context.ChatSessions.FirstOrDefaultAsync(s => s.ChatId == chatId);
+ if (session == null)
+ return false;
+
+ _context.ChatSessions.Remove(session);
+ await _context.SaveChangesAsync();
+
+ _logger.LogInformation("Deleted session for chat {ChatId}", chatId);
+ return true;
+ }
+
+ public async Task> GetMessagesAsync(int sessionId)
+ {
+ return await _context
+ .ChatMessages.Where(m => m.SessionId == sessionId)
+ .OrderBy(m => m.MessageOrder)
+ .ToListAsync();
+ }
+
+ public async Task AddMessageAsync(
+ int sessionId,
+ string content,
+ string role,
+ int messageOrder
+ )
+ {
+ var message = new ChatMessageEntity
+ {
+ SessionId = sessionId,
+ Content = content,
+ Role = role,
+ MessageOrder = messageOrder,
+ CreatedAt = DateTime.UtcNow,
+ };
+
+ _context.ChatMessages.Add(message);
+ await _context.SaveChangesAsync();
+ return message;
+ }
+
+ public async Task ClearMessagesAsync(int sessionId)
+ {
+ var messages = await _context
+ .ChatMessages.Where(m => m.SessionId == sessionId)
+ .ToListAsync();
+
+ _context.ChatMessages.RemoveRange(messages);
+ await _context.SaveChangesAsync();
+
+ _logger.LogInformation(
+ "Cleared {Count} messages for session {SessionId}",
+ messages.Count,
+ sessionId
+ );
+ }
+
+ public async Task GetActiveSessionsCountAsync()
+ {
+ return await _context.ChatSessions.CountAsync();
+ }
+
+ public async Task CleanupOldSessionsAsync(int hoursOld = 24)
+ {
+ var cutoffTime = DateTime.UtcNow.AddHours(-hoursOld);
+ var sessionsToRemove = await _context
+ .ChatSessions.Where(s => s.LastUpdatedAt < cutoffTime)
+ .ToListAsync();
+
+ if (sessionsToRemove.Any())
+ {
+ _context.ChatSessions.RemoveRange(sessionsToRemove);
+ await _context.SaveChangesAsync();
+
+ _logger.LogInformation("Cleaned up {Count} old sessions", sessionsToRemove.Count);
+ }
+
+ return sessionsToRemove.Count;
+ }
+
+ public async Task> GetSessionsForCleanupAsync(int hoursOld = 24)
+ {
+ var cutoffTime = DateTime.UtcNow.AddHours(-hoursOld);
+ return await _context
+ .ChatSessions.Where(s => s.LastUpdatedAt < cutoffTime)
+ .ToListAsync();
+ }
+ }
+}
diff --git a/ChatBot/HISTORY_COMPRESSION.md b/ChatBot/HISTORY_COMPRESSION.md
deleted file mode 100644
index 5dc8d73..0000000
--- a/ChatBot/HISTORY_COMPRESSION.md
+++ /dev/null
@@ -1,86 +0,0 @@
-# Система постепенного сжатия истории сообщений
-
-## Обзор
-
-Реализована система постепенного сжатия истории сообщений для оптимизации использования памяти и улучшения производительности чат-бота. Система автоматически сжимает старые сообщения, сохраняя при этом важную информацию.
-
-## Основные возможности
-
-### 1. Автоматическое сжатие
-- Сжатие активируется при превышении порогового количества сообщений
-- Старые сообщения группируются и сжимаются в краткие резюме
-- Системные сообщения всегда сохраняются
-- Последние сообщения остаются без изменений
-
-### 2. Умная суммаризация
-- Использование ИИ для создания кратких резюме старых сообщений
-- Раздельная обработка сообщений пользователя и ассистента
-- Сохранение ключевой информации при сжатии
-
-### 3. Настраиваемые параметры
-- `EnableHistoryCompression` - включение/отключение сжатия
-- `CompressionThreshold` - порог активации сжатия (по умолчанию 20 сообщений)
-- `CompressionTarget` - целевое количество сообщений после сжатия (по умолчанию 10)
-- `MinMessageLengthForSummarization` - минимальная длина сообщения для суммаризации (50 символов)
-- `MaxSummarizedMessageLength` - максимальная длина сжатого сообщения (200 символов)
-
-## Архитектура
-
-### Новые компоненты
-
-1. **IHistoryCompressionService** - интерфейс для сжатия истории
-2. **HistoryCompressionService** - реализация сервиса сжатия
-3. **Обновленный ChatSession** - поддержка асинхронного сжатия
-4. **Обновленный AIService** - интеграция сжатия в генерацию ответов
-5. **Обновленный ChatService** - использование сжатия при обработке сообщений
-
-### Алгоритм сжатия
-
-1. **Проверка необходимости сжатия** - сравнение количества сообщений с порогом
-2. **Разделение сообщений** - отделение системных, старых и новых сообщений
-3. **Группировка по ролям** - отдельная обработка сообщений пользователя и ассистента
-4. **Суммаризация** - создание кратких резюме с помощью ИИ
-5. **Объединение** - формирование финального списка сообщений
-
-## Конфигурация
-
-### appsettings.json
-```json
-{
- "AI": {
- "EnableHistoryCompression": true,
- "CompressionThreshold": 20,
- "CompressionTarget": 10,
- "MinMessageLengthForSummarization": 50,
- "MaxSummarizedMessageLength": 200
- }
-}
-```
-
-## Использование
-
-### Автоматическое сжатие
-Сжатие происходит автоматически при добавлении новых сообщений, если включено в настройках.
-
-### Мониторинг
-Команда `/settings` показывает текущее состояние сжатия и параметры.
-
-## Преимущества
-
-1. **Экономия памяти** - значительное сокращение использования RAM
-2. **Улучшенная производительность** - быстрее обработка длинных диалогов
-3. **Сохранение контекста** - важная информация не теряется
-4. **Гибкость настройки** - возможность адаптации под различные сценарии
-5. **Обратная совместимость** - можно отключить без изменения кода
-
-## Обработка ошибок
-
-- При ошибках сжатия система автоматически переключается на простое обрезание истории
-- Логирование всех операций сжатия для мониторинга
-- Graceful degradation - бот продолжает работать даже при проблемах со сжатием
-
-## Производительность
-
-- Сжатие выполняется асинхронно, не блокируя основной поток
-- Использование кэширования для оптимизации повторных операций
-- Минимальное влияние на время отклика бота
diff --git a/ChatBot/Migrations/20251016214154_InitialCreate.Designer.cs b/ChatBot/Migrations/20251016214154_InitialCreate.Designer.cs
new file mode 100644
index 0000000..016d854
--- /dev/null
+++ b/ChatBot/Migrations/20251016214154_InitialCreate.Designer.cs
@@ -0,0 +1,149 @@
+//
+using System;
+using ChatBot.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace ChatBot.Migrations
+{
+ [DbContext(typeof(ChatBotDbContext))]
+ [Migration("20251016214154_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ChatBot.Models.Entities.ChatMessageEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Content")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("character varying(10000)")
+ .HasColumnName("content");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("MessageOrder")
+ .HasColumnType("integer")
+ .HasColumnName("message_order");
+
+ b.Property("Role")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("role");
+
+ b.Property("SessionId")
+ .HasColumnType("integer")
+ .HasColumnName("session_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("SessionId");
+
+ b.HasIndex("SessionId", "MessageOrder");
+
+ b.ToTable("chat_messages", (string)null);
+ });
+
+ modelBuilder.Entity("ChatBot.Models.Entities.ChatSessionEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ChatId")
+ .HasColumnType("bigint")
+ .HasColumnName("chat_id");
+
+ b.Property("ChatTitle")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("chat_title");
+
+ b.Property("ChatType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("chat_type");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("LastUpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_updated_at");
+
+ b.Property("MaxHistoryLength")
+ .HasColumnType("integer")
+ .HasColumnName("max_history_length");
+
+ b.Property("Model")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("model");
+
+ b.Property("SessionId")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("session_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChatId");
+
+ b.HasIndex("SessionId")
+ .IsUnique();
+
+ b.ToTable("chat_sessions", (string)null);
+ });
+
+ modelBuilder.Entity("ChatBot.Models.Entities.ChatMessageEntity", b =>
+ {
+ b.HasOne("ChatBot.Models.Entities.ChatSessionEntity", "Session")
+ .WithMany("Messages")
+ .HasForeignKey("SessionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Session");
+ });
+
+ modelBuilder.Entity("ChatBot.Models.Entities.ChatSessionEntity", b =>
+ {
+ b.Navigation("Messages");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/ChatBot/Migrations/20251016214154_InitialCreate.cs b/ChatBot/Migrations/20251016214154_InitialCreate.cs
new file mode 100644
index 0000000..5d1b90e
--- /dev/null
+++ b/ChatBot/Migrations/20251016214154_InitialCreate.cs
@@ -0,0 +1,95 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace ChatBot.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "chat_sessions",
+ 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),
+ 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)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_chat_sessions", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "chat_messages",
+ 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)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_chat_messages", x => x.id);
+ table.ForeignKey(
+ name: "FK_chat_messages_chat_sessions_session_id",
+ column: x => x.session_id,
+ principalTable: "chat_sessions",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_chat_messages_created_at",
+ table: "chat_messages",
+ column: "created_at");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_chat_messages_session_id",
+ table: "chat_messages",
+ column: "session_id");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_chat_messages_session_id_message_order",
+ table: "chat_messages",
+ columns: new[] { "session_id", "message_order" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_chat_sessions_chat_id",
+ table: "chat_sessions",
+ column: "chat_id");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_chat_sessions_session_id",
+ table: "chat_sessions",
+ column: "session_id",
+ unique: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "chat_messages");
+
+ migrationBuilder.DropTable(
+ name: "chat_sessions");
+ }
+ }
+}
diff --git a/ChatBot/Migrations/ChatBotDbContextModelSnapshot.cs b/ChatBot/Migrations/ChatBotDbContextModelSnapshot.cs
new file mode 100644
index 0000000..8d434b9
--- /dev/null
+++ b/ChatBot/Migrations/ChatBotDbContextModelSnapshot.cs
@@ -0,0 +1,146 @@
+//
+using System;
+using ChatBot.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace ChatBot.Migrations
+{
+ [DbContext(typeof(ChatBotDbContext))]
+ partial class ChatBotDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("ChatBot.Models.Entities.ChatMessageEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Content")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("character varying(10000)")
+ .HasColumnName("content");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("MessageOrder")
+ .HasColumnType("integer")
+ .HasColumnName("message_order");
+
+ b.Property("Role")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("role");
+
+ b.Property("SessionId")
+ .HasColumnType("integer")
+ .HasColumnName("session_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("SessionId");
+
+ b.HasIndex("SessionId", "MessageOrder");
+
+ b.ToTable("chat_messages", (string)null);
+ });
+
+ modelBuilder.Entity("ChatBot.Models.Entities.ChatSessionEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ChatId")
+ .HasColumnType("bigint")
+ .HasColumnName("chat_id");
+
+ b.Property("ChatTitle")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("chat_title");
+
+ b.Property("ChatType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("chat_type");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("LastUpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_updated_at");
+
+ b.Property("MaxHistoryLength")
+ .HasColumnType("integer")
+ .HasColumnName("max_history_length");
+
+ b.Property("Model")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("model");
+
+ b.Property("SessionId")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("session_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChatId");
+
+ b.HasIndex("SessionId")
+ .IsUnique();
+
+ b.ToTable("chat_sessions", (string)null);
+ });
+
+ modelBuilder.Entity("ChatBot.Models.Entities.ChatMessageEntity", b =>
+ {
+ b.HasOne("ChatBot.Models.Entities.ChatSessionEntity", "Session")
+ .WithMany("Messages")
+ .HasForeignKey("SessionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Session");
+ });
+
+ modelBuilder.Entity("ChatBot.Models.Entities.ChatSessionEntity", b =>
+ {
+ b.Navigation("Messages");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/ChatBot/Models/Configuration/DatabaseSettings.cs b/ChatBot/Models/Configuration/DatabaseSettings.cs
new file mode 100644
index 0000000..5c7616a
--- /dev/null
+++ b/ChatBot/Models/Configuration/DatabaseSettings.cs
@@ -0,0 +1,23 @@
+namespace ChatBot.Models.Configuration
+{
+ ///
+ /// Database configuration settings
+ ///
+ public class DatabaseSettings
+ {
+ ///
+ /// Connection string for the database
+ ///
+ public string ConnectionString { get; set; } = string.Empty;
+
+ ///
+ /// Enable sensitive data logging (for development only)
+ ///
+ public bool EnableSensitiveDataLogging { get; set; } = false;
+
+ ///
+ /// Command timeout in seconds
+ ///
+ public int CommandTimeout { get; set; } = 30;
+ }
+}
diff --git a/ChatBot/Models/Configuration/Validators/DatabaseSettingsValidator.cs b/ChatBot/Models/Configuration/Validators/DatabaseSettingsValidator.cs
new file mode 100644
index 0000000..3e9e7ff
--- /dev/null
+++ b/ChatBot/Models/Configuration/Validators/DatabaseSettingsValidator.cs
@@ -0,0 +1,40 @@
+using FluentValidation;
+using Microsoft.Extensions.Options;
+
+namespace ChatBot.Models.Configuration.Validators
+{
+ ///
+ /// Validator for DatabaseSettings
+ ///
+ public class DatabaseSettingsValidator
+ : AbstractValidator,
+ IValidateOptions
+ {
+ public DatabaseSettingsValidator()
+ {
+ RuleFor(x => x.ConnectionString)
+ .NotEmpty()
+ .WithMessage("Database connection string is required");
+
+ RuleFor(x => x.CommandTimeout)
+ .GreaterThan(0)
+ .WithMessage("Command timeout must be greater than 0");
+
+ RuleFor(x => x.CommandTimeout)
+ .LessThanOrEqualTo(300)
+ .WithMessage("Command timeout must be less than or equal to 300 seconds");
+ }
+
+ public ValidateOptionsResult Validate(string? name, DatabaseSettings options)
+ {
+ var result = Validate(options);
+ if (result.IsValid)
+ {
+ return ValidateOptionsResult.Success;
+ }
+
+ var errors = result.Errors.Select(e => e.ErrorMessage).ToArray();
+ return ValidateOptionsResult.Fail(errors);
+ }
+ }
+}
diff --git a/ChatBot/Models/Entities/ChatMessageEntity.cs b/ChatBot/Models/Entities/ChatMessageEntity.cs
new file mode 100644
index 0000000..91f8e05
--- /dev/null
+++ b/ChatBot/Models/Entities/ChatMessageEntity.cs
@@ -0,0 +1,61 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace ChatBot.Models.Entities
+{
+ ///
+ /// Entity model for chat message stored in database
+ ///
+ [Table("chat_messages")]
+ public class ChatMessageEntity
+ {
+ ///
+ /// Primary key
+ ///
+ [Key]
+ [Column("id")]
+ public int Id { get; set; }
+
+ ///
+ /// Foreign key to chat session
+ ///
+ [Required]
+ [Column("session_id")]
+ public int SessionId { get; set; }
+
+ ///
+ /// The content of the message
+ ///
+ [Required]
+ [Column("content")]
+ [StringLength(10000)]
+ public string Content { get; set; } = string.Empty;
+
+ ///
+ /// The role of the message author (system, user, assistant)
+ ///
+ [Required]
+ [Column("role")]
+ [StringLength(20)]
+ public string Role { get; set; } = string.Empty;
+
+ ///
+ /// When the message was created
+ ///
+ [Required]
+ [Column("created_at")]
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Order of the message in the conversation
+ ///
+ [Column("message_order")]
+ public int MessageOrder { get; set; }
+
+ ///
+ /// Navigation property to chat session
+ ///
+ [ForeignKey("SessionId")]
+ public virtual ChatSessionEntity Session { get; set; } = null!;
+ }
+}
diff --git a/ChatBot/Models/Entities/ChatSessionEntity.cs b/ChatBot/Models/Entities/ChatSessionEntity.cs
new file mode 100644
index 0000000..ed2ce78
--- /dev/null
+++ b/ChatBot/Models/Entities/ChatSessionEntity.cs
@@ -0,0 +1,82 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace ChatBot.Models.Entities
+{
+ ///
+ /// Entity model for chat session stored in database
+ ///
+ [Table("chat_sessions")]
+ public class ChatSessionEntity
+ {
+ ///
+ /// Primary key
+ ///
+ [Key]
+ [Column("id")]
+ public int Id { get; set; }
+
+ ///
+ /// Unique identifier for the chat session
+ ///
+ [Required]
+ [Column("session_id")]
+ [StringLength(50)]
+ public string SessionId { get; set; } = string.Empty;
+
+ ///
+ /// Telegram chat ID (can be private chat or group chat)
+ ///
+ [Required]
+ [Column("chat_id")]
+ public long ChatId { get; set; }
+
+ ///
+ /// Chat type (private, group, supergroup, channel)
+ ///
+ [Required]
+ [Column("chat_type")]
+ [StringLength(20)]
+ public string ChatType { get; set; } = "private";
+
+ ///
+ /// Chat title (for groups)
+ ///
+ [Column("chat_title")]
+ [StringLength(200)]
+ public string ChatTitle { get; set; } = string.Empty;
+
+ ///
+ /// AI model to use for this session
+ ///
+ [Column("model")]
+ [StringLength(100)]
+ public string Model { get; set; } = string.Empty;
+
+ ///
+ /// When the session was created
+ ///
+ [Required]
+ [Column("created_at")]
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// When the session was last updated
+ ///
+ [Required]
+ [Column("last_updated_at")]
+ public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// Maximum number of messages to keep in history
+ ///
+ [Column("max_history_length")]
+ public int MaxHistoryLength { get; set; } = 30;
+
+ ///
+ /// Navigation property for messages
+ ///
+ public virtual ICollection Messages { get; set; } =
+ new List();
+ }
+}
diff --git a/ChatBot/Program.cs b/ChatBot/Program.cs
index 1acdf0e..f7edebe 100644
--- a/ChatBot/Program.cs
+++ b/ChatBot/Program.cs
@@ -1,3 +1,6 @@
+using ChatBot.Data;
+using ChatBot.Data.Interfaces;
+using ChatBot.Data.Repositories;
using ChatBot.Models.Configuration;
using ChatBot.Models.Configuration.Validators;
using ChatBot.Services;
@@ -6,6 +9,7 @@ using ChatBot.Services.Interfaces;
using ChatBot.Services.Telegram.Commands;
using ChatBot.Services.Telegram.Interfaces;
using ChatBot.Services.Telegram.Services;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Serilog;
using Telegram.Bot;
@@ -35,10 +39,15 @@ try
.Services.Configure(builder.Configuration.GetSection("AI"))
.AddSingleton, AISettingsValidator>();
+ builder
+ .Services.Configure(builder.Configuration.GetSection("Database"))
+ .AddSingleton, DatabaseSettingsValidator>();
+
// Валидируем конфигурацию при старте
builder.Services.AddOptions().ValidateOnStart();
builder.Services.AddOptions().ValidateOnStart();
builder.Services.AddOptions().ValidateOnStart();
+ builder.Services.AddOptions().ValidateOnStart();
// Регистрируем IOllamaClient
builder.Services.AddSingleton(sp =>
@@ -47,22 +56,49 @@ try
return new OllamaClientAdapter(settings.Value.Url);
});
+ // Регистрируем Entity Framework и базу данных
+ builder.Services.AddDbContext(
+ (serviceProvider, options) =>
+ {
+ var dbSettings = serviceProvider.GetRequiredService>().Value;
+ options.UseNpgsql(
+ dbSettings.ConnectionString,
+ npgsqlOptions =>
+ {
+ npgsqlOptions.CommandTimeout(dbSettings.CommandTimeout);
+ }
+ );
+
+ if (dbSettings.EnableSensitiveDataLogging)
+ {
+ options.EnableSensitiveDataLogging();
+ }
+ }
+ );
+
+ // Регистрируем репозиторий
+ builder.Services.AddScoped();
+
// Регистрируем интерфейсы и сервисы
- builder.Services.AddSingleton();
+ // Можно переключиться между InMemorySessionStorage и DatabaseSessionStorage
+ builder.Services.AddScoped();
// Регистрируем основные сервисы
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
- builder.Services.AddSingleton();
+ builder.Services.AddScoped();
+
+ // Регистрируем сервис инициализации базы данных
+ builder.Services.AddHostedService();
// Регистрируем Telegram команды
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
+ builder.Services.AddScoped();
+ builder.Services.AddScoped();
+ builder.Services.AddScoped();
+ builder.Services.AddScoped();
+ builder.Services.AddScoped();
// Регистрируем Telegram сервисы
builder.Services.AddSingleton(provider =>
@@ -72,17 +108,24 @@ try
});
builder.Services.AddSingleton();
builder.Services.AddSingleton();
- builder.Services.AddSingleton();
+ builder.Services.AddScoped();
builder.Services.AddSingleton();
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
+ builder.Services.AddScoped();
+ builder.Services.AddScoped();
- // Регистрируем TelegramBotService как singleton и используем один экземпляр для интерфейса и HostedService
- builder.Services.AddSingleton();
- builder.Services.AddSingleton(sp =>
+ // Регистрируем TelegramBotService как scoped и используем один экземпляр для интерфейса и HostedService
+ builder.Services.AddScoped();
+ builder.Services.AddScoped(sp =>
sp.GetRequiredService()
);
- builder.Services.AddHostedService(sp => sp.GetRequiredService());
+
+ // Создаем обертку для HostedService, которая создает scope
+ builder.Services.AddSingleton(sp =>
+ {
+ var serviceProvider = sp.GetRequiredService();
+ var logger = sp.GetRequiredService>();
+ return new TelegramBotHostedService(serviceProvider, logger);
+ });
// Регистрируем Health Checks
builder
diff --git a/ChatBot/Services/ChatService.cs b/ChatBot/Services/ChatService.cs
index ecf7509..d29d53a 100644
--- a/ChatBot/Services/ChatService.cs
+++ b/ChatBot/Services/ChatService.cs
@@ -83,6 +83,9 @@ namespace ChatBot.Services
session.AddUserMessage(message, username);
}
+ // Save session changes to database
+ await _sessionStorage.SaveSessionAsync(session);
+
_logger.LogInformation(
"Processing message from user {Username} in chat {ChatId} ({ChatType}): {Message}",
username,
@@ -130,6 +133,9 @@ namespace ChatBot.Services
session.AddAssistantMessage(response);
}
+ // Save session changes to database
+ await _sessionStorage.SaveSessionAsync(session);
+
_logger.LogDebug(
"AI response generated for chat {ChatId} (length: {Length})",
chatId,
@@ -149,7 +155,7 @@ namespace ChatBot.Services
///
/// Update session parameters
///
- public void UpdateSessionParameters(long chatId, string? model = null)
+ public async Task UpdateSessionParametersAsync(long chatId, string? model = null)
{
var session = _sessionStorage.Get(chatId);
if (session != null)
@@ -158,6 +164,10 @@ namespace ChatBot.Services
session.Model = model;
session.LastUpdatedAt = DateTime.UtcNow;
+
+ // Save session changes to database
+ await _sessionStorage.SaveSessionAsync(session);
+
_logger.LogInformation("Updated session parameters for chat {ChatId}", chatId);
}
}
@@ -165,12 +175,16 @@ namespace ChatBot.Services
///
/// Clear chat history for a session
///
- public void ClearHistory(long chatId)
+ public async Task ClearHistoryAsync(long chatId)
{
var session = _sessionStorage.Get(chatId);
if (session != null)
{
session.ClearHistory();
+
+ // Save session changes to database
+ await _sessionStorage.SaveSessionAsync(session);
+
_logger.LogInformation("Cleared history for chat {ChatId}", chatId);
}
}
diff --git a/ChatBot/Services/DatabaseInitializationService.cs b/ChatBot/Services/DatabaseInitializationService.cs
new file mode 100644
index 0000000..8381d86
--- /dev/null
+++ b/ChatBot/Services/DatabaseInitializationService.cs
@@ -0,0 +1,74 @@
+using ChatBot.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace ChatBot.Services
+{
+ ///
+ /// Service for initializing database on application startup
+ ///
+ public class DatabaseInitializationService : IHostedService
+ {
+ private readonly IServiceProvider _serviceProvider;
+ private readonly ILogger _logger;
+
+ public DatabaseInitializationService(
+ IServiceProvider serviceProvider,
+ ILogger logger
+ )
+ {
+ _serviceProvider = serviceProvider;
+ _logger = logger;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Starting database initialization...");
+
+ using var scope = _serviceProvider.CreateScope();
+ var context = scope.ServiceProvider.GetRequiredService();
+
+ try
+ {
+ // Check if database exists and is accessible
+ var canConnect = await context.Database.CanConnectAsync(cancellationToken);
+ if (!canConnect)
+ {
+ _logger.LogInformation(
+ "Database does not exist. Creating database and applying migrations..."
+ );
+ }
+ else
+ {
+ _logger.LogInformation("Database exists. Applying migrations...");
+ }
+
+ // Ensure database is created and migrations are applied
+ await context.Database.MigrateAsync(cancellationToken);
+
+ _logger.LogInformation("Database initialization completed successfully");
+ }
+ catch (Exception ex)
+ when (ex.Message.Contains("database") && ex.Message.Contains("does not exist"))
+ {
+ // This is expected when database doesn't exist - MigrateAsync will create it
+ _logger.LogInformation("Database does not exist, will be created during migration");
+ await context.Database.MigrateAsync(cancellationToken);
+ _logger.LogInformation("Database initialization completed successfully");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to initialize database");
+ throw new InvalidOperationException("Database initialization failed", ex);
+ }
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Database initialization service stopped");
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/ChatBot/Services/DatabaseSessionStorage.cs b/ChatBot/Services/DatabaseSessionStorage.cs
new file mode 100644
index 0000000..dce80f7
--- /dev/null
+++ b/ChatBot/Services/DatabaseSessionStorage.cs
@@ -0,0 +1,189 @@
+using ChatBot.Data.Interfaces;
+using ChatBot.Models;
+using ChatBot.Models.Dto;
+using ChatBot.Models.Entities;
+using ChatBot.Services.Interfaces;
+using OllamaSharp.Models.Chat;
+
+namespace ChatBot.Services
+{
+ ///
+ /// Database implementation of session storage
+ ///
+ public class DatabaseSessionStorage : ISessionStorage
+ {
+ private readonly IChatSessionRepository _repository;
+ private readonly ILogger _logger;
+ private readonly IHistoryCompressionService? _compressionService;
+
+ public DatabaseSessionStorage(
+ IChatSessionRepository repository,
+ ILogger logger,
+ IHistoryCompressionService? compressionService = null
+ )
+ {
+ _repository = repository;
+ _logger = logger;
+ _compressionService = compressionService;
+ }
+
+ public ChatSession GetOrCreate(
+ long chatId,
+ string chatType = "private",
+ string chatTitle = ""
+ )
+ {
+ try
+ {
+ var sessionEntity = _repository
+ .GetOrCreateAsync(chatId, chatType, chatTitle)
+ .GetAwaiter()
+ .GetResult();
+ return ConvertToChatSession(sessionEntity);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to get or create session for chat {ChatId}", chatId);
+ throw new InvalidOperationException(
+ $"Failed to get or create session for chat {chatId}",
+ ex
+ );
+ }
+ }
+
+ public ChatSession? Get(long chatId)
+ {
+ try
+ {
+ var sessionEntity = _repository.GetByChatIdAsync(chatId).GetAwaiter().GetResult();
+ return sessionEntity != null ? ConvertToChatSession(sessionEntity) : null;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to get session for chat {ChatId}", chatId);
+ return null;
+ }
+ }
+
+ public bool Remove(long chatId)
+ {
+ try
+ {
+ return _repository.DeleteAsync(chatId).GetAwaiter().GetResult();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to remove session for chat {ChatId}", chatId);
+ return false;
+ }
+ }
+
+ public int GetActiveSessionsCount()
+ {
+ try
+ {
+ return _repository.GetActiveSessionsCountAsync().GetAwaiter().GetResult();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to get active sessions count");
+ return 0;
+ }
+ }
+
+ public int CleanupOldSessions(int hoursOld = 24)
+ {
+ try
+ {
+ return _repository.CleanupOldSessionsAsync(hoursOld).GetAwaiter().GetResult();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to cleanup old sessions");
+ return 0;
+ }
+ }
+
+ ///
+ /// Save session changes to database
+ ///
+ public async Task SaveSessionAsync(ChatSession session)
+ {
+ try
+ {
+ var sessionEntity = await _repository.GetByChatIdAsync(session.ChatId);
+ if (sessionEntity == null)
+ {
+ _logger.LogWarning(
+ "Session not found for chat {ChatId} during save",
+ session.ChatId
+ );
+ return;
+ }
+
+ // Update session properties
+ sessionEntity.Model = session.Model;
+ sessionEntity.MaxHistoryLength = session.MaxHistoryLength;
+ sessionEntity.LastUpdatedAt = DateTime.UtcNow;
+
+ // Clear existing messages and add new ones
+ await _repository.ClearMessagesAsync(sessionEntity.Id);
+
+ var messages = session.GetAllMessages();
+ for (int i = 0; i < messages.Count; i++)
+ {
+ await _repository.AddMessageAsync(
+ sessionEntity.Id,
+ messages[i].Content,
+ messages[i].Role.ToString(),
+ i
+ );
+ }
+
+ await _repository.UpdateAsync(sessionEntity);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to save session for chat {ChatId}", session.ChatId);
+ throw new InvalidOperationException(
+ $"Failed to save session for chat {session.ChatId}",
+ ex
+ );
+ }
+ }
+
+ ///
+ /// Convert ChatSessionEntity to ChatSession
+ ///
+ private ChatSession ConvertToChatSession(ChatSessionEntity entity)
+ {
+ var session = new ChatSession
+ {
+ SessionId = entity.SessionId,
+ ChatId = entity.ChatId,
+ ChatType = entity.ChatType,
+ ChatTitle = entity.ChatTitle,
+ Model = entity.Model,
+ CreatedAt = entity.CreatedAt,
+ LastUpdatedAt = entity.LastUpdatedAt,
+ MaxHistoryLength = entity.MaxHistoryLength,
+ };
+
+ // Set compression service if available
+ if (_compressionService != null)
+ {
+ session.SetCompressionService(_compressionService);
+ }
+
+ // Add messages to session
+ foreach (var messageEntity in entity.Messages.OrderBy(m => m.MessageOrder))
+ {
+ var role = Enum.Parse(messageEntity.Role);
+ var message = new ChatMessage { Content = messageEntity.Content, Role = role };
+ session.AddMessage(message);
+ }
+
+ return session;
+ }
+ }
+}
diff --git a/ChatBot/Services/InMemorySessionStorage.cs b/ChatBot/Services/InMemorySessionStorage.cs
index 77a1ee4..78dca3b 100644
--- a/ChatBot/Services/InMemorySessionStorage.cs
+++ b/ChatBot/Services/InMemorySessionStorage.cs
@@ -98,5 +98,12 @@ namespace ChatBot.Services
return sessionsToRemove.Count;
}
+
+ public Task SaveSessionAsync(ChatSession session)
+ {
+ // For in-memory storage, no additional save is needed
+ // The session is already in memory and will be updated automatically
+ return Task.CompletedTask;
+ }
}
}
diff --git a/ChatBot/Services/Interfaces/ISessionStorage.cs b/ChatBot/Services/Interfaces/ISessionStorage.cs
index 8b23691..0b9e6c6 100644
--- a/ChatBot/Services/Interfaces/ISessionStorage.cs
+++ b/ChatBot/Services/Interfaces/ISessionStorage.cs
@@ -31,5 +31,10 @@ namespace ChatBot.Services.Interfaces
/// Clean up old sessions
///
int CleanupOldSessions(int hoursOld = 24);
+
+ ///
+ /// Save session changes to storage (for database implementations)
+ ///
+ Task SaveSessionAsync(ChatSession session);
}
}
diff --git a/ChatBot/Services/Telegram/Commands/ClearCommand.cs b/ChatBot/Services/Telegram/Commands/ClearCommand.cs
index b277146..fa5b115 100644
--- a/ChatBot/Services/Telegram/Commands/ClearCommand.cs
+++ b/ChatBot/Services/Telegram/Commands/ClearCommand.cs
@@ -12,13 +12,13 @@ namespace ChatBot.Services.Telegram.Commands
public override string CommandName => "/clear";
public override string Description => "Очистить историю чата";
- public override Task ExecuteAsync(
+ public override async Task ExecuteAsync(
TelegramCommandContext context,
CancellationToken cancellationToken = default
)
{
- _chatService.ClearHistory(context.ChatId);
- return Task.FromResult("История чата очищена. Начинаем новый разговор!");
+ await _chatService.ClearHistoryAsync(context.ChatId);
+ return "История чата очищена. Начинаем новый разговор!";
}
}
}
diff --git a/ChatBot/Services/TelegramBotHostedService.cs b/ChatBot/Services/TelegramBotHostedService.cs
new file mode 100644
index 0000000..4d56753
--- /dev/null
+++ b/ChatBot/Services/TelegramBotHostedService.cs
@@ -0,0 +1,46 @@
+using ChatBot.Services.Telegram.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace ChatBot.Services
+{
+ ///
+ /// Hosted service wrapper for TelegramBotService to handle scoped dependencies
+ ///
+ public class TelegramBotHostedService : IHostedService
+ {
+ private readonly IServiceProvider _serviceProvider;
+ private readonly ILogger _logger;
+ private ITelegramBotService? _botService;
+
+ public TelegramBotHostedService(
+ IServiceProvider serviceProvider,
+ ILogger logger
+ )
+ {
+ _serviceProvider = serviceProvider;
+ _logger = logger;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Starting Telegram Bot Hosted Service...");
+
+ using var scope = _serviceProvider.CreateScope();
+ _botService = scope.ServiceProvider.GetRequiredService();
+
+ await _botService.StartAsync(cancellationToken);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Stopping Telegram Bot Hosted Service...");
+
+ if (_botService != null)
+ {
+ await _botService.StopAsync(cancellationToken);
+ }
+ }
+ }
+}
diff --git a/ChatBot/appsettings.json b/ChatBot/appsettings.json
index cf82639..62e0096 100644
--- a/ChatBot/appsettings.json
+++ b/ChatBot/appsettings.json
@@ -5,6 +5,8 @@
"Default": "Information",
"Override": {
"Microsoft": "Warning",
+ "Microsoft.EntityFrameworkCore.Database.Connection": "Information",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Information",
"System": "Warning",
"Telegram.Bot": "Information"
}
@@ -50,5 +52,10 @@
"MaxRetryDelayMs": 30000,
"CompressionTimeoutSeconds": 30,
"StatusCheckTimeoutSeconds": 10
+ },
+ "Database": {
+ "ConnectionString": "Host=localhost;Port=5432;Database=chatbot;Username=postgres;Password=postgres",
+ "EnableSensitiveDataLogging": false,
+ "CommandTimeout": 30
}
}