feat: add POST /ai/session-to-flow endpoint (Task 21)

Converts a completed session into a reusable procedural flow using AI.
Includes quota checking, usage recording, and proper error handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-16 01:17:46 -04:00
parent 831ef07ceb
commit df1a828e0c

View File

@@ -6,6 +6,9 @@
POST /ai/branch-detail — Stage 3: AI generates detail for one branch
POST /ai/assemble — Stage 4: assemble branches into tree (no AI)
GET /ai/quota — quota status
Session conversion:
POST /ai/session-to-flow — Convert a completed session into a procedural flow
"""
import logging
from typing import Annotated
@@ -40,6 +43,8 @@ from app.schemas.ai_builder import (
AIAssembleResponse,
AIQuotaStatusResponse,
)
from app.schemas.session_to_flow import SessionToFlowRequest, SessionToFlowResponse
from app.services.session_to_flow_service import generate_flow_from_session
logger = logging.getLogger(__name__)
@@ -437,3 +442,97 @@ async def assemble(
summary=stats,
status="completed",
)
@router.post("/session-to-flow", response_model=SessionToFlowResponse)
@limiter.limit("5/minute")
async def session_to_flow(
request: Request,
data: SessionToFlowRequest,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
_: None = Depends(require_engineer_or_admin),
):
"""Convert a completed troubleshooting session into a reusable procedural flow."""
_require_ai_enabled()
# Check AI quota
allowed, quota_status = await check_ai_quota(
user_id=current_user.id,
account_id=current_user.account_id,
db=db,
billing_anchor=current_user.ai_billing_cycle_anchor_at,
is_super_admin=current_user.is_super_admin,
)
if not allowed:
reset_key = (
"daily_reset_at"
if quota_status.get("deny_reason") == "daily"
else "monthly_reset_at"
)
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail={
"message": f"AI build limit exceeded ({quota_status['deny_reason']})",
"reset_at": quota_status.get(reset_key),
"quota": quota_status,
},
)
plan = await get_user_plan(current_user.account_id, db)
try:
result = await generate_flow_from_session(
session_id=data.session_id,
user_id=current_user.id,
account_id=current_user.account_id,
db=db,
)
except ValueError as e:
logger.warning("session_to_flow validation error: %s", e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
except Exception as e:
logger.exception("session_to_flow failed: %s", e)
await record_ai_usage(
user_id=current_user.id,
account_id=current_user.account_id,
conversation_id=None,
generation_type="session_to_flow",
tier=plan,
input_tokens=0,
output_tokens=0,
estimated_cost=0,
succeeded=False,
counts_toward_quota=False,
error_code=type(e).__name__,
extra_data={"session_id": data.session_id, "error": str(e)},
db=db,
)
await db.commit()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to generate flow: {type(e).__name__}. Please try again.",
)
# Record successful quota-consuming usage
await record_ai_usage(
user_id=current_user.id,
account_id=current_user.account_id,
conversation_id=None,
generation_type="session_to_flow",
tier=plan,
input_tokens=0,
output_tokens=0,
estimated_cost=0,
succeeded=True,
counts_toward_quota=True,
error_code=None,
extra_data={"session_id": data.session_id},
db=db,
)
await db.commit()
return SessionToFlowResponse(**result)