124 lines
3.8 KiB
Python
124 lines
3.8 KiB
Python
"""Cross-user roundtable room admission.
|
|
|
|
Routes (prefix ``/api/roundtable-rooms``)::
|
|
|
|
GET /summary occupied room count across every account
|
|
POST /acquire atomically reserve a room, or 409 when full
|
|
POST /heartbeat renew the same durable lease
|
|
POST /release release the foreground lease
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.gateway.roundtable_room_gate import (
|
|
admit_roundtable_room,
|
|
backend_occupancy_keys,
|
|
release_roundtable_room,
|
|
renew_roundtable_room,
|
|
)
|
|
from deerflow.config.system_settings import get_roundtable_room_limit_settings
|
|
|
|
router = APIRouter(prefix="/api/roundtable-rooms", tags=["roundtable-rooms"])
|
|
|
|
|
|
class RoomScopeRequest(BaseModel):
|
|
draftId: str | None = Field(default=None, max_length=64)
|
|
taskId: str | None = Field(default=None, max_length=128)
|
|
roomSessionId: str | None = Field(default=None, max_length=128)
|
|
|
|
|
|
class RoomAdmissionResponse(BaseModel):
|
|
ok: bool = True
|
|
enabled: bool = True
|
|
max_rooms: int = 3
|
|
active_count: int = 0
|
|
renewed: bool = False
|
|
code: str = ""
|
|
message: str = ""
|
|
|
|
|
|
class RoomSummaryResponse(BaseModel):
|
|
enabled: bool = True
|
|
max_rooms: int = 3
|
|
active_count: int = 0
|
|
|
|
|
|
def _response(decision: dict) -> RoomAdmissionResponse:
|
|
return RoomAdmissionResponse(
|
|
ok=bool(decision.get("ok")),
|
|
enabled=bool(decision.get("enabled")),
|
|
max_rooms=int(decision.get("max_rooms") or 0),
|
|
active_count=int(decision.get("active_count") or 0),
|
|
renewed=bool(decision.get("renewed")),
|
|
code=str(decision.get("code") or ""),
|
|
message=str(decision.get("message") or ""),
|
|
)
|
|
|
|
|
|
def _require_store(request: Request):
|
|
store = getattr(request.app.state, "roundtable_room_store", None)
|
|
if store is None:
|
|
raise HTTPException(status_code=503, detail="Roundtable room store not available")
|
|
return store
|
|
|
|
|
|
@router.get("/summary", response_model=RoomSummaryResponse)
|
|
async def room_summary(request: Request) -> RoomSummaryResponse:
|
|
settings = get_roundtable_room_limit_settings()
|
|
store = getattr(request.app.state, "roundtable_room_store", None)
|
|
extra = await backend_occupancy_keys(request)
|
|
active = (
|
|
0
|
|
if store is None
|
|
else await store.summary(
|
|
extra_keys=extra,
|
|
max_hold_minutes=getattr(settings, "max_hold_minutes", 30),
|
|
)
|
|
)
|
|
return RoomSummaryResponse(
|
|
enabled=settings.enabled,
|
|
max_rooms=settings.max_rooms,
|
|
active_count=active,
|
|
)
|
|
|
|
|
|
@router.post("/acquire", response_model=RoomAdmissionResponse)
|
|
async def acquire_room(request: Request, body: RoomScopeRequest) -> RoomAdmissionResponse:
|
|
decision = await admit_roundtable_room(
|
|
request,
|
|
draft_id=body.draftId,
|
|
task_id=body.taskId,
|
|
room_session_id=body.roomSessionId,
|
|
)
|
|
if not decision.get("ok", False):
|
|
raise HTTPException(status_code=409, detail=decision)
|
|
return _response(decision)
|
|
|
|
|
|
@router.post("/heartbeat", response_model=RoomAdmissionResponse)
|
|
async def heartbeat_room(request: Request, body: RoomScopeRequest) -> RoomAdmissionResponse:
|
|
decision = await renew_roundtable_room(
|
|
request,
|
|
draft_id=body.draftId,
|
|
task_id=body.taskId,
|
|
room_session_id=body.roomSessionId,
|
|
)
|
|
if not decision.get("ok", False):
|
|
raise HTTPException(status_code=409, detail=decision)
|
|
return _response(decision)
|
|
|
|
|
|
@router.post("/release")
|
|
async def release_room(request: Request, body: RoomScopeRequest) -> dict[str, bool]:
|
|
_require_store(request)
|
|
released = await release_roundtable_room(
|
|
request,
|
|
draft_id=body.draftId,
|
|
task_id=body.taskId,
|
|
room_session_id=body.roomSessionId,
|
|
)
|
|
return {"released": released}
|