Files
ai-rpg/backend/app/api/sessions.py

192 lines
7.3 KiB
Python
Raw Normal View History

2026-06-19 11:28:04 +03:00
"""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
2026-06-19 19:14:27 +03:00
from app.engine.orchestrator import generate_intro_scene, run_iteration
2026-06-19 11:28:04 +03:00
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)}
2026-06-19 19:14:27 +03:00
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)}
2026-06-19 11:28:04 +03:00
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()