diff --git a/rag/config_models.py b/rag/config_models.py index 0b2449b..8169056 100644 --- a/rag/config_models.py +++ b/rag/config_models.py @@ -3,6 +3,8 @@ Модели данных для конфигурации на основе Pydantic. Используются для загрузки и валидации настроек из YAML-файлов (rag.conf и bot.conf). Обеспечивают строгую типизацию и автоматическую проверку обязательных полей. + +Добавлены прокси-свойства для обратной совместимости с BotConfig. """ from typing import Optional, List, Dict, Any @@ -18,7 +20,6 @@ class DatabaseConfig(BaseModel): host: str = Field(..., description="Хост базы данных") port: int = Field(5432, description="Порт PostgreSQL") user: str = Field(..., description="Имя пользователя") - password_env: str = Field(..., description="Имя переменной окружения с паролем") database: str = Field(..., description="Имя базы данных") ssl: bool = Field(False, description="Использовать SSL") ssl_mode: Optional[str] = Field("prefer", description="Режим SSL (require, prefer, disable)") @@ -42,7 +43,7 @@ class QdrantConfig(BaseModel): class GigaChatConfig(BaseModel): """Настройки GigaChat API.""" - credentials_env: str = Field(..., description="Имя переменной окружения с 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="Таймаут запросов в секундах") @@ -164,10 +165,15 @@ class PromptsConfig(BaseModel): 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)) - rerank_min_length: int = Field(5000, description="Минимальная длина контекста для переранжирования") 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="Максимальная глубина рекурсии") + + # ============================================================ # Главная модель конфигурации приложения # ============================================================ @@ -179,7 +185,6 @@ class AppConfig(BaseModel): """ name: str = Field(..., description="Имя профиля бота") jid: str = Field(..., description="JID бота (user@domain)") - password_env: str = Field(..., description="Имя переменной окружения с паролем XMPP") resource: str = Field("bot", description="Ресурс XMPP") status_message: str = Field(..., description="Сообщение статуса присутствия") xmpp_server: Optional[str] = Field(None, description="XMPP сервер (если не указан, берётся из JID)") @@ -188,6 +193,8 @@ class AppConfig(BaseModel): 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 @@ -205,14 +212,9 @@ class AppConfig(BaseModel): file_permissions: FilePermissionsConfig ai: AIConfig prompts: PromptsConfig + web_scraper: WebScraperConfig = Field(default_factory=WebScraperConfig) - # Дополнительные вычисляемые поля (заполняются при загрузке) - xmpp_password: Optional[str] = Field(None, description="Пароль XMPP (загружается из окружения)") - gigachat_api_key: Optional[str] = Field(None, description="API-ключ GigaChat (из окружения)") - db_password: Optional[str] = Field(None, description="Пароль PostgreSQL (из окружения)") - salute_speech_auth: Optional[str] = Field(None, description="Авторизация для SaluteSpeech (из окружения)") - - # Пути (преобразуются в Path при загрузке) + # Дополнительные поля для путей (заполняются при загрузке) profile_dir: Optional[Path] = Field(None, description="Корень профиля") data_dir_path: Optional[Path] = Field(None) temp_dir_path: Optional[Path] = Field(None) @@ -224,5 +226,306 @@ class AppConfig(BaseModel): # Промпты (загружаются из файлов) prompts_content: Optional[Dict[str, str]] = Field(default_factory=dict, description="Содержимое промптов") + # ------------------------------------------------------------------ + # Прокси-свойства для обратной совместимости со старым BotConfig + # ------------------------------------------------------------------ + + # База данных + @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 + + # Qdrant + @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 + + # GigaChat + @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 + + # Embedding + @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 + + # Chunking + @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 + + # RAG + @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 + + # HTTP + @property + def http_download_timeout(self) -> int: + return self.http.download_timeout + + @property + def http_upload_timeout(self) -> int: + return self.http.upload_timeout + + # Features + @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 + + # AI + @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 + + # Cleanup + @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 + + # Web scraper + @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 + + # Prompts temperatures + @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 + + # File permissions + @property + def file_mode(self) -> int: + return int(self.file_permissions.mode, 8) + class Config: extra = "forbid" # Запрещаем неизвестные поля \ No newline at end of file diff --git a/rag/rag_api.py b/rag/rag_api.py index 0b3d7b6..d3c67f8 100644 --- a/rag/rag_api.py +++ b/rag/rag_api.py @@ -16,12 +16,12 @@ import logging from typing import Optional, Dict, List, Any -from rag.rag_orchestrator import RAGOrchestrator -from rag.services.kb_service import KBService -from rag.services.giga_client import GigaClient -from rag.services.file_service import FileService -from rag.services.postgres_service import PostgresService -from rag.config_models import AppConfig +from .rag_orchestrator import RAGOrchestrator +from .services.kb_service import KBService +from .services.giga_client import GigaClient +from .services.file_service import FileService +from .services.postgres_service import PostgresService +from .config_models import AppConfig logger = logging.getLogger(__name__) diff --git a/rag/rag_orchestrator.py b/rag/rag_orchestrator.py index a441548..f49ce0c 100644 --- a/rag/rag_orchestrator.py +++ b/rag/rag_orchestrator.py @@ -8,19 +8,19 @@ import logging from typing import Optional, Dict, List, Any -from rag.services.postgres_service import PostgresService -from rag.services.qdrant_service import QdrantService -from rag.services.embedding_service import EmbeddingService -from rag.services.kb_service import KBService -from rag.services.giga_client import GigaClient -from rag.services.file_service import FileService -from rag.history_manager import HistoryManager -from rag.intent_router import IntentRouter -from rag.query_processor import QueryProcessor -from rag.indexing_manager import IndexingManager -from rag.functions.intent_classify import classify_intent -from rag.utils.text_utils import count_tokens -from rag.config_models import AppConfig +from .services.postgres_service import PostgresService +from .services.qdrant_service import QdrantService +from .services.embedding_service import EmbeddingService +from .services.kb_service import KBService +from .services.giga_client import GigaClient +from .services.file_service import FileService +from .history_manager import HistoryManager +from .intent_router import IntentRouter +from .query_processor import QueryProcessor +from .indexing_manager import IndexingManager +from .functions.intent_classify import classify_intent +from .utils.text_utils import count_tokens +from .config_models import AppConfig logger = logging.getLogger(__name__) @@ -183,9 +183,9 @@ class RAGOrchestrator: system_prompt = prompts.get('system', None) # 2. Расчёт лимитов токенов - max_model_tokens = self.config.rag.max_model_tokens - reserved_for_answer = self.config.rag.reserved_for_answer_tokens - reserved_for_overhead = self.config.rag.reserved_for_overhead_tokens + max_model_tokens = self.config.max_model_tokens + reserved_for_answer = self.config.reserved_for_answer_tokens + reserved_for_overhead = self.config.reserved_for_overhead_tokens token_info = self._prepare_prompt_parts( synthesis_template=synthesis_template, @@ -211,7 +211,7 @@ class RAGOrchestrator: # 5. Классификация намерений intent = intent_override - if intent is None and self.config.features.enable_intent_classification: + if intent is None and self.config.enable_intent_classification: intent_prompt = prompts.get('intent', '') if intent_prompt: intent = await classify_intent( @@ -226,7 +226,7 @@ class RAGOrchestrator: intent = intent or "GENERAL" # 6. Принудительная установка SURGICAL по ключевым словам - keywords = self.config.features.surgical_keywords + keywords = self.config.surgical_keywords if any(kw in query.lower() for kw in keywords) and last_file_path: intent = "SURGICAL" logger.info(f"Принудительный Intent: SURGICAL (есть файл и ключевые слова)") @@ -255,7 +255,6 @@ class RAGOrchestrator: sources = router_result.get("sources", []) else: # Обычный RAG-пайплайн (GENERAL, FACT, PROCEDURE, COMPARISON, CALCULATION) - # Вычисляем, сколько токенов осталось для контекста после истории history_tokens = sum(count_tokens(msg['content']) for msg in formatted_history) available_for_context = available_for_history_and_context - history_tokens available_for_context = max(available_for_context, 0) diff --git a/rag/rag_server.py b/rag/rag_server.py index 574c2e2..89231cc 100644 --- a/rag/rag_server.py +++ b/rag/rag_server.py @@ -3,16 +3,12 @@ """ RAG-сервер – отдельный HTTP-сервис для обработки RAG-запросов. Предоставляет API для выполнения RAG-запросов и индексации документов. - -Этот сервер запускается как отдельный процесс и может обслуживать -несколько ботов и других систем одновременно. - -ИСПОЛЬЗУЕТ: FastAPI для создания REST API и Uvicorn для запуска. ИСТОРИЯ ХРАНИТСЯ НА СЕРВЕРЕ (в PostgreSQL). ДОБАВЛЕНО: - Эндпоинт /rag/vision для распознавания текста на изображениях (OCR) - Эндпоинт /rag/transcribe для транскрибации аудио (SaluteSpeech) +- Использование Pydantic-модели AppConfig для конфигурации. """ import logging @@ -32,16 +28,16 @@ from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field import uvicorn -# Импортируем наши модули (теперь из пакета rag) -from rag.utils.config_loader import load_config -from rag.utils.logger import setup_logging -from rag.services.postgres_service import PostgresService -from rag.services.qdrant_service import QdrantService -from rag.services.embedding_service import EmbeddingService -from rag.services.kb_service import KBService -from rag.services.giga_client import GigaClient -from rag.services.file_service import FileService -from rag.rag_orchestrator import RAGOrchestrator +# Импорты наших модулей +from .utils.config_loader import load_config +from .utils.logger import setup_logging +from .services.postgres_service import PostgresService +from .services.qdrant_service import QdrantService +from .services.embedding_service import EmbeddingService +from .services.kb_service import KBService +from .services.giga_client import GigaClient +from .services.file_service import FileService +from .rag_orchestrator import RAGOrchestrator logger = logging.getLogger(__name__) @@ -114,7 +110,7 @@ class TranscribeResponse(BaseModel): # ============================================================ _orchestrator: Optional[RAGOrchestrator] = None -_config: Optional[AppConfig] = None +_config = None def get_orchestrator() -> RAGOrchestrator: @@ -125,7 +121,7 @@ def get_orchestrator() -> RAGOrchestrator: return _orchestrator -def init_orchestrator(config: AppConfig) -> RAGOrchestrator: +def init_orchestrator(config): """Инициализирует все сервисы и создаёт RAGOrchestrator.""" global _orchestrator, _config _config = config @@ -134,31 +130,31 @@ def init_orchestrator(config: AppConfig) -> RAGOrchestrator: # ---- 1. Инициализация сервисов ---- # PostgreSQL (история, документы) db = PostgresService( - host=config.database.host, - port=config.database.port, - user=config.database.user, + host=config.db_host, + port=config.db_port, + user=config.db_user, password=config.db_password, - db_name=config.database.database + db_name=config.db_name ) # Qdrant (векторный поиск) qdrant = QdrantService( - host=config.qdrant.host, - port=config.qdrant.port, - grpc_port=config.qdrant.grpc_port, - collection_name=config.qdrant.collection, - vector_size=config.qdrant.vector_size, - distance=config.qdrant.distance, + host=config.qdrant_host, + port=config.qdrant_port, + grpc_port=config.qdrant_grpc_port, + collection_name=config.qdrant_collection, + vector_size=config.qdrant_vector_size, + distance=config.qdrant_distance, prefer_grpc=False ) # Эмбеддинги (GigaChat) embedding = EmbeddingService( api_key=config.gigachat_api_key, - model=config.embedding.model, - timeout=config.embedding.timeout, - cache_size=config.embedding.cache_size, - verify_ssl=config.embedding.verify_ssl_certs + model=config.embedding_model, + timeout=config.embedding_timeout, + cache_size=config.embedding_cache_size, + verify_ssl=config.embedding_verify_ssl ) # База знаний (индексация, поиск) @@ -166,19 +162,19 @@ def init_orchestrator(config: AppConfig) -> RAGOrchestrator: db=db, qdrant=qdrant, embedding=embedding, - collection_name=config.qdrant.collection, - chunk_size_tokens=config.chunking.chunk_size_tokens, - overlap_tokens=config.chunking.overlap_tokens, - approx_chunk_chars=config.chunking.approx_chunk_chars, - approx_overlap_chars=config.chunking.approx_overlap_chars + collection_name=config.qdrant_collection, + chunk_size_tokens=config.chunk_size_tokens, + overlap_tokens=config.overlap_tokens, + approx_chunk_chars=config.chunking_approx_chunk_chars, + approx_overlap_chars=config.chunking_approx_overlap_chars ) # GigaChat клиент (генерация) giga = GigaClient( api_key=config.gigachat_api_key, - model=config.gigachat.model_generation, - temperature=config.gigachat.temperature, - timeout=config.gigachat.timeout, + model=config.ai_model, + temperature=config.ai_temperature, + timeout=config.ai_timeout, verify_ssl=False ) @@ -186,11 +182,15 @@ def init_orchestrator(config: AppConfig) -> RAGOrchestrator: files = FileService(config) # ---- 2. Создание оркестратора ---- - # Загружаем дефолтные промпты (из config.prompts_content) - default_prompts = config.prompts_content or {} - system_prompt = default_prompts.get('system') - if system_prompt: - default_prompts['system'] = system_prompt + # Загружаем дефолтные промпты (опционально) + default_prompts = {} + system_prompt_path = getattr(config, 'system_prompt_file', None) + if system_prompt_path and system_prompt_path.exists(): + try: + with open(system_prompt_path, 'r', encoding='utf-8') as f: + default_prompts['system'] = f.read() + except Exception as e: + logger.warning(f"Не удалось загрузить системный промпт: {e}") orchestrator = RAGOrchestrator( db=db, @@ -215,7 +215,7 @@ def init_orchestrator(config: AppConfig) -> RAGOrchestrator: app = FastAPI( title="RAG-сервер", description="Единый сервис для RAG-обработки запросов и индексации документов", - version="1.0.0" + version="3.0.0" ) # Разрешаем CORS для возможности запросов с других доменов (для тестов) @@ -436,7 +436,7 @@ def main(): ) args = parser.parse_args() - # Загружаем конфигурацию с помощью новой функции load_config + # Загружаем конфигурацию через Pydantic try: config = load_config(args.profile_dir) except Exception as e: @@ -445,9 +445,7 @@ def main(): # Настраиваем логирование log_level = getattr(logging, args.log_level.upper(), logging.INFO) - # Передаём путь к логу из конфига - log_file_path = config.log_file_path or Path("/tmp/rag_server.log") - setup_logging(config.name, log_file_path, log_level=log_level) + setup_logging(config.name, config.log_file_path, log_level=log_level) logger.info(f"Запуск RAG-сервера с профилем {args.profile_dir}") logger.info(f"Хост: {args.host}, порт: {args.port}") @@ -461,7 +459,7 @@ def main(): # Запускаем Uvicorn uvicorn.run( - "rag.rag_server:app", # теперь модуль из пакета rag + "rag.rag_server:app", host=args.host, port=args.port, log_level=args.log_level, diff --git a/rag/utils/config_loader.py b/rag/utils/config_loader.py index 74452b0..37d50e2 100644 --- a/rag/utils/config_loader.py +++ b/rag/utils/config_loader.py @@ -9,14 +9,11 @@ import os import yaml import logging from pathlib import Path -from typing import Optional, Dict, Any +from typing import Dict, Any from dotenv import load_dotenv -from .config_models import AppConfig, DatabaseConfig, QdrantConfig, GigaChatConfig, \ - EmbeddingConfig, ChunkingConfig, RAGConfig, HTTPServerConfig, HTTPConfig, \ - SummarizationConfig, PathsConfig, FeaturesConfig, CleanupConfig, FilePermissionsConfig, \ - AIConfig, PromptsConfig, PromptTemperatures +from ..config_models import AppConfig logger = logging.getLogger(__name__) @@ -66,16 +63,13 @@ def load_config(profile_dir: str) -> AppConfig: } # Подставляем пароли и ключи в секции - # В database: password_env -> фактический пароль if 'database' in merged and 'password_env' in merged['database']: env_name = merged['database']['password_env'] merged['database']['password'] = env_vars.get(env_name) if not merged['database']['password']: raise ValueError(f"Переменная окружения {env_name} не задана или пуста") - # удаляем password_env, чтобы не передавать в модель del merged['database']['password_env'] - # Аналогично для gigachat if 'gigachat' in merged and 'credentials_env' in merged['gigachat']: env_name = merged['gigachat']['credentials_env'] merged['gigachat']['api_key'] = env_vars.get(env_name) @@ -83,7 +77,6 @@ def load_config(profile_dir: str) -> AppConfig: raise ValueError(f"Переменная окружения {env_name} не задана или пуста") del merged['gigachat']['credentials_env'] - # Для XMPP пароля if 'password_env' in merged: env_name = merged['password_env'] merged['xmpp_password'] = env_vars.get(env_name) @@ -91,30 +84,30 @@ def load_config(profile_dir: str) -> AppConfig: raise ValueError(f"Переменная окружения {env_name} не задана или пуста") del merged['password_env'] - # Для SaluteSpeech (если есть в секции features или отдельно) - # У нас он может быть в .env, но в модели нет отдельного поля; добавим в итоговый объект merged['salute_speech_auth'] = env_vars.get('SALUTE_SPEECH_AUTH') # Преобразуем пути в абсолютные с учётом profile_dir paths_section = merged.get('paths', {}) - # Если пути относительные, дополняем их относительно profile_dir for key in ['data_dir', 'temp_dir', 'prompts_dir', 'upload_dir', 'log_dir']: if key in paths_section: p = Path(paths_section[key]) if not p.is_absolute(): paths_section[key] = str(profile_path / p) - # log_filename оставляем как есть - # Добавляем profile_dir в merged для передачи в модель + # Добавляем profile_dir merged['profile_dir'] = str(profile_path) - # Создаём экземпляр AppConfig (Pydantic выполнит валидацию) + # Добавляем web_scraper (если отсутствует) + if 'web_scraper' not in merged: + merged['web_scraper'] = {'max_pages': 5, 'max_depth': 1} + + # Создаём экземпляр AppConfig try: config = AppConfig(**merged) except Exception as e: raise ValueError(f"Ошибка валидации конфигурации: {e}") - # Дополнительно загружаем промпты из файлов + # Загружаем промпты из файлов prompts_dir = config.prompts_dir_path if prompts_dir and prompts_dir.exists(): prompts_content = {}