Files
fckbot/rag/utils/config_loader.py
2026-06-30 13:57:28 +00:00

148 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Загрузчик конфигурации с использованием Pydantic.
Читает YAML-файлы (bot.conf и rag.conf), подставляет переменные окружения,
создаёт экземпляр AppConfig.
"""
import os
import yaml
import logging
from pathlib import Path
from typing import Optional, 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
logger = logging.getLogger(__name__)
# Загружаем переменные окружения из .env
load_dotenv()
def load_config(profile_dir: str) -> AppConfig:
"""
Загружает конфигурацию из профиля.
Аргументы:
profile_dir: путь к каталогу профиля бота (например, /usr/local/etc/bots/metabot)
Возвращает:
AppConfig: экземпляр модели конфигурации.
"""
profile_path = Path(profile_dir).resolve()
bot_config_path = profile_path / "bot.conf"
if not bot_config_path.exists():
raise FileNotFoundError(f"Файл конфигурации бота не найден: {bot_config_path}")
# Загружаем bot.conf
with open(bot_config_path, 'r', encoding='utf-8') as f:
bot_raw = yaml.safe_load(f) or {}
# Загружаем rag.conf (общий для всех ботов) ищем в родительском каталоге или рядом
rag_config_path = profile_path.parent / "rag" / "rag.conf"
if not rag_config_path.exists():
# fallback: возможно, rag.conf лежит в той же директории, что и profile
rag_config_path = profile_path / "rag.conf"
if not rag_config_path.exists():
raise FileNotFoundError(f"Файл rag.conf не найден: {rag_config_path}")
with open(rag_config_path, 'r', encoding='utf-8') as f:
rag_raw = yaml.safe_load(f) or {}
# Глубокое слияние: bot.conf имеет приоритет над rag.conf
merged = deep_merge(rag_raw, bot_raw)
# Извлекаем переменные окружения
env_vars = {
'XMPP_PASSWORD': os.getenv('XMPP_PASSWORD'),
'DB_PASSWORD': os.getenv('DB_PASSWORD'),
'GIGACHAT_API_KEY': os.getenv('GIGACHAT_API_KEY'),
'SALUTE_SPEECH_AUTH': os.getenv('SALUTE_SPEECH_AUTH'),
}
# Подставляем пароли и ключи в секции
# В 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)
if not merged['gigachat']['api_key']:
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)
if not merged['xmpp_password']:
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 для передачи в модель
merged['profile_dir'] = str(profile_path)
# Создаём экземпляр AppConfig (Pydantic выполнит валидацию)
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 = {}
for prompt_name, filename in config.ai.prompts.items():
file_path = prompts_dir / filename
if file_path.exists():
try:
with open(file_path, 'r', encoding='utf-8') as f:
prompts_content[prompt_name] = f.read()
except Exception as e:
logger.warning(f"Не удалось загрузить промпт {prompt_name} из {file_path}: {e}")
else:
logger.warning(f"Файл промпта {filename} не найден в {prompts_dir}")
config.prompts_content = prompts_content
else:
logger.warning(f"Каталог промптов не существует: {prompts_dir}")
return config
def deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
"""
Рекурсивное слияние словарей: override имеет приоритет над base.
"""
result = base.copy()
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = value
return result