diff --git a/backend/app/api/endpoints/ai_builder.py b/backend/app/api/endpoints/ai_builder.py index f3740d07..48c3d010 100644 --- a/backend/app/api/endpoints/ai_builder.py +++ b/backend/app/api/endpoints/ai_builder.py @@ -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)