add db
This commit is contained in:
89
ChatBot/.gitea/workflows/build.yml
Normal file
89
ChatBot/.gitea/workflows/build.yml
Normal file
@@ -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 }}"
|
||||||
@@ -18,6 +18,12 @@
|
|||||||
<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="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
|
||||||
|
<PackageReference Include="FluentValidation" Version="11.9.0" />
|
||||||
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="Prompts\system-prompt.txt">
|
<None Update="Prompts\system-prompt.txt">
|
||||||
|
|||||||
78
ChatBot/Data/ChatBotDbContext.cs
Normal file
78
ChatBot/Data/ChatBotDbContext.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using ChatBot.Models.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ChatBot.Data
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// DbContext for ChatBot application
|
||||||
|
/// </summary>
|
||||||
|
public class ChatBotDbContext : DbContext
|
||||||
|
{
|
||||||
|
public ChatBotDbContext(DbContextOptions<ChatBotDbContext> options)
|
||||||
|
: base(options) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chat sessions table
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<ChatSessionEntity> ChatSessions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chat messages table
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<ChatMessageEntity> ChatMessages { get; set; }
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
// Configure ChatSessionEntity
|
||||||
|
modelBuilder.Entity<ChatSessionEntity>(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<ChatMessageEntity>(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<ChatSessionEntity>().ToTable("chat_sessions");
|
||||||
|
modelBuilder.Entity<ChatMessageEntity>().ToTable("chat_messages");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
ChatBot/Data/Interfaces/IChatSessionRepository.cs
Normal file
74
ChatBot/Data/Interfaces/IChatSessionRepository.cs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
using ChatBot.Models.Entities;
|
||||||
|
|
||||||
|
namespace ChatBot.Data.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Repository interface for chat session operations
|
||||||
|
/// </summary>
|
||||||
|
public interface IChatSessionRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get or create a chat session by chat ID
|
||||||
|
/// </summary>
|
||||||
|
Task<ChatSessionEntity> GetOrCreateAsync(
|
||||||
|
long chatId,
|
||||||
|
string chatType = "private",
|
||||||
|
string chatTitle = ""
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a chat session by chat ID
|
||||||
|
/// </summary>
|
||||||
|
Task<ChatSessionEntity?> GetByChatIdAsync(long chatId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a chat session by session ID
|
||||||
|
/// </summary>
|
||||||
|
Task<ChatSessionEntity?> GetBySessionIdAsync(string sessionId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update a chat session
|
||||||
|
/// </summary>
|
||||||
|
Task<ChatSessionEntity> UpdateAsync(ChatSessionEntity session);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete a chat session
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> DeleteAsync(long chatId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all messages for a session
|
||||||
|
/// </summary>
|
||||||
|
Task<List<ChatMessageEntity>> GetMessagesAsync(int sessionId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a message to a session
|
||||||
|
/// </summary>
|
||||||
|
Task<ChatMessageEntity> AddMessageAsync(
|
||||||
|
int sessionId,
|
||||||
|
string content,
|
||||||
|
string role,
|
||||||
|
int messageOrder
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear all messages for a session
|
||||||
|
/// </summary>
|
||||||
|
Task ClearMessagesAsync(int sessionId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get count of active sessions
|
||||||
|
/// </summary>
|
||||||
|
Task<int> GetActiveSessionsCountAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clean up old sessions
|
||||||
|
/// </summary>
|
||||||
|
Task<int> CleanupOldSessionsAsync(int hoursOld = 24);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get sessions that need cleanup
|
||||||
|
/// </summary>
|
||||||
|
Task<List<ChatSessionEntity>> GetSessionsForCleanupAsync(int hoursOld = 24);
|
||||||
|
}
|
||||||
|
}
|
||||||
171
ChatBot/Data/Repositories/ChatSessionRepository.cs
Normal file
171
ChatBot/Data/Repositories/ChatSessionRepository.cs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
using ChatBot.Data.Interfaces;
|
||||||
|
using ChatBot.Models.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ChatBot.Data.Repositories
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Repository implementation for chat session operations
|
||||||
|
/// </summary>
|
||||||
|
public class ChatSessionRepository : IChatSessionRepository
|
||||||
|
{
|
||||||
|
private readonly ChatBotDbContext _context;
|
||||||
|
private readonly ILogger<ChatSessionRepository> _logger;
|
||||||
|
|
||||||
|
public ChatSessionRepository(
|
||||||
|
ChatBotDbContext context,
|
||||||
|
ILogger<ChatSessionRepository> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChatSessionEntity> 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<ChatSessionEntity?> GetByChatIdAsync(long chatId)
|
||||||
|
{
|
||||||
|
return await _context
|
||||||
|
.ChatSessions.Include(s => s.Messages.OrderBy(m => m.MessageOrder))
|
||||||
|
.FirstOrDefaultAsync(s => s.ChatId == chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChatSessionEntity?> GetBySessionIdAsync(string sessionId)
|
||||||
|
{
|
||||||
|
return await _context
|
||||||
|
.ChatSessions.Include(s => s.Messages.OrderBy(m => m.MessageOrder))
|
||||||
|
.FirstOrDefaultAsync(s => s.SessionId == sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChatSessionEntity> UpdateAsync(ChatSessionEntity session)
|
||||||
|
{
|
||||||
|
session.LastUpdatedAt = DateTime.UtcNow;
|
||||||
|
_context.ChatSessions.Update(session);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> 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<List<ChatMessageEntity>> GetMessagesAsync(int sessionId)
|
||||||
|
{
|
||||||
|
return await _context
|
||||||
|
.ChatMessages.Where(m => m.SessionId == sessionId)
|
||||||
|
.OrderBy(m => m.MessageOrder)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChatMessageEntity> 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<int> GetActiveSessionsCountAsync()
|
||||||
|
{
|
||||||
|
return await _context.ChatSessions.CountAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> 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<List<ChatSessionEntity>> GetSessionsForCleanupAsync(int hoursOld = 24)
|
||||||
|
{
|
||||||
|
var cutoffTime = DateTime.UtcNow.AddHours(-hoursOld);
|
||||||
|
return await _context
|
||||||
|
.ChatSessions.Where(s => s.LastUpdatedAt < cutoffTime)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 - бот продолжает работать даже при проблемах со сжатием
|
|
||||||
|
|
||||||
## Производительность
|
|
||||||
|
|
||||||
- Сжатие выполняется асинхронно, не блокируя основной поток
|
|
||||||
- Использование кэширования для оптимизации повторных операций
|
|
||||||
- Минимальное влияние на время отклика бота
|
|
||||||
149
ChatBot/Migrations/20251016214154_InitialCreate.Designer.cs
generated
Normal file
149
ChatBot/Migrations/20251016214154_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10000)
|
||||||
|
.HasColumnType("character varying(10000)")
|
||||||
|
.HasColumnName("content");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("MessageOrder")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("message_order");
|
||||||
|
|
||||||
|
b.Property<string>("Role")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasColumnName("role");
|
||||||
|
|
||||||
|
b.Property<int>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<long>("ChatId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("chat_id");
|
||||||
|
|
||||||
|
b.Property<string>("ChatTitle")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("chat_title");
|
||||||
|
|
||||||
|
b.Property<string>("ChatType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasColumnName("chat_type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_updated_at");
|
||||||
|
|
||||||
|
b.Property<int>("MaxHistoryLength")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("max_history_length");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
ChatBot/Migrations/20251016214154_InitialCreate.cs
Normal file
95
ChatBot/Migrations/20251016214154_InitialCreate.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ChatBot.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "chat_sessions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
session_id = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
chat_id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
chat_type = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||||
|
chat_title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
model = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
last_updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
max_history_length = table.Column<int>(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<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
session_id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
content = table.Column<string>(type: "character varying(10000)", maxLength: 10000, nullable: false),
|
||||||
|
role = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||||
|
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
message_order = table.Column<int>(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "chat_messages");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "chat_sessions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
146
ChatBot/Migrations/ChatBotDbContextModelSnapshot.cs
Normal file
146
ChatBot/Migrations/ChatBotDbContextModelSnapshot.cs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10000)
|
||||||
|
.HasColumnType("character varying(10000)")
|
||||||
|
.HasColumnName("content");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<int>("MessageOrder")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("message_order");
|
||||||
|
|
||||||
|
b.Property<string>("Role")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasColumnName("role");
|
||||||
|
|
||||||
|
b.Property<int>("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<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<long>("ChatId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("chat_id");
|
||||||
|
|
||||||
|
b.Property<string>("ChatTitle")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("chat_title");
|
||||||
|
|
||||||
|
b.Property<string>("ChatType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)")
|
||||||
|
.HasColumnName("chat_type");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_updated_at");
|
||||||
|
|
||||||
|
b.Property<int>("MaxHistoryLength")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("max_history_length");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("model");
|
||||||
|
|
||||||
|
b.Property<string>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
ChatBot/Models/Configuration/DatabaseSettings.cs
Normal file
23
ChatBot/Models/Configuration/DatabaseSettings.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace ChatBot.Models.Configuration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Database configuration settings
|
||||||
|
/// </summary>
|
||||||
|
public class DatabaseSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Connection string for the database
|
||||||
|
/// </summary>
|
||||||
|
public string ConnectionString { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable sensitive data logging (for development only)
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableSensitiveDataLogging { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command timeout in seconds
|
||||||
|
/// </summary>
|
||||||
|
public int CommandTimeout { get; set; } = 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace ChatBot.Models.Configuration.Validators
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validator for DatabaseSettings
|
||||||
|
/// </summary>
|
||||||
|
public class DatabaseSettingsValidator
|
||||||
|
: AbstractValidator<DatabaseSettings>,
|
||||||
|
IValidateOptions<DatabaseSettings>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
ChatBot/Models/Entities/ChatMessageEntity.cs
Normal file
61
ChatBot/Models/Entities/ChatMessageEntity.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace ChatBot.Models.Entities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Entity model for chat message stored in database
|
||||||
|
/// </summary>
|
||||||
|
[Table("chat_messages")]
|
||||||
|
public class ChatMessageEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Primary key
|
||||||
|
/// </summary>
|
||||||
|
[Key]
|
||||||
|
[Column("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Foreign key to chat session
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[Column("session_id")]
|
||||||
|
public int SessionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The content of the message
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[Column("content")]
|
||||||
|
[StringLength(10000)]
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The role of the message author (system, user, assistant)
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[Column("role")]
|
||||||
|
[StringLength(20)]
|
||||||
|
public string Role { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the message was created
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[Column("created_at")]
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Order of the message in the conversation
|
||||||
|
/// </summary>
|
||||||
|
[Column("message_order")]
|
||||||
|
public int MessageOrder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Navigation property to chat session
|
||||||
|
/// </summary>
|
||||||
|
[ForeignKey("SessionId")]
|
||||||
|
public virtual ChatSessionEntity Session { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
ChatBot/Models/Entities/ChatSessionEntity.cs
Normal file
82
ChatBot/Models/Entities/ChatSessionEntity.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace ChatBot.Models.Entities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Entity model for chat session stored in database
|
||||||
|
/// </summary>
|
||||||
|
[Table("chat_sessions")]
|
||||||
|
public class ChatSessionEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Primary key
|
||||||
|
/// </summary>
|
||||||
|
[Key]
|
||||||
|
[Column("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unique identifier for the chat session
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[Column("session_id")]
|
||||||
|
[StringLength(50)]
|
||||||
|
public string SessionId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Telegram chat ID (can be private chat or group chat)
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[Column("chat_id")]
|
||||||
|
public long ChatId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chat type (private, group, supergroup, channel)
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[Column("chat_type")]
|
||||||
|
[StringLength(20)]
|
||||||
|
public string ChatType { get; set; } = "private";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chat title (for groups)
|
||||||
|
/// </summary>
|
||||||
|
[Column("chat_title")]
|
||||||
|
[StringLength(200)]
|
||||||
|
public string ChatTitle { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AI model to use for this session
|
||||||
|
/// </summary>
|
||||||
|
[Column("model")]
|
||||||
|
[StringLength(100)]
|
||||||
|
public string Model { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the session was created
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[Column("created_at")]
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the session was last updated
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[Column("last_updated_at")]
|
||||||
|
public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of messages to keep in history
|
||||||
|
/// </summary>
|
||||||
|
[Column("max_history_length")]
|
||||||
|
public int MaxHistoryLength { get; set; } = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Navigation property for messages
|
||||||
|
/// </summary>
|
||||||
|
public virtual ICollection<ChatMessageEntity> Messages { get; set; } =
|
||||||
|
new List<ChatMessageEntity>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using ChatBot.Data;
|
||||||
|
using ChatBot.Data.Interfaces;
|
||||||
|
using ChatBot.Data.Repositories;
|
||||||
using ChatBot.Models.Configuration;
|
using ChatBot.Models.Configuration;
|
||||||
using ChatBot.Models.Configuration.Validators;
|
using ChatBot.Models.Configuration.Validators;
|
||||||
using ChatBot.Services;
|
using ChatBot.Services;
|
||||||
@@ -6,6 +9,7 @@ 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 Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
@@ -35,10 +39,15 @@ try
|
|||||||
.Services.Configure<AISettings>(builder.Configuration.GetSection("AI"))
|
.Services.Configure<AISettings>(builder.Configuration.GetSection("AI"))
|
||||||
.AddSingleton<IValidateOptions<AISettings>, AISettingsValidator>();
|
.AddSingleton<IValidateOptions<AISettings>, AISettingsValidator>();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.Services.Configure<DatabaseSettings>(builder.Configuration.GetSection("Database"))
|
||||||
|
.AddSingleton<IValidateOptions<DatabaseSettings>, DatabaseSettingsValidator>();
|
||||||
|
|
||||||
// Валидируем конфигурацию при старте
|
// Валидируем конфигурацию при старте
|
||||||
builder.Services.AddOptions<TelegramBotSettings>().ValidateOnStart();
|
builder.Services.AddOptions<TelegramBotSettings>().ValidateOnStart();
|
||||||
builder.Services.AddOptions<OllamaSettings>().ValidateOnStart();
|
builder.Services.AddOptions<OllamaSettings>().ValidateOnStart();
|
||||||
builder.Services.AddOptions<AISettings>().ValidateOnStart();
|
builder.Services.AddOptions<AISettings>().ValidateOnStart();
|
||||||
|
builder.Services.AddOptions<DatabaseSettings>().ValidateOnStart();
|
||||||
|
|
||||||
// Регистрируем IOllamaClient
|
// Регистрируем IOllamaClient
|
||||||
builder.Services.AddSingleton<IOllamaClient>(sp =>
|
builder.Services.AddSingleton<IOllamaClient>(sp =>
|
||||||
@@ -47,22 +56,49 @@ try
|
|||||||
return new OllamaClientAdapter(settings.Value.Url);
|
return new OllamaClientAdapter(settings.Value.Url);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Регистрируем Entity Framework и базу данных
|
||||||
|
builder.Services.AddDbContext<ChatBotDbContext>(
|
||||||
|
(serviceProvider, options) =>
|
||||||
|
{
|
||||||
|
var dbSettings = serviceProvider.GetRequiredService<IOptions<DatabaseSettings>>().Value;
|
||||||
|
options.UseNpgsql(
|
||||||
|
dbSettings.ConnectionString,
|
||||||
|
npgsqlOptions =>
|
||||||
|
{
|
||||||
|
npgsqlOptions.CommandTimeout(dbSettings.CommandTimeout);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dbSettings.EnableSensitiveDataLogging)
|
||||||
|
{
|
||||||
|
options.EnableSensitiveDataLogging();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Регистрируем репозиторий
|
||||||
|
builder.Services.AddScoped<IChatSessionRepository, ChatSessionRepository>();
|
||||||
|
|
||||||
// Регистрируем интерфейсы и сервисы
|
// Регистрируем интерфейсы и сервисы
|
||||||
builder.Services.AddSingleton<ISessionStorage, InMemorySessionStorage>();
|
// Можно переключиться между InMemorySessionStorage и DatabaseSessionStorage
|
||||||
|
builder.Services.AddScoped<ISessionStorage, DatabaseSessionStorage>();
|
||||||
|
|
||||||
// Регистрируем основные сервисы
|
// Регистрируем основные сервисы
|
||||||
builder.Services.AddSingleton<ModelService>();
|
builder.Services.AddSingleton<ModelService>();
|
||||||
builder.Services.AddSingleton<SystemPromptService>();
|
builder.Services.AddSingleton<SystemPromptService>();
|
||||||
builder.Services.AddSingleton<IHistoryCompressionService, HistoryCompressionService>();
|
builder.Services.AddSingleton<IHistoryCompressionService, HistoryCompressionService>();
|
||||||
builder.Services.AddSingleton<IAIService, AIService>();
|
builder.Services.AddSingleton<IAIService, AIService>();
|
||||||
builder.Services.AddSingleton<ChatService>();
|
builder.Services.AddScoped<ChatService>();
|
||||||
|
|
||||||
|
// Регистрируем сервис инициализации базы данных
|
||||||
|
builder.Services.AddHostedService<DatabaseInitializationService>();
|
||||||
|
|
||||||
// Регистрируем Telegram команды
|
// Регистрируем Telegram команды
|
||||||
builder.Services.AddSingleton<ITelegramCommand, StartCommand>();
|
builder.Services.AddScoped<ITelegramCommand, StartCommand>();
|
||||||
builder.Services.AddSingleton<ITelegramCommand, HelpCommand>();
|
builder.Services.AddScoped<ITelegramCommand, HelpCommand>();
|
||||||
builder.Services.AddSingleton<ITelegramCommand, ClearCommand>();
|
builder.Services.AddScoped<ITelegramCommand, ClearCommand>();
|
||||||
builder.Services.AddSingleton<ITelegramCommand, SettingsCommand>();
|
builder.Services.AddScoped<ITelegramCommand, SettingsCommand>();
|
||||||
builder.Services.AddSingleton<ITelegramCommand, StatusCommand>();
|
builder.Services.AddScoped<ITelegramCommand, StatusCommand>();
|
||||||
|
|
||||||
// Регистрируем Telegram сервисы
|
// Регистрируем Telegram сервисы
|
||||||
builder.Services.AddSingleton<ITelegramBotClient>(provider =>
|
builder.Services.AddSingleton<ITelegramBotClient>(provider =>
|
||||||
@@ -72,17 +108,24 @@ try
|
|||||||
});
|
});
|
||||||
builder.Services.AddSingleton<ITelegramMessageSender, TelegramMessageSender>();
|
builder.Services.AddSingleton<ITelegramMessageSender, TelegramMessageSender>();
|
||||||
builder.Services.AddSingleton<ITelegramErrorHandler, TelegramErrorHandler>();
|
builder.Services.AddSingleton<ITelegramErrorHandler, TelegramErrorHandler>();
|
||||||
builder.Services.AddSingleton<CommandRegistry>();
|
builder.Services.AddScoped<CommandRegistry>();
|
||||||
builder.Services.AddSingleton<BotInfoService>();
|
builder.Services.AddSingleton<BotInfoService>();
|
||||||
builder.Services.AddSingleton<ITelegramCommandProcessor, TelegramCommandProcessor>();
|
builder.Services.AddScoped<ITelegramCommandProcessor, TelegramCommandProcessor>();
|
||||||
builder.Services.AddSingleton<ITelegramMessageHandler, TelegramMessageHandler>();
|
builder.Services.AddScoped<ITelegramMessageHandler, TelegramMessageHandler>();
|
||||||
|
|
||||||
// Регистрируем TelegramBotService как singleton и используем один экземпляр для интерфейса и HostedService
|
// Регистрируем TelegramBotService как scoped и используем один экземпляр для интерфейса и HostedService
|
||||||
builder.Services.AddSingleton<TelegramBotService>();
|
builder.Services.AddScoped<TelegramBotService>();
|
||||||
builder.Services.AddSingleton<ITelegramBotService>(sp =>
|
builder.Services.AddScoped<ITelegramBotService>(sp =>
|
||||||
sp.GetRequiredService<TelegramBotService>()
|
sp.GetRequiredService<TelegramBotService>()
|
||||||
);
|
);
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<TelegramBotService>());
|
|
||||||
|
// Создаем обертку для HostedService, которая создает scope
|
||||||
|
builder.Services.AddSingleton<IHostedService>(sp =>
|
||||||
|
{
|
||||||
|
var serviceProvider = sp.GetRequiredService<IServiceProvider>();
|
||||||
|
var logger = sp.GetRequiredService<ILogger<TelegramBotHostedService>>();
|
||||||
|
return new TelegramBotHostedService(serviceProvider, logger);
|
||||||
|
});
|
||||||
|
|
||||||
// Регистрируем Health Checks
|
// Регистрируем Health Checks
|
||||||
builder
|
builder
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ namespace ChatBot.Services
|
|||||||
session.AddUserMessage(message, username);
|
session.AddUserMessage(message, username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save session changes to database
|
||||||
|
await _sessionStorage.SaveSessionAsync(session);
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Processing message from user {Username} in chat {ChatId} ({ChatType}): {Message}",
|
"Processing message from user {Username} in chat {ChatId} ({ChatType}): {Message}",
|
||||||
username,
|
username,
|
||||||
@@ -130,6 +133,9 @@ namespace ChatBot.Services
|
|||||||
session.AddAssistantMessage(response);
|
session.AddAssistantMessage(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save session changes to database
|
||||||
|
await _sessionStorage.SaveSessionAsync(session);
|
||||||
|
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"AI response generated for chat {ChatId} (length: {Length})",
|
"AI response generated for chat {ChatId} (length: {Length})",
|
||||||
chatId,
|
chatId,
|
||||||
@@ -149,7 +155,7 @@ namespace ChatBot.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update session parameters
|
/// Update session parameters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UpdateSessionParameters(long chatId, string? model = null)
|
public async Task UpdateSessionParametersAsync(long chatId, string? model = null)
|
||||||
{
|
{
|
||||||
var session = _sessionStorage.Get(chatId);
|
var session = _sessionStorage.Get(chatId);
|
||||||
if (session != null)
|
if (session != null)
|
||||||
@@ -158,6 +164,10 @@ namespace ChatBot.Services
|
|||||||
session.Model = model;
|
session.Model = model;
|
||||||
|
|
||||||
session.LastUpdatedAt = DateTime.UtcNow;
|
session.LastUpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Save session changes to database
|
||||||
|
await _sessionStorage.SaveSessionAsync(session);
|
||||||
|
|
||||||
_logger.LogInformation("Updated session parameters for chat {ChatId}", chatId);
|
_logger.LogInformation("Updated session parameters for chat {ChatId}", chatId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,12 +175,16 @@ namespace ChatBot.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clear chat history for a session
|
/// Clear chat history for a session
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ClearHistory(long chatId)
|
public async Task ClearHistoryAsync(long chatId)
|
||||||
{
|
{
|
||||||
var session = _sessionStorage.Get(chatId);
|
var session = _sessionStorage.Get(chatId);
|
||||||
if (session != null)
|
if (session != null)
|
||||||
{
|
{
|
||||||
session.ClearHistory();
|
session.ClearHistory();
|
||||||
|
|
||||||
|
// Save session changes to database
|
||||||
|
await _sessionStorage.SaveSessionAsync(session);
|
||||||
|
|
||||||
_logger.LogInformation("Cleared history for chat {ChatId}", chatId);
|
_logger.LogInformation("Cleared history for chat {ChatId}", chatId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
74
ChatBot/Services/DatabaseInitializationService.cs
Normal file
74
ChatBot/Services/DatabaseInitializationService.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Service for initializing database on application startup
|
||||||
|
/// </summary>
|
||||||
|
public class DatabaseInitializationService : IHostedService
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<DatabaseInitializationService> _logger;
|
||||||
|
|
||||||
|
public DatabaseInitializationService(
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ILogger<DatabaseInitializationService> 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<ChatBotDbContext>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
189
ChatBot/Services/DatabaseSessionStorage.cs
Normal file
189
ChatBot/Services/DatabaseSessionStorage.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Database implementation of session storage
|
||||||
|
/// </summary>
|
||||||
|
public class DatabaseSessionStorage : ISessionStorage
|
||||||
|
{
|
||||||
|
private readonly IChatSessionRepository _repository;
|
||||||
|
private readonly ILogger<DatabaseSessionStorage> _logger;
|
||||||
|
private readonly IHistoryCompressionService? _compressionService;
|
||||||
|
|
||||||
|
public DatabaseSessionStorage(
|
||||||
|
IChatSessionRepository repository,
|
||||||
|
ILogger<DatabaseSessionStorage> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save session changes to database
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert ChatSessionEntity to ChatSession
|
||||||
|
/// </summary>
|
||||||
|
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<ChatRole>(messageEntity.Role);
|
||||||
|
var message = new ChatMessage { Content = messageEntity.Content, Role = role };
|
||||||
|
session.AddMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,5 +98,12 @@ namespace ChatBot.Services
|
|||||||
|
|
||||||
return sessionsToRemove.Count;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,5 +31,10 @@ namespace ChatBot.Services.Interfaces
|
|||||||
/// Clean up old sessions
|
/// Clean up old sessions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int CleanupOldSessions(int hoursOld = 24);
|
int CleanupOldSessions(int hoursOld = 24);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save session changes to storage (for database implementations)
|
||||||
|
/// </summary>
|
||||||
|
Task SaveSessionAsync(ChatSession session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ namespace ChatBot.Services.Telegram.Commands
|
|||||||
public override string CommandName => "/clear";
|
public override string CommandName => "/clear";
|
||||||
public override string Description => "Очистить историю чата";
|
public override string Description => "Очистить историю чата";
|
||||||
|
|
||||||
public override Task<string> ExecuteAsync(
|
public override async Task<string> ExecuteAsync(
|
||||||
TelegramCommandContext context,
|
TelegramCommandContext context,
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_chatService.ClearHistory(context.ChatId);
|
await _chatService.ClearHistoryAsync(context.ChatId);
|
||||||
return Task.FromResult("История чата очищена. Начинаем новый разговор!");
|
return "История чата очищена. Начинаем новый разговор!";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
ChatBot/Services/TelegramBotHostedService.cs
Normal file
46
ChatBot/Services/TelegramBotHostedService.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using ChatBot.Services.Telegram.Interfaces;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ChatBot.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Hosted service wrapper for TelegramBotService to handle scoped dependencies
|
||||||
|
/// </summary>
|
||||||
|
public class TelegramBotHostedService : IHostedService
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<TelegramBotHostedService> _logger;
|
||||||
|
private ITelegramBotService? _botService;
|
||||||
|
|
||||||
|
public TelegramBotHostedService(
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ILogger<TelegramBotHostedService> 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<ITelegramBotService>();
|
||||||
|
|
||||||
|
await _botService.StartAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Stopping Telegram Bot Hosted Service...");
|
||||||
|
|
||||||
|
if (_botService != null)
|
||||||
|
{
|
||||||
|
await _botService.StopAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Override": {
|
"Override": {
|
||||||
"Microsoft": "Warning",
|
"Microsoft": "Warning",
|
||||||
|
"Microsoft.EntityFrameworkCore.Database.Connection": "Information",
|
||||||
|
"Microsoft.EntityFrameworkCore.Database.Command": "Information",
|
||||||
"System": "Warning",
|
"System": "Warning",
|
||||||
"Telegram.Bot": "Information"
|
"Telegram.Bot": "Information"
|
||||||
}
|
}
|
||||||
@@ -50,5 +52,10 @@
|
|||||||
"MaxRetryDelayMs": 30000,
|
"MaxRetryDelayMs": 30000,
|
||||||
"CompressionTimeoutSeconds": 30,
|
"CompressionTimeoutSeconds": 30,
|
||||||
"StatusCheckTimeoutSeconds": 10
|
"StatusCheckTimeoutSeconds": 10
|
||||||
|
},
|
||||||
|
"Database": {
|
||||||
|
"ConnectionString": "Host=localhost;Port=5432;Database=chatbot;Username=postgres;Password=postgres",
|
||||||
|
"EnableSensitiveDataLogging": false,
|
||||||
|
"CommandTimeout": 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user