"""Sessions routes: list / create / get / messages / start iteration (SSE).""" from __future__ import annotations import json from datetime import datetime, timezone from typing import List from uuid import UUID from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from fastapi import APIRouter, Depends, HTTPException, Query from sse_starlette.sse import EventSourceResponse from app.db import get_db_dep from app.deps import get_current_user from app.engine.orchestrator import generate_intro_scene, run_iteration from app.models import Message, Session, User, World from app.schemas import IterationRequest, MessageOut, SessionCreate, SessionOut router = APIRouter(prefix="/api/sessions", tags=["sessions"]) @router.get("", response_model=List[SessionOut]) async def list_sessions( db: AsyncSession = Depends(get_db_dep), user: User = Depends(get_current_user), ): result = await db.execute( select(Session) .join(World, Session.world_id == World.id) .where(World.owner_id == user.id) .order_by(Session.last_played_at.desc().nullslast()) ) return result.scalars().all() @router.post("", response_model=SessionOut, status_code=201) async def create_session( payload: SessionCreate, db: AsyncSession = Depends(get_db_dep), user: User = Depends(get_current_user), ): # Verify world ownership result = await db.execute(select(World).where(World.id == payload.world_id)) world = result.scalars().first() if not world: raise HTTPException(status_code=404, detail="world_not_found") if world.owner_id != user.id and not user.is_admin: raise HTTPException(status_code=403, detail="forbidden") if world.status not in ("ready", "active"): raise HTTPException(status_code=400, detail=f"world_not_ready: status={world.status}") session = Session( world_id=world.id, title=payload.title or f"Сессия в мире «{world.name}»", ) db.add(session) # Mark world as active world.status = "active" await db.commit() await db.refresh(session) return session @router.get("/{session_id}", response_model=SessionOut) async def get_session( session_id: UUID, db: AsyncSession = Depends(get_db_dep), user: User = Depends(get_current_user), ): result = await db.execute( select(Session).join(World, Session.world_id == World.id).where(Session.id == session_id) ) session = result.scalars().first() if not session: raise HTTPException(status_code=404, detail="session_not_found") # Verify ownership via world w_result = await db.execute(select(World).where(World.id == session.world_id)) world = w_result.scalars().first() if not world or (world.owner_id != user.id and not user.is_admin): raise HTTPException(status_code=403, detail="forbidden") return session @router.get("/{session_id}/messages", response_model=List[MessageOut]) async def list_messages( session_id: UUID, include_hidden: bool = Query(False), db: AsyncSession = Depends(get_db_dep), user: User = Depends(get_current_user), ): # Verify access result = await db.execute( select(Session).join(World, Session.world_id == World.id).where(Session.id == session_id) ) session = result.scalars().first() if not session: raise HTTPException(status_code=404, detail="session_not_found") w_result = await db.execute(select(World).where(World.id == session.world_id)) world = w_result.scalars().first() if not world or (world.owner_id != user.id and not user.is_admin): raise HTTPException(status_code=403, detail="forbidden") q = select(Message).where(Message.session_id == session_id).order_by(Message.seq) if not include_hidden: q = q.where(Message.hidden.is_(False)) result = await db.execute(q) return result.scalars().all() @router.post("/{session_id}/iterate") async def iterate_session( session_id: UUID, payload: IterationRequest, db: AsyncSession = Depends(get_db_dep), user: User = Depends(get_current_user), ): """SSE stream of the iteration.""" # Verify access result = await db.execute( select(Session).join(World, Session.world_id == World.id).where(Session.id == session_id) ) session = result.scalars().first() if not session: raise HTTPException(status_code=404, detail="session_not_found") w_result = await db.execute(select(World).where(World.id == session.world_id)) world = w_result.scalars().first() if not world or (world.owner_id != user.id and not user.is_admin): raise HTTPException(status_code=403, detail="forbidden") if payload.session_id != session_id: raise HTTPException(status_code=400, detail="session_id_mismatch") async def event_generator(): try: async for event in run_iteration(db=db, user_id=user.id, session_id=session_id, action_text=payload.action_text): yield {"event": event["type"], "data": json.dumps(event.get("data", {}), ensure_ascii=False, default=str)} except Exception as e: yield {"event": "error", "data": json.dumps({"message": str(e)}, ensure_ascii=False)} yield {"event": "done", "data": "{}"} return EventSourceResponse(event_generator()) @router.post("/{session_id}/intro") async def intro_session( session_id: UUID, db: AsyncSession = Depends(get_db_dep), user: User = Depends(get_current_user), ): """SSE stream that generates the opening cinematic scene for a new session.""" result = await db.execute( select(Session).join(World, Session.world_id == World.id).where(Session.id == session_id) ) session = result.scalars().first() if not session: raise HTTPException(status_code=404, detail="session_not_found") w_result = await db.execute(select(World).where(World.id == session.world_id)) world = w_result.scalars().first() if not world or (world.owner_id != user.id and not user.is_admin): raise HTTPException(status_code=403, detail="forbidden") async def event_generator(): try: async for event in generate_intro_scene(db=db, user_id=user.id, session_id=session_id): yield {"event": event["type"], "data": json.dumps(event.get("data", {}), ensure_ascii=False, default=str)} except Exception as e: yield {"event": "error", "data": json.dumps({"message": str(e)}, ensure_ascii=False)} yield {"event": "done", "data": "{}"} return EventSourceResponse(event_generator()) @router.delete("/{session_id}", status_code=204) async def delete_session( session_id: UUID, db: AsyncSession = Depends(get_db_dep), user: User = Depends(get_current_user), ): result = await db.execute( select(Session).join(World, Session.world_id == World.id).where(Session.id == session_id) ) session = result.scalars().first() if not session: raise HTTPException(status_code=404, detail="session_not_found") w_result = await db.execute(select(World).where(World.id == session.world_id)) world = w_result.scalars().first() if not world or (world.owner_id != user.id and not user.is_admin): raise HTTPException(status_code=403, detail="forbidden") await db.delete(session) await db.commit()