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