# -*- coding: utf-8 -*- """ Модели данных для конфигурации на основе Pydantic. Используются для загрузки и валидации настроек из YAML-файлов (rag.conf и bot.conf). Обеспечивают строгую типизацию и автоматическую проверку обязательных полей. Добавлены прокси-свойства для обратной совместимости с BotConfig. """ from typing import Optional, List, Dict, Any from pydantic import BaseModel, Field, field_validator from pathlib import Path # ============================================================ # Базовые модели для секций конфигурации # ============================================================ class DatabaseConfig(BaseModel): """Настройки подключения к PostgreSQL.""" host: str = Field(..., description="Хост базы данных") port: int = Field(5432, description="Порт PostgreSQL") user: str = Field(..., description="Имя пользователя") database: str = Field(..., description="Имя базы данных") ssl: bool = Field(False, description="Использовать SSL") ssl_mode: Optional[str] = Field("prefer", description="Режим SSL (require, prefer, disable)") @field_validator('port') def validate_port(cls, v): if not 1 <= v <= 65535: raise ValueError('port must be between 1 and 65535') return v class QdrantConfig(BaseModel): """Настройки векторной базы Qdrant.""" host: str = Field(..., description="Хост Qdrant") port: int = Field(6333, description="HTTP порт Qdrant") grpc_port: int = Field(6334, description="gRPC порт Qdrant") collection: str = Field(..., description="Имя коллекции") vector_size: int = Field(1024, description="Размерность dense-векторов") distance: str = Field("Cosine", description="Метрика расстояния (Cosine или Euclid)") class GigaChatConfig(BaseModel): """Настройки GigaChat API.""" api_key: Optional[str] = Field(None, description="API-ключ (загружается из окружения)") model_embedding: str = Field("Embeddings", description="Модель для эмбеддингов") model_generation: str = Field("GigaChat-Max", description="Модель для генерации") timeout: int = Field(600, description="Таймаут запросов в секундах") temperature: float = Field(0.1, description="Температура генерации по умолчанию") class EmbeddingConfig(BaseModel): """Настройки кэширования эмбеддингов.""" cache_size: int = Field(4096, description="Размер кэша (количество элементов)") verify_ssl_certs: bool = Field(False, description="Проверять SSL-сертификаты") model: str = Field("Embeddings", description="Модель эмбеддингов") timeout: int = Field(30, description="Таймаут запроса эмбеддингов") class ChunkingConfig(BaseModel): """Настройки разбиения документов на чанки.""" enabled: bool = Field(True, description="Включить чанкинг") chunk_size_tokens: int = Field(200, description="Размер чанка в токенах") overlap_tokens: int = Field(50, description="Перекрытие между чанками в токенах") strategy: str = Field("recursive_split_by_sentences", description="Стратегия разбиения") approx_chunk_chars: int = Field(500, description="Приблизительный размер чанка в символах (fallback)") approx_overlap_chars: int = Field(100, description="Приблизительное перекрытие в символах (fallback)") class RAGConfig(BaseModel): """Параметры RAG-пайплайна.""" default_top_k: int = Field(30, description="Количество фрагментов по умолчанию") metrics_top_k: int = Field(30, description="Количество фрагментов для извлечения метрик") contradiction_top_k: int = Field(10, description="Количество фрагментов для проверки противоречий") max_context_tokens: int = Field(3000, description="Максимальное количество токенов контекста") reserved_for_answer_tokens: int = Field(1000, description="Резерв токенов для ответа") reserved_for_overhead_tokens: int = Field(200, description="Резерв для служебных нужд") max_model_tokens: int = Field(8192, description="Максимальный размер контекстного окна модели") rerank_min_length: int = Field(5000, description="Минимальная длина контекста для переранжирования (символы)") class HTTPServerConfig(BaseModel): """Настройки HTTP-сервера.""" host: str = Field("0.0.0.0", description="Хост для привязки") port: int = Field(8080, description="Порт для привязки") class HTTPConfig(BaseModel): """Таймауты HTTP-запросов.""" download_timeout: int = Field(30, description="Таймаут скачивания") upload_timeout: int = Field(60, description="Таймаут загрузки") class SummarizationConfig(BaseModel): """Настройки иерархического резюмирования.""" enable_hierarchical_summarization: bool = Field(True, description="Включить сжатие") max_tokens_for_document: int = Field(8000, description="Порог для сжатия документа (токены)") target_tokens_after_summary: int = Field(3000, description="Целевой размер после сжатия") chunk_size_tokens: int = Field(500, description="Размер блока для резюмирования") max_depth: int = Field(2, description="Максимальная глубина рекурсии") class PathsConfig(BaseModel): """Пути к каталогам.""" data_dir: str = Field("data", description="Каталог для постоянных данных") temp_dir: str = Field("temp", description="Каталог для временных файлов") prompts_dir: str = Field("prompts", description="Каталог с промптами") upload_dir: str = Field("upload", description="Каталог для загрузок") log_dir: str = Field("logs", description="Каталог для логов") log_filename: str = Field("{name}.log", description="Шаблон имени лог-файла") class FeaturesConfig(BaseModel): """Включение/отключение функций.""" file_processing: bool = Field(True, description="Обработка файлов") surgical_replace: bool = Field(True, description="Хирургическая замена") voice_recognition: bool = Field(True, description="Распознавание голоса") vision: bool = Field(True, description="OCR") archive_support: bool = Field(True, description="Поддержка архивов") ocr: bool = Field(True, description="Распознавание текста на изображениях") allow_public_knowledge: bool = Field(False, description="Разрешить использование общих знаний") max_file_size_mb: int = Field(100, description="Максимальный размер файла в МБ") max_archive_files: int = Field(100, description="Максимальное количество файлов в архиве") enable_intent_classification: bool = Field(True, description="Включить классификацию намерений") enable_self_critique: bool = Field(True, description="Включить самокритику") surgical_keywords: List[str] = Field(["замени", "заменить"], description="Ключевые слова для хирургической замены") mention_keyword: Optional[str] = Field(None, description="Ключевое слово для упоминания бота в группе") class CleanupConfig(BaseModel): """Настройки очистки старых файлов.""" temp_days: int = Field(3, description="Срок хранения временных файлов (дни)") upload_days: int = Field(7, description="Срок хранения загруженных файлов (дни)") interval_seconds: int = Field(3600, description="Интервал очистки (секунды)") user_data_days: int = Field(30, description="Срок хранения пользовательских данных (дни)") class FilePermissionsConfig(BaseModel): """Права доступа к создаваемым файлам.""" mode: str = Field("0644", description="Права в восьмеричном формате (строка)") class AIConfig(BaseModel): """Настройки AI (температура, таймауты).""" model: Optional[str] = Field(None, description="Модель генерации (если не указана, берётся из GigaChatConfig)") temperature: float = Field(0.1, description="Температура генерации") timeout: int = Field(600, description="Таймаут генерации") prompts: Dict[str, str] = Field(..., description="Имена файлов промптов (ключ -> имя файла)") class PromptTemperatures(BaseModel): """Температуры для отдельных функций.""" temperature: float = Field(0.1, description="Температура") class PromptsConfig(BaseModel): """Секция с температурами для разных функций.""" intent: PromptTemperatures = Field(default_factory=lambda: PromptTemperatures(temperature=0.1)) expand: PromptTemperatures = Field(default_factory=lambda: PromptTemperatures(temperature=0.1)) metrics: PromptTemperatures = Field(default_factory=lambda: PromptTemperatures(temperature=0.1)) summary: PromptTemperatures = Field(default_factory=lambda: PromptTemperatures(temperature=0.1)) summary_max_chars: int = Field(8000, description="Максимальная длина текста для суммаризации") consistency: PromptTemperatures = Field(default_factory=lambda: PromptTemperatures(temperature=0.1)) consistency_max_fragments: int = Field(5, description="Максимальное количество фрагментов для проверки противоречий") critique: PromptTemperatures = Field(default_factory=lambda: PromptTemperatures(temperature=0.1)) rerank: PromptTemperatures = Field(default_factory=lambda: PromptTemperatures(temperature=0.1)) generate_document: PromptTemperatures = Field(default_factory=lambda: PromptTemperatures(temperature=0.1)) class WebScraperConfig(BaseModel): """Настройки веб-скрапера.""" max_pages: int = Field(5, description="Максимальное количество страниц для рекурсивного обхода") max_depth: int = Field(1, description="Максимальная глубина рекурсии") class AuthConfig(BaseModel): """Настройки аутентификации.""" api_key_env: str = Field("RAG_API_KEY", description="Имя переменной окружения с API-ключом для доступа к RAG-серверу") class FewshotConfig(BaseModel): """Настройки few-shot примеров.""" max_examples: int = Field(3, description="Максимальное количество примеров, включаемых в промт") examples_file: str = Field("data/fewshot_examples.json", description="Путь к файлу с примерами (относительно профиля)") # ============================================================ # Главная модель конфигурации приложения # ============================================================ class AppConfig(BaseModel): """ Полная конфигурация приложения. Объединяет все секции. """ name: str = Field(..., description="Имя профиля бота") jid: str = Field(..., description="JID бота (user@domain)") resource: str = Field("bot", description="Ресурс XMPP") status_message: str = Field(..., description="Сообщение статуса присутствия") xmpp_server: Optional[str] = Field(None, description="XMPP сервер (если не указан, берётся из JID)") xmpp_port: int = Field(5222, description="Порт XMPP") xmpp_use_ssl: bool = Field(False, description="Использовать SSL для XMPP") admin_jids: List[str] = Field([], description="Список JID администраторов") log_level: str = Field("INFO", description="Уровень логирования") rag_server_url: str = Field("http://localhost:8080", description="URL RAG-сервера") xmpp_password: Optional[str] = Field(None, description="Пароль XMPP (из окружения)") salute_speech_auth: Optional[str] = Field(None, description="Авторизация для SaluteSpeech (из окружения)") # Секции database: DatabaseConfig qdrant: QdrantConfig gigachat: GigaChatConfig embedding: EmbeddingConfig chunking: ChunkingConfig rag: RAGConfig http_server: HTTPServerConfig http: HTTPConfig summarization: SummarizationConfig paths: PathsConfig features: FeaturesConfig cleanup: CleanupConfig file_permissions: FilePermissionsConfig ai: AIConfig prompts: PromptsConfig web_scraper: WebScraperConfig = Field(default_factory=WebScraperConfig) auth: AuthConfig = Field(default_factory=AuthConfig) fewshot: FewshotConfig = Field(default_factory=FewshotConfig) # Дополнительные поля для путей (заполняются при загрузке) profile_dir: Optional[Path] = Field(None, description="Корень профиля") data_dir_path: Optional[Path] = Field(None) temp_dir_path: Optional[Path] = Field(None) prompts_dir_path: Optional[Path] = Field(None) upload_dir_path: Optional[Path] = Field(None) log_dir_path: Optional[Path] = Field(None) log_file_path: Optional[Path] = Field(None) # Промпты (загружаются из файлов) prompts_content: Optional[Dict[str, str]] = Field(default_factory=dict, description="Содержимое промптов") # Приватное поле для API-ключа RAG (не сериализуется) _rag_api_key: Optional[str] = Field(None, description="API-ключ RAG-сервера", exclude=True) # ------------------------------------------------------------------ # Прокси-свойства для обратной совместимости со старым BotConfig # ------------------------------------------------------------------ @property def rag_api_key(self) -> Optional[str]: return self._rag_api_key @property def db_host(self) -> str: return self.database.host @property def db_port(self) -> int: return self.database.port @property def db_user(self) -> str: return self.database.user @property def db_password(self) -> str: return self.database.password @property def db_name(self) -> str: return self.database.database @property def db_ssl(self) -> bool: return self.database.ssl @property def db_ssl_mode(self) -> Optional[str]: return self.database.ssl_mode @property def qdrant_host(self) -> str: return self.qdrant.host @property def qdrant_port(self) -> int: return self.qdrant.port @property def qdrant_grpc_port(self) -> int: return self.qdrant.grpc_port @property def qdrant_collection(self) -> str: return self.qdrant.collection @property def qdrant_vector_size(self) -> int: return self.qdrant.vector_size @property def qdrant_distance(self) -> str: return self.qdrant.distance @property def gigachat_api_key(self) -> Optional[str]: return self.gigachat.api_key @property def gigachat_model_embedding(self) -> str: return self.gigachat.model_embedding @property def gigachat_model_generation(self) -> str: return self.gigachat.model_generation @property def gigachat_timeout(self) -> int: return self.gigachat.timeout @property def gigachat_temperature(self) -> float: return self.gigachat.temperature @property def embedding_cache_size(self) -> int: return self.embedding.cache_size @property def embedding_model(self) -> str: return self.embedding.model @property def embedding_timeout(self) -> int: return self.embedding.timeout @property def embedding_verify_ssl(self) -> bool: return self.embedding.verify_ssl_certs @property def chunking_enabled(self) -> bool: return self.chunking.enabled @property def chunk_size_tokens(self) -> int: return self.chunking.chunk_size_tokens @property def overlap_tokens(self) -> int: return self.chunking.overlap_tokens @property def chunking_strategy(self) -> str: return self.chunking.strategy @property def chunking_approx_chunk_chars(self) -> int: return self.chunking.approx_chunk_chars @property def chunking_approx_overlap_chars(self) -> int: return self.chunking.approx_overlap_chars @property def rag_default_top_k(self) -> int: return self.rag.default_top_k @property def rag_metrics_top_k(self) -> int: return self.rag.metrics_top_k @property def rag_contradiction_top_k(self) -> int: return self.rag.contradiction_top_k @property def max_context_tokens(self) -> int: return self.rag.max_context_tokens @property def reserved_for_answer_tokens(self) -> int: return self.rag.reserved_for_answer_tokens @property def reserved_for_overhead_tokens(self) -> int: return self.rag.reserved_for_overhead_tokens @property def max_model_tokens(self) -> int: return self.rag.max_model_tokens @property def rerank_min_length(self) -> int: return self.rag.rerank_min_length @property def http_download_timeout(self) -> int: return self.http.download_timeout @property def http_upload_timeout(self) -> int: return self.http.upload_timeout @property def file_processing(self) -> bool: return self.features.file_processing @property def surgical_replace(self) -> bool: return self.features.surgical_replace @property def voice_recognition(self) -> bool: return self.features.voice_recognition @property def vision(self) -> bool: return self.features.vision @property def archive_support(self) -> bool: return self.features.archive_support @property def ocr(self) -> bool: return self.features.ocr @property def allow_public_knowledge(self) -> bool: return self.features.allow_public_knowledge @property def max_file_size_mb(self) -> int: return self.features.max_file_size_mb @property def max_archive_files(self) -> int: return self.features.max_archive_files @property def enable_intent_classification(self) -> bool: return self.features.enable_intent_classification @property def enable_self_critique(self) -> bool: return self.features.enable_self_critique @property def surgical_keywords(self) -> List[str]: return self.features.surgical_keywords @property def mention_keyword(self) -> Optional[str]: return self.features.mention_keyword @property def ai_model(self) -> str: return self.ai.model or self.gigachat.model_generation @property def ai_temperature(self) -> float: return self.ai.temperature @property def ai_timeout(self) -> int: return self.ai.timeout @property def temp_cleanup_days(self) -> int: return self.cleanup.temp_days @property def upload_cleanup_days(self) -> int: return self.cleanup.upload_days @property def cleanup_interval(self) -> int: return self.cleanup.interval_seconds @property def user_data_cleanup_days(self) -> int: return self.cleanup.user_data_days @property def ws_max_pages(self) -> int: return self.web_scraper.max_pages @property def ws_max_depth(self) -> int: return self.web_scraper.max_depth @property def intent_temperature(self) -> float: return self.prompts.intent.temperature @property def expand_temperature(self) -> float: return self.prompts.expand.temperature @property def metrics_temperature(self) -> float: return self.prompts.metrics.temperature @property def summary_temperature(self) -> float: return self.prompts.summary.temperature @property def summary_max_chars(self) -> int: return self.prompts.summary_max_chars @property def consistency_temperature(self) -> float: return self.prompts.consistency.temperature @property def consistency_max_fragments(self) -> int: return self.prompts.consistency_max_fragments @property def critique_temperature(self) -> float: return self.prompts.critique.temperature @property def rerank_temperature(self) -> float: return self.prompts.rerank.temperature @property def generate_document_temperature(self) -> float: return self.prompts.generate_document.temperature @property def file_mode(self) -> int: return int(self.file_permissions.mode, 8) @property def max_fewshot_examples(self) -> int: return self.fewshot.max_examples @property def fewshot_examples_file(self) -> str: return self.fewshot.examples_file class Config: extra = "forbid" # Запрещаем неизвестные поля