Add new file
This commit is contained in:
379
core/rag_server.py
Normal file
379
core/rag_server.py
Normal file
@@ -0,0 +1,379 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
RAG-сервер – отдельный HTTP-сервис для обработки RAG-запросов.
|
||||
Предоставляет API для выполнения RAG-запросов и индексации документов.
|
||||
|
||||
Этот сервер запускается как отдельный процесс и может обслуживать
|
||||
несколько ботов и других систем одновременно.
|
||||
|
||||
ИСПОЛЬЗУЕТ: FastAPI для создания REST API и Uvicorn для запуска.
|
||||
ИСТОРИЯ ХРАНИТСЯ НА СЕРВЕРЕ (в PostgreSQL).
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List, Any
|
||||
|
||||
# Добавляем путь к core, если запускаем из корня проекта
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel, Field
|
||||
import uvicorn
|
||||
|
||||
# Импортируем наши модули
|
||||
from core.utils.config_loader import BotConfig
|
||||
from core.utils.logger import setup_logging
|
||||
from core.services.postgres_service import PostgresService
|
||||
from core.services.qdrant_service import QdrantService
|
||||
from core.services.embedding_service import EmbeddingService
|
||||
from core.services.kb_service import KBService
|
||||
from core.services.giga_client import GigaClient
|
||||
from core.services.file_service import FileService
|
||||
from core.rag_orchestrator import RAGOrchestrator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# МОДЕЛИ ДАННЫХ (Pydantic) ДЛЯ API
|
||||
# ============================================================
|
||||
|
||||
class QueryRequest(BaseModel):
|
||||
"""
|
||||
Модель запроса к RAG-серверу на выполнение RAG-запроса.
|
||||
ИСТОРИЯ НЕ ПЕРЕДАЁТСЯ – сервер получает её из БД.
|
||||
"""
|
||||
query: str = Field(..., description="Текст запроса пользователя")
|
||||
user_jid: str = Field(..., description="JID пользователя (без ресурса)")
|
||||
room_jid: Optional[str] = Field(None, description="JID комнаты (None для личного чата)")
|
||||
|
||||
# history: List[Dict[str, str]] = Field(...) # УБРАНО!
|
||||
|
||||
prompts: Dict[str, str] = Field(
|
||||
default_factory=dict,
|
||||
description="Словарь промптов (system, synthesis, intent, expand, critique и т.д.)"
|
||||
)
|
||||
|
||||
intent_override: Optional[str] = Field(None, description="Принудительное переопределение намерения")
|
||||
last_file_path: Optional[str] = Field(None, description="Путь к последнему загруженному файлу")
|
||||
last_file_text: Optional[str] = Field(None, description="Текст последнего загруженного файла")
|
||||
|
||||
|
||||
class QueryResponse(BaseModel):
|
||||
"""Модель ответа RAG-сервера на RAG-запрос."""
|
||||
answer: str = Field(..., description="Итоговый ответ")
|
||||
intent: str = Field(default="GENERAL", description="Распознанное намерение")
|
||||
context: Optional[str] = Field(None, description="Использованный контекст (для отладки)")
|
||||
sources: List[str] = Field(default_factory=list, description="Список источников")
|
||||
confidence: Optional[float] = Field(None, description="Оценка уверенности (0-1)")
|
||||
error: Optional[str] = Field(None, description="Сообщение об ошибке, если есть")
|
||||
|
||||
|
||||
class IndexRequest(BaseModel):
|
||||
"""Модель запроса на индексацию документа."""
|
||||
file_name: str = Field(..., description="Исходное имя файла")
|
||||
file_text: str = Field(..., description="Извлечённый текст документа")
|
||||
user_jid: str = Field(..., description="JID владельца документа")
|
||||
room_jid: Optional[str] = Field(None, description="JID комнаты (если документ комнатный)")
|
||||
is_global: bool = Field(False, description="Глобальный ли документ")
|
||||
title: Optional[str] = Field(None, description="Отображаемое название")
|
||||
metadata: Optional[Dict[str, Any]] = Field(None, description="Дополнительные метаданные")
|
||||
file_hash: Optional[str] = Field(None, description="SHA-256 хеш содержимого")
|
||||
update_if_exists: bool = Field(True, description="Заменять ли существующий документ")
|
||||
|
||||
|
||||
class IndexResponse(BaseModel):
|
||||
"""Модель ответа на индексацию."""
|
||||
doc_id: Optional[int] = Field(None, description="Идентификатор документа в БД")
|
||||
chunk_count: int = Field(0, description="Количество проиндексированных чанков")
|
||||
error: Optional[str] = Field(None, description="Сообщение об ошибке, если есть")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ГЛОБАЛЬНЫЙ ОРКЕСТРАТОР (инициализируется один раз при старте)
|
||||
# ============================================================
|
||||
|
||||
_orchestrator: Optional[RAGOrchestrator] = None
|
||||
_config: Optional[BotConfig] = None
|
||||
|
||||
|
||||
def get_orchestrator() -> RAGOrchestrator:
|
||||
"""Возвращает экземпляр RAGOrchestrator (инициализирует при первом вызове)."""
|
||||
global _orchestrator
|
||||
if _orchestrator is None:
|
||||
raise RuntimeError("RAGOrchestrator не инициализирован. Сервер не запущен корректно.")
|
||||
return _orchestrator
|
||||
|
||||
|
||||
def init_orchestrator(config: BotConfig) -> RAGOrchestrator:
|
||||
"""Инициализирует все сервисы и создаёт RAGOrchestrator."""
|
||||
global _orchestrator, _config
|
||||
_config = config
|
||||
logger.info("Инициализация RAG-сервера...")
|
||||
|
||||
# ---- 1. Инициализация сервисов ----
|
||||
# PostgreSQL (история, документы)
|
||||
db = PostgresService(
|
||||
host=config.db_host,
|
||||
port=config.db_port,
|
||||
user=config.db_user,
|
||||
password=config.db_password,
|
||||
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,
|
||||
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
|
||||
)
|
||||
|
||||
# База знаний (индексация, поиск)
|
||||
kb = KBService(
|
||||
db=db,
|
||||
qdrant=qdrant,
|
||||
embedding=embedding,
|
||||
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.ai_model,
|
||||
temperature=config.ai_temperature,
|
||||
timeout=config.ai_timeout,
|
||||
verify_ssl=False
|
||||
)
|
||||
|
||||
# Файловый сервис (извлечение текста)
|
||||
files = FileService(config)
|
||||
|
||||
# ---- 2. Создание оркестратора ----
|
||||
# Загружаем дефолтные промпты (опционально)
|
||||
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,
|
||||
qdrant=qdrant,
|
||||
embedding=embedding,
|
||||
kb=kb,
|
||||
giga=giga,
|
||||
files=files,
|
||||
config=config,
|
||||
default_prompts=default_prompts
|
||||
)
|
||||
|
||||
_orchestrator = orchestrator
|
||||
logger.info("RAG-сервер успешно инициализирован")
|
||||
return orchestrator
|
||||
|
||||
|
||||
# ============================================================
|
||||
# FASTAPI ПРИЛОЖЕНИЕ
|
||||
# ============================================================
|
||||
|
||||
app = FastAPI(
|
||||
title="RAG-сервер",
|
||||
description="Единый сервис для RAG-обработки запросов и индексации документов",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# Разрешаем CORS для возможности запросов с других доменов (для тестов)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Подключаемся к БД при старте сервера."""
|
||||
if _orchestrator is None:
|
||||
logger.error("Оркестратор не инициализирован.")
|
||||
else:
|
||||
try:
|
||||
await _orchestrator.db.connect()
|
||||
logger.info("Подключение к PostgreSQL установлено")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка подключения к PostgreSQL: {e}")
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
"""Закрываем соединение с БД при завершении сервера."""
|
||||
if _orchestrator:
|
||||
try:
|
||||
await _orchestrator.db.close()
|
||||
logger.info("Подключение к PostgreSQL закрыто")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка закрытия PostgreSQL: {e}")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ЭНДПОИНТЫ API
|
||||
# ============================================================
|
||||
|
||||
@app.post("/rag/query", response_model=QueryResponse)
|
||||
async def rag_query(request: QueryRequest):
|
||||
"""
|
||||
Выполняет RAG-запрос.
|
||||
ИСТОРИЯ БЕРЁТСЯ СЕРВЕРОМ ИЗ БД (не передаётся в запросе).
|
||||
"""
|
||||
orchestrator = get_orchestrator()
|
||||
|
||||
try:
|
||||
result = await orchestrator.process_query(
|
||||
query=request.query,
|
||||
user_jid=request.user_jid,
|
||||
room_jid=request.room_jid,
|
||||
prompts=request.prompts,
|
||||
intent_override=request.intent_override,
|
||||
last_file_path=request.last_file_path,
|
||||
last_file_text=request.last_file_text
|
||||
)
|
||||
return QueryResponse(
|
||||
answer=result.get('answer', ''),
|
||||
intent=result.get('intent', 'GENERAL'),
|
||||
context=result.get('context'),
|
||||
sources=result.get('sources', []),
|
||||
confidence=result.get('confidence'),
|
||||
error=result.get('error')
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в /rag/query: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/rag/index", response_model=IndexResponse)
|
||||
async def index_document(request: IndexRequest):
|
||||
"""Индексирует документ в базу знаний."""
|
||||
orchestrator = get_orchestrator()
|
||||
|
||||
try:
|
||||
result = await orchestrator.index_document(
|
||||
file_name=request.file_name,
|
||||
file_text=request.file_text,
|
||||
user_jid=request.user_jid,
|
||||
room_jid=request.room_jid,
|
||||
is_global=request.is_global,
|
||||
title=request.title,
|
||||
metadata=request.metadata,
|
||||
file_hash=request.file_hash,
|
||||
update_if_exists=request.update_if_exists
|
||||
)
|
||||
return IndexResponse(
|
||||
doc_id=result.get('doc_id'),
|
||||
chunk_count=result.get('chunk_count', 0),
|
||||
error=result.get('error')
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в /rag/index: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Проверка здоровья сервиса."""
|
||||
if _orchestrator is None:
|
||||
return {"status": "unhealthy", "message": "Orchestrator not initialized"}
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ТОЧКА ВХОДА
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
"""Главная функция запуска RAG-сервера."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="RAG-сервер")
|
||||
parser.add_argument(
|
||||
"--profile-dir",
|
||||
required=True,
|
||||
help="Путь к директории профиля бота (например, /usr/local/etc/bots/metabot)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default="0.0.0.0",
|
||||
help="Хост для привязки сервера"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8080,
|
||||
help="Порт для привязки сервера"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-level",
|
||||
default="info",
|
||||
choices=["debug", "info", "warning", "error", "critical"],
|
||||
help="Уровень логирования"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Загружаем конфигурацию
|
||||
try:
|
||||
config = BotConfig(args.profile_dir)
|
||||
except Exception as e:
|
||||
print(f"Ошибка загрузки конфигурации: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Настраиваем логирование
|
||||
log_level = getattr(logging, args.log_level.upper(), logging.INFO)
|
||||
setup_logging(config.name, config.log_file, log_level=log_level)
|
||||
|
||||
logger.info(f"Запуск RAG-сервера с профилем {args.profile_dir}")
|
||||
logger.info(f"Хост: {args.host}, порт: {args.port}")
|
||||
|
||||
# Инициализируем оркестратор
|
||||
try:
|
||||
init_orchestrator(config)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка инициализации оркестратора: {e}", exc_info=True)
|
||||
sys.exit(1)
|
||||
|
||||
# Запускаем Uvicorn
|
||||
uvicorn.run(
|
||||
"core.rag_server:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
log_level=args.log_level,
|
||||
reload=False
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user