193 lines
7.9 KiB
Python
193 lines
7.9 KiB
Python
"""
|
|
SUNT — бекенд сервер.
|
|
Запуск: uvicorn server:app --host 0.0.0.0 --port 3000 --reload
|
|
"""
|
|
|
|
import logging
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
from dotenv import load_dotenv
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from services.yandex import get_standard_answer, get_meta_monitoring_result
|
|
|
|
load_dotenv()
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
app = FastAPI(title="SUNT Backend", docs_url=None, redoc_url=None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Хранилище сессий (в памяти — очищается при перезапуске сервера)
|
|
#
|
|
# Структура одной сессии:
|
|
# {
|
|
# "history": [
|
|
# {"role": "user", "content": "..."},
|
|
# {"role": "assistant", "content": "..."},
|
|
# ...
|
|
# ],
|
|
# "meta_feedbacks": [
|
|
# "Рекомендация 1 из мета-мониторинга",
|
|
# "Рекомендация 2 из мета-мониторинга",
|
|
# ...
|
|
# ]
|
|
# }
|
|
# ---------------------------------------------------------------------------
|
|
|
|
sessions: dict[str, dict] = {}
|
|
|
|
MAX_HISTORY_MESSAGES = 20 # максимум сообщений передаём модели (чтобы не раздувать контекст)
|
|
MAX_META_FEEDBACKS = 5 # максимум рекомендаций от мета-мониторинга
|
|
|
|
|
|
def get_or_create_session(session_id: str | None) -> tuple[str, dict]:
|
|
"""Возвращает (session_id, данные сессии). Создаёт новую если не найдена."""
|
|
if session_id and session_id in sessions:
|
|
return session_id, sessions[session_id]
|
|
new_id = str(uuid.uuid4())
|
|
sessions[new_id] = {"history": [], "meta_feedbacks": []}
|
|
logger.info("Создана новая сессия: %s", new_id)
|
|
return new_id, sessions[new_id]
|
|
|
|
|
|
def trim_history(history: list) -> list:
|
|
"""Оставляет только последние MAX_HISTORY_MESSAGES сообщений."""
|
|
if len(history) > MAX_HISTORY_MESSAGES:
|
|
return history[-MAX_HISTORY_MESSAGES:]
|
|
return history
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Единственный API-маршрут
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@app.post("/api/message")
|
|
async def handle_message(request: Request) -> JSONResponse:
|
|
try:
|
|
body = await request.json()
|
|
except Exception:
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={"error": "Тело запроса должно быть валидным JSON."},
|
|
)
|
|
|
|
request_type = body.get("type")
|
|
session_id, session = get_or_create_session(body.get("session_id"))
|
|
logger.info("Тип запроса: %s | Сессия: %s | Сообщений в истории: %d",
|
|
request_type, session_id, len(session["history"]))
|
|
|
|
# ------------------------------------------------------------------
|
|
# Стандартный запрос к модели
|
|
# ------------------------------------------------------------------
|
|
if request_type == "standard_request":
|
|
user_request = (body.get("user_request") or "").strip()
|
|
if not user_request:
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={"error": "Поле user_request не может быть пустым."},
|
|
)
|
|
|
|
# Добавляем сообщение пользователя в историю сессии
|
|
session["history"].append({"role": "user", "content": user_request})
|
|
|
|
try:
|
|
answer = await get_standard_answer(
|
|
history=trim_history(session["history"]),
|
|
meta_feedbacks=session["meta_feedbacks"],
|
|
)
|
|
except RuntimeError as e:
|
|
logger.error("Ошибка стандартного запроса: %s", e)
|
|
# Убираем последнее сообщение пользователя — оно не было обработано
|
|
session["history"].pop()
|
|
return JSONResponse(
|
|
status_code=502,
|
|
content={"error": str(e)},
|
|
)
|
|
|
|
# Добавляем ответ ассистента в историю
|
|
session["history"].append({"role": "assistant", "content": answer})
|
|
|
|
return JSONResponse(content={
|
|
"type": "standard_answer",
|
|
"answer": answer,
|
|
"session_id": session_id,
|
|
"status": "completed",
|
|
})
|
|
|
|
# ------------------------------------------------------------------
|
|
# Запрос мета-мониторинга
|
|
# ------------------------------------------------------------------
|
|
if request_type == "meta_monitoring_request":
|
|
user_request = (body.get("user_request") or "").strip()
|
|
model_answer = (body.get("model_answer") or "").strip()
|
|
|
|
if not user_request or not model_answer:
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={"error": "Поля user_request и model_answer обязательны для мета-мониторинга."},
|
|
)
|
|
|
|
try:
|
|
result = await get_meta_monitoring_result(user_request, model_answer)
|
|
except RuntimeError as e:
|
|
logger.error("Ошибка мета-мониторинга: %s", e)
|
|
return JSONResponse(
|
|
status_code=502,
|
|
content={"error": str(e)},
|
|
)
|
|
|
|
# Сохраняем рекомендацию из мета-мониторинга в сессию
|
|
recommendation = result.get("recommendations", "").strip()
|
|
if recommendation:
|
|
session["meta_feedbacks"].append(recommendation)
|
|
# Оставляем только последние MAX_META_FEEDBACKS рекомендаций
|
|
if len(session["meta_feedbacks"]) > MAX_META_FEEDBACKS:
|
|
session["meta_feedbacks"] = session["meta_feedbacks"][-MAX_META_FEEDBACKS:]
|
|
logger.info("Сохранена рекомендация мета-мониторинга в сессию %s", session_id)
|
|
|
|
return JSONResponse(content={
|
|
"type": "meta_monitoring_result",
|
|
"factual_correctness": result.get("factual_correctness", 0),
|
|
"completeness": result.get("completeness", 0),
|
|
"logical_consistency": result.get("logical_consistency", 0),
|
|
"overall_score": result.get("overall_score", 0),
|
|
"comment": result.get("comment", ""),
|
|
"issues": result.get("issues", []),
|
|
"recommendations": result.get("recommendations", ""),
|
|
"session_id": session_id,
|
|
"status": "completed",
|
|
})
|
|
|
|
# ------------------------------------------------------------------
|
|
# Неизвестный тип запроса
|
|
# ------------------------------------------------------------------
|
|
logger.warning("Неизвестный тип запроса: %s", request_type)
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={"error": f"Неизвестный тип запроса: {request_type!r}."},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Раздача статики фронтенда
|
|
# ---------------------------------------------------------------------------
|
|
|
|
FRONTEND_DIR = Path(__file__).parent / "frontend"
|
|
|
|
if FRONTEND_DIR.exists():
|
|
app.mount("/", StaticFiles(directory=str(FRONTEND_DIR), html=True), name="frontend")
|
|
logger.info("Фронтенд раздаётся из папки: %s", FRONTEND_DIR)
|
|
else:
|
|
logger.warning(
|
|
"Папка frontend/ не найдена. Создай её и положи туда index.html, styles.css, app.js."
|
|
)
|