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.
Используются для загрузки и валидации настроек из 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" # Запрещаем неизвестные поля

View File

@@ -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__)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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 = {}