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:
@@ -6,6 +6,9 @@
|
|||||||
POST /ai/branch-detail — Stage 3: AI generates detail for one branch
|
POST /ai/branch-detail — Stage 3: AI generates detail for one branch
|
||||||
POST /ai/assemble — Stage 4: assemble branches into tree (no AI)
|
POST /ai/assemble — Stage 4: assemble branches into tree (no AI)
|
||||||
GET /ai/quota — quota status
|
GET /ai/quota — quota status
|
||||||
|
|
||||||
|
Session conversion:
|
||||||
|
POST /ai/session-to-flow — Convert a completed session into a procedural flow
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
@@ -40,6 +43,8 @@ from app.schemas.ai_builder import (
|
|||||||
AIAssembleResponse,
|
AIAssembleResponse,
|
||||||
AIQuotaStatusResponse,
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -437,3 +442,97 @@ async def assemble(
|
|||||||
summary=stats,
|
summary=stats,
|
||||||
status="completed",
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user