Update 5 files

- /rag/config_models.py
- /rag/utils/config_loader.py
- /rag/rag_orchestrator.py
- /rag/rag_server.py
- /rag/rag_api.py
This commit is contained in:
Markov Andrey
2026-06-30 14:11:06 +00:00
parent a3f52888db
commit 068504c988
5 changed files with 395 additions and 102 deletions

View File

@@ -3,6 +3,8 @@
Модели данных для конфигурации на основе Pydantic. Модели данных для конфигурации на основе Pydantic.
Используются для загрузки и валидации настроек из YAML-файлов (rag.conf и bot.conf). Используются для загрузки и валидации настроек из YAML-файлов (rag.conf и bot.conf).
Обеспечивают строгую типизацию и автоматическую проверку обязательных полей. Обеспечивают строгую типизацию и автоматическую проверку обязательных полей.
Добавлены прокси-свойства для обратной совместимости с BotConfig.
""" """
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
@@ -18,7 +20,6 @@ class DatabaseConfig(BaseModel):
host: str = Field(..., description="Хост базы данных") host: str = Field(..., description="Хост базы данных")
port: int = Field(5432, description="Порт PostgreSQL") port: int = Field(5432, description="Порт PostgreSQL")
user: str = Field(..., description="Имя пользователя") user: str = Field(..., description="Имя пользователя")
password_env: str = Field(..., description="Имя переменной окружения с паролем")
database: str = Field(..., description="Имя базы данных") database: str = Field(..., description="Имя базы данных")
ssl: bool = Field(False, description="Использовать SSL") ssl: bool = Field(False, description="Использовать SSL")
ssl_mode: Optional[str] = Field("prefer", description="Режим SSL (require, prefer, disable)") ssl_mode: Optional[str] = Field("prefer", description="Режим SSL (require, prefer, disable)")
@@ -42,7 +43,7 @@ class QdrantConfig(BaseModel):
class GigaChatConfig(BaseModel): class GigaChatConfig(BaseModel):
"""Настройки GigaChat API.""" """Настройки GigaChat API."""
credentials_env: str = Field(..., description="Имя переменной окружения с API-ключом") api_key: Optional[str] = Field(None, description="API-ключ (загружается из окружения)")
model_embedding: str = Field("Embeddings", description="Модель для эмбеддингов") model_embedding: str = Field("Embeddings", description="Модель для эмбеддингов")
model_generation: str = Field("GigaChat-Max", description="Модель для генерации") model_generation: str = Field("GigaChat-Max", description="Модель для генерации")
timeout: int = Field(600, description="Таймаут запросов в секундах") timeout: int = Field(600, description="Таймаут запросов в секундах")
@@ -164,10 +165,15 @@ class PromptsConfig(BaseModel):
consistency_max_fragments: int = Field(5, description="Максимальное количество фрагментов для проверки противоречий") consistency_max_fragments: int = Field(5, description="Максимальное количество фрагментов для проверки противоречий")
critique: PromptTemperatures = Field(default_factory=lambda: PromptTemperatures(temperature=0.1)) critique: PromptTemperatures = Field(default_factory=lambda: PromptTemperatures(temperature=0.1))
rerank: 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)) 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="Имя профиля бота") name: str = Field(..., description="Имя профиля бота")
jid: str = Field(..., description="JID бота (user@domain)") jid: str = Field(..., description="JID бота (user@domain)")
password_env: str = Field(..., description="Имя переменной окружения с паролем XMPP")
resource: str = Field("bot", description="Ресурс XMPP") resource: str = Field("bot", description="Ресурс XMPP")
status_message: str = Field(..., description="Сообщение статуса присутствия") status_message: str = Field(..., description="Сообщение статуса присутствия")
xmpp_server: Optional[str] = Field(None, description="XMPP сервер (если не указан, берётся из JID)") xmpp_server: Optional[str] = Field(None, description="XMPP сервер (если не указан, берётся из JID)")
@@ -188,6 +193,8 @@ class AppConfig(BaseModel):
admin_jids: List[str] = Field([], description="Список JID администраторов") admin_jids: List[str] = Field([], description="Список JID администраторов")
log_level: str = Field("INFO", description="Уровень логирования") log_level: str = Field("INFO", description="Уровень логирования")
rag_server_url: str = Field("http://localhost:8080", description="URL RAG-сервера") 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 database: DatabaseConfig
@@ -205,14 +212,9 @@ class AppConfig(BaseModel):
file_permissions: FilePermissionsConfig file_permissions: FilePermissionsConfig
ai: AIConfig ai: AIConfig
prompts: PromptsConfig 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="Корень профиля") profile_dir: Optional[Path] = Field(None, description="Корень профиля")
data_dir_path: Optional[Path] = Field(None) data_dir_path: Optional[Path] = Field(None)
temp_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="Содержимое промптов") 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: class Config:
extra = "forbid" # Запрещаем неизвестные поля extra = "forbid" # Запрещаем неизвестные поля

View File

@@ -16,12 +16,12 @@
import logging import logging
from typing import Optional, Dict, List, Any from typing import Optional, Dict, List, Any
from rag.rag_orchestrator import RAGOrchestrator from .rag_orchestrator import RAGOrchestrator
from rag.services.kb_service import KBService from .services.kb_service import KBService
from rag.services.giga_client import GigaClient from .services.giga_client import GigaClient
from rag.services.file_service import FileService from .services.file_service import FileService
from rag.services.postgres_service import PostgresService from .services.postgres_service import PostgresService
from rag.config_models import AppConfig from .config_models import AppConfig
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -8,19 +8,19 @@
import logging import logging
from typing import Optional, Dict, List, Any from typing import Optional, Dict, List, Any
from rag.services.postgres_service import PostgresService from .services.postgres_service import PostgresService
from rag.services.qdrant_service import QdrantService from .services.qdrant_service import QdrantService
from rag.services.embedding_service import EmbeddingService from .services.embedding_service import EmbeddingService
from rag.services.kb_service import KBService from .services.kb_service import KBService
from rag.services.giga_client import GigaClient from .services.giga_client import GigaClient
from rag.services.file_service import FileService from .services.file_service import FileService
from rag.history_manager import HistoryManager from .history_manager import HistoryManager
from rag.intent_router import IntentRouter from .intent_router import IntentRouter
from rag.query_processor import QueryProcessor from .query_processor import QueryProcessor
from rag.indexing_manager import IndexingManager from .indexing_manager import IndexingManager
from rag.functions.intent_classify import classify_intent from .functions.intent_classify import classify_intent
from rag.utils.text_utils import count_tokens from .utils.text_utils import count_tokens
from rag.config_models import AppConfig from .config_models import AppConfig
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -183,9 +183,9 @@ class RAGOrchestrator:
system_prompt = prompts.get('system', None) system_prompt = prompts.get('system', None)
# 2. Расчёт лимитов токенов # 2. Расчёт лимитов токенов
max_model_tokens = self.config.rag.max_model_tokens max_model_tokens = self.config.max_model_tokens
reserved_for_answer = self.config.rag.reserved_for_answer_tokens reserved_for_answer = self.config.reserved_for_answer_tokens
reserved_for_overhead = self.config.rag.reserved_for_overhead_tokens reserved_for_overhead = self.config.reserved_for_overhead_tokens
token_info = self._prepare_prompt_parts( token_info = self._prepare_prompt_parts(
synthesis_template=synthesis_template, synthesis_template=synthesis_template,
@@ -211,7 +211,7 @@ class RAGOrchestrator:
# 5. Классификация намерений # 5. Классификация намерений
intent = intent_override 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', '') intent_prompt = prompts.get('intent', '')
if intent_prompt: if intent_prompt:
intent = await classify_intent( intent = await classify_intent(
@@ -226,7 +226,7 @@ class RAGOrchestrator:
intent = intent or "GENERAL" intent = intent or "GENERAL"
# 6. Принудительная установка SURGICAL по ключевым словам # 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: if any(kw in query.lower() for kw in keywords) and last_file_path:
intent = "SURGICAL" intent = "SURGICAL"
logger.info(f"Принудительный Intent: SURGICAL (есть файл и ключевые слова)") logger.info(f"Принудительный Intent: SURGICAL (есть файл и ключевые слова)")
@@ -255,7 +255,6 @@ class RAGOrchestrator:
sources = router_result.get("sources", []) sources = router_result.get("sources", [])
else: else:
# Обычный RAG-пайплайн (GENERAL, FACT, PROCEDURE, COMPARISON, CALCULATION) # Обычный RAG-пайплайн (GENERAL, FACT, PROCEDURE, COMPARISON, CALCULATION)
# Вычисляем, сколько токенов осталось для контекста после истории
history_tokens = sum(count_tokens(msg['content']) for msg in formatted_history) 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 = available_for_history_and_context - history_tokens
available_for_context = max(available_for_context, 0) available_for_context = max(available_for_context, 0)

View File

@@ -3,16 +3,12 @@
""" """
RAG-сервер отдельный HTTP-сервис для обработки RAG-запросов. RAG-сервер отдельный HTTP-сервис для обработки RAG-запросов.
Предоставляет API для выполнения RAG-запросов и индексации документов. Предоставляет API для выполнения RAG-запросов и индексации документов.
Этот сервер запускается как отдельный процесс и может обслуживать
несколько ботов и других систем одновременно.
ИСПОЛЬЗУЕТ: FastAPI для создания REST API и Uvicorn для запуска.
ИСТОРИЯ ХРАНИТСЯ НА СЕРВЕРЕ (в PostgreSQL). ИСТОРИЯ ХРАНИТСЯ НА СЕРВЕРЕ (в PostgreSQL).
ДОБАВЛЕНО: ДОБАВЛЕНО:
- Эндпоинт /rag/vision для распознавания текста на изображениях (OCR) - Эндпоинт /rag/vision для распознавания текста на изображениях (OCR)
- Эндпоинт /rag/transcribe для транскрибации аудио (SaluteSpeech) - Эндпоинт /rag/transcribe для транскрибации аудио (SaluteSpeech)
- Использование Pydantic-модели AppConfig для конфигурации.
""" """
import logging import logging
@@ -32,16 +28,16 @@ from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import uvicorn import uvicorn
# Импортируем наши модули (теперь из пакета rag) # Импорты наших модулей
from rag.utils.config_loader import load_config from .utils.config_loader import load_config
from rag.utils.logger import setup_logging from .utils.logger import setup_logging
from rag.services.postgres_service import PostgresService from .services.postgres_service import PostgresService
from rag.services.qdrant_service import QdrantService from .services.qdrant_service import QdrantService
from rag.services.embedding_service import EmbeddingService from .services.embedding_service import EmbeddingService
from rag.services.kb_service import KBService from .services.kb_service import KBService
from rag.services.giga_client import GigaClient from .services.giga_client import GigaClient
from rag.services.file_service import FileService from .services.file_service import FileService
from rag.rag_orchestrator import RAGOrchestrator from .rag_orchestrator import RAGOrchestrator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -114,7 +110,7 @@ class TranscribeResponse(BaseModel):
# ============================================================ # ============================================================
_orchestrator: Optional[RAGOrchestrator] = None _orchestrator: Optional[RAGOrchestrator] = None
_config: Optional[AppConfig] = None _config = None
def get_orchestrator() -> RAGOrchestrator: def get_orchestrator() -> RAGOrchestrator:
@@ -125,7 +121,7 @@ def get_orchestrator() -> RAGOrchestrator:
return _orchestrator return _orchestrator
def init_orchestrator(config: AppConfig) -> RAGOrchestrator: def init_orchestrator(config):
"""Инициализирует все сервисы и создаёт RAGOrchestrator.""" """Инициализирует все сервисы и создаёт RAGOrchestrator."""
global _orchestrator, _config global _orchestrator, _config
_config = config _config = config
@@ -134,31 +130,31 @@ def init_orchestrator(config: AppConfig) -> RAGOrchestrator:
# ---- 1. Инициализация сервисов ---- # ---- 1. Инициализация сервисов ----
# PostgreSQL (история, документы) # PostgreSQL (история, документы)
db = PostgresService( db = PostgresService(
host=config.database.host, host=config.db_host,
port=config.database.port, port=config.db_port,
user=config.database.user, user=config.db_user,
password=config.db_password, password=config.db_password,
db_name=config.database.database db_name=config.db_name
) )
# Qdrant (векторный поиск) # Qdrant (векторный поиск)
qdrant = QdrantService( qdrant = QdrantService(
host=config.qdrant.host, host=config.qdrant_host,
port=config.qdrant.port, port=config.qdrant_port,
grpc_port=config.qdrant.grpc_port, grpc_port=config.qdrant_grpc_port,
collection_name=config.qdrant.collection, collection_name=config.qdrant_collection,
vector_size=config.qdrant.vector_size, vector_size=config.qdrant_vector_size,
distance=config.qdrant.distance, distance=config.qdrant_distance,
prefer_grpc=False prefer_grpc=False
) )
# Эмбеддинги (GigaChat) # Эмбеддинги (GigaChat)
embedding = EmbeddingService( embedding = EmbeddingService(
api_key=config.gigachat_api_key, api_key=config.gigachat_api_key,
model=config.embedding.model, model=config.embedding_model,
timeout=config.embedding.timeout, timeout=config.embedding_timeout,
cache_size=config.embedding.cache_size, cache_size=config.embedding_cache_size,
verify_ssl=config.embedding.verify_ssl_certs verify_ssl=config.embedding_verify_ssl
) )
# База знаний (индексация, поиск) # База знаний (индексация, поиск)
@@ -166,19 +162,19 @@ def init_orchestrator(config: AppConfig) -> RAGOrchestrator:
db=db, db=db,
qdrant=qdrant, qdrant=qdrant,
embedding=embedding, embedding=embedding,
collection_name=config.qdrant.collection, collection_name=config.qdrant_collection,
chunk_size_tokens=config.chunking.chunk_size_tokens, chunk_size_tokens=config.chunk_size_tokens,
overlap_tokens=config.chunking.overlap_tokens, overlap_tokens=config.overlap_tokens,
approx_chunk_chars=config.chunking.approx_chunk_chars, approx_chunk_chars=config.chunking_approx_chunk_chars,
approx_overlap_chars=config.chunking.approx_overlap_chars approx_overlap_chars=config.chunking_approx_overlap_chars
) )
# GigaChat клиент (генерация) # GigaChat клиент (генерация)
giga = GigaClient( giga = GigaClient(
api_key=config.gigachat_api_key, api_key=config.gigachat_api_key,
model=config.gigachat.model_generation, model=config.ai_model,
temperature=config.gigachat.temperature, temperature=config.ai_temperature,
timeout=config.gigachat.timeout, timeout=config.ai_timeout,
verify_ssl=False verify_ssl=False
) )
@@ -186,11 +182,15 @@ def init_orchestrator(config: AppConfig) -> RAGOrchestrator:
files = FileService(config) files = FileService(config)
# ---- 2. Создание оркестратора ---- # ---- 2. Создание оркестратора ----
# Загружаем дефолтные промпты (из config.prompts_content) # Загружаем дефолтные промпты (опционально)
default_prompts = config.prompts_content or {} default_prompts = {}
system_prompt = default_prompts.get('system') system_prompt_path = getattr(config, 'system_prompt_file', None)
if system_prompt: if system_prompt_path and system_prompt_path.exists():
default_prompts['system'] = system_prompt 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( orchestrator = RAGOrchestrator(
db=db, db=db,
@@ -215,7 +215,7 @@ def init_orchestrator(config: AppConfig) -> RAGOrchestrator:
app = FastAPI( app = FastAPI(
title="RAG-сервер", title="RAG-сервер",
description="Единый сервис для RAG-обработки запросов и индексации документов", description="Единый сервис для RAG-обработки запросов и индексации документов",
version="1.0.0" version="3.0.0"
) )
# Разрешаем CORS для возможности запросов с других доменов (для тестов) # Разрешаем CORS для возможности запросов с других доменов (для тестов)
@@ -436,7 +436,7 @@ def main():
) )
args = parser.parse_args() args = parser.parse_args()
# Загружаем конфигурацию с помощью новой функции load_config # Загружаем конфигурацию через Pydantic
try: try:
config = load_config(args.profile_dir) config = load_config(args.profile_dir)
except Exception as e: except Exception as e:
@@ -445,9 +445,7 @@ def main():
# Настраиваем логирование # Настраиваем логирование
log_level = getattr(logging, args.log_level.upper(), logging.INFO) log_level = getattr(logging, args.log_level.upper(), logging.INFO)
# Передаём путь к логу из конфига setup_logging(config.name, config.log_file_path, log_level=log_level)
log_file_path = config.log_file_path or Path("/tmp/rag_server.log")
setup_logging(config.name, log_file_path, log_level=log_level)
logger.info(f"Запуск RAG-сервера с профилем {args.profile_dir}") logger.info(f"Запуск RAG-сервера с профилем {args.profile_dir}")
logger.info(f"Хост: {args.host}, порт: {args.port}") logger.info(f"Хост: {args.host}, порт: {args.port}")
@@ -461,7 +459,7 @@ def main():
# Запускаем Uvicorn # Запускаем Uvicorn
uvicorn.run( uvicorn.run(
"rag.rag_server:app", # теперь модуль из пакета rag "rag.rag_server:app",
host=args.host, host=args.host,
port=args.port, port=args.port,
log_level=args.log_level, log_level=args.log_level,

View File

@@ -9,14 +9,11 @@ import os
import yaml import yaml
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any from typing import Dict, Any
from dotenv import load_dotenv from dotenv import load_dotenv
from .config_models import AppConfig, DatabaseConfig, QdrantConfig, GigaChatConfig, \ from ..config_models import AppConfig
EmbeddingConfig, ChunkingConfig, RAGConfig, HTTPServerConfig, HTTPConfig, \
SummarizationConfig, PathsConfig, FeaturesConfig, CleanupConfig, FilePermissionsConfig, \
AIConfig, PromptsConfig, PromptTemperatures
logger = logging.getLogger(__name__) 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']: if 'database' in merged and 'password_env' in merged['database']:
env_name = merged['database']['password_env'] env_name = merged['database']['password_env']
merged['database']['password'] = env_vars.get(env_name) merged['database']['password'] = env_vars.get(env_name)
if not merged['database']['password']: if not merged['database']['password']:
raise ValueError(f"Переменная окружения {env_name} не задана или пуста") raise ValueError(f"Переменная окружения {env_name} не задана или пуста")
# удаляем password_env, чтобы не передавать в модель
del merged['database']['password_env'] del merged['database']['password_env']
# Аналогично для gigachat
if 'gigachat' in merged and 'credentials_env' in merged['gigachat']: if 'gigachat' in merged and 'credentials_env' in merged['gigachat']:
env_name = merged['gigachat']['credentials_env'] env_name = merged['gigachat']['credentials_env']
merged['gigachat']['api_key'] = env_vars.get(env_name) 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} не задана или пуста") raise ValueError(f"Переменная окружения {env_name} не задана или пуста")
del merged['gigachat']['credentials_env'] del merged['gigachat']['credentials_env']
# Для XMPP пароля
if 'password_env' in merged: if 'password_env' in merged:
env_name = merged['password_env'] env_name = merged['password_env']
merged['xmpp_password'] = env_vars.get(env_name) merged['xmpp_password'] = env_vars.get(env_name)
@@ -91,30 +84,30 @@ def load_config(profile_dir: str) -> AppConfig:
raise ValueError(f"Переменная окружения {env_name} не задана или пуста") raise ValueError(f"Переменная окружения {env_name} не задана или пуста")
del merged['password_env'] del merged['password_env']
# Для SaluteSpeech (если есть в секции features или отдельно)
# У нас он может быть в .env, но в модели нет отдельного поля; добавим в итоговый объект
merged['salute_speech_auth'] = env_vars.get('SALUTE_SPEECH_AUTH') merged['salute_speech_auth'] = env_vars.get('SALUTE_SPEECH_AUTH')
# Преобразуем пути в абсолютные с учётом profile_dir # Преобразуем пути в абсолютные с учётом profile_dir
paths_section = merged.get('paths', {}) paths_section = merged.get('paths', {})
# Если пути относительные, дополняем их относительно profile_dir
for key in ['data_dir', 'temp_dir', 'prompts_dir', 'upload_dir', 'log_dir']: for key in ['data_dir', 'temp_dir', 'prompts_dir', 'upload_dir', 'log_dir']:
if key in paths_section: if key in paths_section:
p = Path(paths_section[key]) p = Path(paths_section[key])
if not p.is_absolute(): if not p.is_absolute():
paths_section[key] = str(profile_path / p) paths_section[key] = str(profile_path / p)
# log_filename оставляем как есть
# Добавляем profile_dir в merged для передачи в модель # Добавляем profile_dir
merged['profile_dir'] = str(profile_path) 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: try:
config = AppConfig(**merged) config = AppConfig(**merged)
except Exception as e: except Exception as e:
raise ValueError(f"Ошибка валидации конфигурации: {e}") raise ValueError(f"Ошибка валидации конфигурации: {e}")
# Дополнительно загружаем промпты из файлов # Загружаем промпты из файлов
prompts_dir = config.prompts_dir_path prompts_dir = config.prompts_dir_path
if prompts_dir and prompts_dir.exists(): if prompts_dir and prompts_dir.exists():
prompts_content = {} prompts_content = {}