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.Settings.Configuration" 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>
|
||||
<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.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<AISettings>(builder.Configuration.GetSection("AI"))
|
||||
.AddSingleton<IValidateOptions<AISettings>, AISettingsValidator>();
|
||||
|
||||
builder
|
||||
.Services.Configure<DatabaseSettings>(builder.Configuration.GetSection("Database"))
|
||||
.AddSingleton<IValidateOptions<DatabaseSettings>, DatabaseSettingsValidator>();
|
||||
|
||||
// Валидируем конфигурацию при старте
|
||||
builder.Services.AddOptions<TelegramBotSettings>().ValidateOnStart();
|
||||
builder.Services.AddOptions<OllamaSettings>().ValidateOnStart();
|
||||
builder.Services.AddOptions<AISettings>().ValidateOnStart();
|
||||
builder.Services.AddOptions<DatabaseSettings>().ValidateOnStart();
|
||||
|
||||
// Регистрируем IOllamaClient
|
||||
builder.Services.AddSingleton<IOllamaClient>(sp =>
|
||||
@@ -47,22 +56,49 @@ try
|
||||
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<SystemPromptService>();
|
||||
builder.Services.AddSingleton<IHistoryCompressionService, HistoryCompressionService>();
|
||||
builder.Services.AddSingleton<IAIService, AIService>();
|
||||
builder.Services.AddSingleton<ChatService>();
|
||||
builder.Services.AddScoped<ChatService>();
|
||||
|
||||
// Регистрируем сервис инициализации базы данных
|
||||
builder.Services.AddHostedService<DatabaseInitializationService>();
|
||||
|
||||
// Регистрируем Telegram команды
|
||||
builder.Services.AddSingleton<ITelegramCommand, StartCommand>();
|
||||
builder.Services.AddSingleton<ITelegramCommand, HelpCommand>();
|
||||
builder.Services.AddSingleton<ITelegramCommand, ClearCommand>();
|
||||
builder.Services.AddSingleton<ITelegramCommand, SettingsCommand>();
|
||||
builder.Services.AddSingleton<ITelegramCommand, StatusCommand>();
|
||||
builder.Services.AddScoped<ITelegramCommand, StartCommand>();
|
||||
builder.Services.AddScoped<ITelegramCommand, HelpCommand>();
|
||||
builder.Services.AddScoped<ITelegramCommand, ClearCommand>();
|
||||
builder.Services.AddScoped<ITelegramCommand, SettingsCommand>();
|
||||
builder.Services.AddScoped<ITelegramCommand, StatusCommand>();
|
||||
|
||||
// Регистрируем Telegram сервисы
|
||||
builder.Services.AddSingleton<ITelegramBotClient>(provider =>
|
||||
@@ -72,17 +108,24 @@ try
|
||||
});
|
||||
builder.Services.AddSingleton<ITelegramMessageSender, TelegramMessageSender>();
|
||||
builder.Services.AddSingleton<ITelegramErrorHandler, TelegramErrorHandler>();
|
||||
builder.Services.AddSingleton<CommandRegistry>();
|
||||
builder.Services.AddScoped<CommandRegistry>();
|
||||
builder.Services.AddSingleton<BotInfoService>();
|
||||
builder.Services.AddSingleton<ITelegramCommandProcessor, TelegramCommandProcessor>();
|
||||
builder.Services.AddSingleton<ITelegramMessageHandler, TelegramMessageHandler>();
|
||||
builder.Services.AddScoped<ITelegramCommandProcessor, TelegramCommandProcessor>();
|
||||
builder.Services.AddScoped<ITelegramMessageHandler, TelegramMessageHandler>();
|
||||
|
||||
// Регистрируем TelegramBotService как singleton и используем один экземпляр для интерфейса и HostedService
|
||||
builder.Services.AddSingleton<TelegramBotService>();
|
||||
builder.Services.AddSingleton<ITelegramBotService>(sp =>
|
||||
// Регистрируем TelegramBotService как scoped и используем один экземпляр для интерфейса и HostedService
|
||||
builder.Services.AddScoped<TelegramBotService>();
|
||||
builder.Services.AddScoped<ITelegramBotService>(sp =>
|
||||
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
|
||||
builder
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Update session parameters
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// Clear chat history for a session
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
/// </summary>
|
||||
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 Description => "Очистить историю чата";
|
||||
|
||||
public override Task<string> ExecuteAsync(
|
||||
public override async Task<string> ExecuteAsync(
|
||||
TelegramCommandContext context,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
_chatService.ClearHistory(context.ChatId);
|
||||
return Task.FromResult("История чата очищена. Начинаем новый разговор!");
|
||||
await _chatService.ClearHistoryAsync(context.ChatId);
|
||||
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",
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user