Files
resolutionflow/backend/app/api/endpoints/session_handoffs.py
chihlasm 9d2a8332aa feat: add handoff API endpoints with queue and integration tests
Four endpoints: create handoff (park/escalate), list handoff history,
claim session, and team queue. Two routers: session-scoped router with
prefix /ai-sessions/{session_id} and queue_router with prefix /ai-sessions.
queue_router registered before ai_sessions.router to avoid /{session_id}
path conflict on GET /ai-sessions/queue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 08:43:32 +00:00

117 lines
3.8 KiB
Python

"""Handoff endpoints — unified park/escalate.
POST /ai-sessions/{id}/handoff — Create handoff
GET /ai-sessions/{id}/handoffs — Handoff history
POST /ai-sessions/{id}/handoffs/{hid}/claim — Claim session
GET /ai-sessions/queue — Team queue
"""
import logging
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_active_user, get_db
from app.models.user import User
from app.models.ai_session import AISession
from app.models.session_handoff import SessionHandoff
from app.services.handoff_manager import HandoffManager
from app.schemas.session_handoff import (
HandoffCreateRequest,
HandoffResponse,
)
logger = logging.getLogger(__name__)
# Queue endpoint needs its own router (no session_id prefix)
queue_router = APIRouter(prefix="/ai-sessions", tags=["session-handoffs"])
# Session-scoped endpoints
router = APIRouter(prefix="/ai-sessions/{session_id}", tags=["session-handoffs"])
@router.post("/handoff", response_model=HandoffResponse, status_code=status.HTTP_201_CREATED)
async def create_handoff(
session_id: UUID,
body: HandoffCreateRequest,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> HandoffResponse:
"""Create a handoff (park or escalate)."""
result = await db.execute(
select(AISession).where(
AISession.id == session_id,
AISession.user_id == current_user.id,
)
)
session = result.scalar_one_or_none()
if not session:
raise HTTPException(status_code=404, detail="Session not found")
manager = HandoffManager(db)
try:
handoff = await manager.create_handoff(
session_id=session_id,
intent=body.intent,
engineer_notes=body.engineer_notes,
user_id=current_user.id,
priority=body.priority,
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
await db.commit()
return HandoffResponse.model_validate(handoff)
@router.get("/handoffs", response_model=list[HandoffResponse])
async def list_handoffs(
session_id: UUID,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> list[HandoffResponse]:
"""Get handoff history for a session."""
result = await db.execute(
select(SessionHandoff)
.where(SessionHandoff.session_id == session_id)
.order_by(SessionHandoff.created_at.desc())
)
handoffs = result.scalars().all()
return [HandoffResponse.model_validate(h) for h in handoffs]
@router.post("/handoffs/{handoff_id}/claim", response_model=HandoffResponse)
async def claim_handoff(
session_id: UUID,
handoff_id: UUID,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> HandoffResponse:
"""Claim a handed-off session."""
manager = HandoffManager(db)
try:
handoff = await manager.claim_session(
handoff_id=handoff_id,
claiming_user_id=current_user.id,
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
await db.commit()
return HandoffResponse.model_validate(handoff)
@queue_router.get("/queue")
async def get_queue(
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
) -> list[dict]:
"""Get team queue of parked + escalated sessions."""
manager = HandoffManager(db)
return await manager.get_queue(
team_id=current_user.team_id,
account_id=current_user.account_id,
)