pp003/server.py
2026-05-27 15:55:36 +03:00

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."
)