Spaces:
Runtime error
Runtime error
| """ | |
| FastAPI endpoint for chapter content personalization. | |
| POST /api/personalize - Personalizes chapter content based on user profile. | |
| """ | |
| import logging | |
| from datetime import datetime | |
| from typing import Union | |
| from fastapi import APIRouter, Request, Depends, HTTPException | |
| from fastapi.responses import JSONResponse | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| from api.personalize_models import ( | |
| PersonalizeRequest, | |
| PersonalizeSuccessResponse, | |
| PersonalizeErrorResponse, | |
| PersonalizationSummary, | |
| HardwareContext, | |
| ErrorCodes, | |
| UserProfile | |
| ) | |
| from api.auth_helper import validate_session | |
| from api.rate_limiter import get_personalize_rate_limiter | |
| from db.postgres_client import get_db_session | |
| from db.profile_queries import get_user_profile | |
| from agents.personalize_agent import get_personalize_agent | |
| logger = logging.getLogger(__name__) | |
| # Create router | |
| router = APIRouter(prefix="/api", tags=["personalization"]) | |
| async def personalize_content( | |
| request: Request, | |
| body: PersonalizeRequest, | |
| session: AsyncSession = Depends(get_db_session) | |
| ): | |
| """ | |
| Personalize chapter content based on user profile. | |
| Requires authentication via Better-Auth session cookie. | |
| User must have a complete profile with all required fields. | |
| Rate limited to 5 requests per minute per user. | |
| """ | |
| try: | |
| # ========================================================== | |
| # LOCAL TESTING MODE - Set to False before production deploy | |
| # ========================================================== | |
| # Step 1: Get user ID | |
| # Priority: 1) Request body user_id, 2) Session cookie validation | |
| user_id = body.user_id | |
| if user_id: | |
| logger.info(f"Using user_id from request body: {user_id}") | |
| else: | |
| # Fallback to session cookie validation | |
| user_id = await validate_session(request) | |
| if not user_id: | |
| logger.warning("Personalization request without valid session or user_id") | |
| return JSONResponse( | |
| status_code=401, | |
| content={ | |
| "success": False, | |
| "error": ErrorCodes.AUTH_REQUIRED, | |
| "message": "Authentication required. Please log in to personalize content." | |
| } | |
| ) | |
| # Step 2: Get user profile from database | |
| profile = await get_user_profile(user_id, session) | |
| if not profile: | |
| logger.warning(f"No profile found for user: {user_id}") | |
| return JSONResponse( | |
| status_code=404, | |
| content={ | |
| "success": False, | |
| "error": ErrorCodes.PROFILE_NOT_FOUND, | |
| "message": "User profile not found. Please complete your profile setup." | |
| } | |
| ) | |
| # Step 3: Check if profile is complete | |
| is_complete, missing_fields = profile.is_complete() | |
| if not is_complete: | |
| logger.warning(f"Incomplete profile for user {user_id}: missing {missing_fields}") | |
| return JSONResponse( | |
| status_code=400, | |
| content={ | |
| "success": False, | |
| "error": ErrorCodes.PROFILE_INCOMPLETE, | |
| "message": "Profile incomplete. Please fill in all required fields.", | |
| "missing_fields": missing_fields | |
| } | |
| ) | |
| # Step 4: Apply rate limiting | |
| rate_limiter = get_personalize_rate_limiter() | |
| is_allowed, retry_after = rate_limiter.check(user_id) | |
| if not is_allowed: | |
| logger.warning(f"Rate limited user: {user_id}") | |
| return JSONResponse( | |
| status_code=429, | |
| content={ | |
| "success": False, | |
| "error": ErrorCodes.RATE_LIMITED, | |
| "message": "Too many requests. Please wait before trying again.", | |
| "retry_after": retry_after or 60 | |
| } | |
| ) | |
| logger.info(f"Personalizing for user {user_id}: level={profile.experience_level}, " | |
| f"languages={profile.programming_languages}, capability={profile.system_capability}") | |
| # Step 5: Call PersonalizeContentAgent | |
| logger.info(f"Personalizing chapter '{body.chapter_id}' for user: {user_id}") | |
| agent = get_personalize_agent() | |
| try: | |
| result = await agent.personalize( | |
| content=body.chapter_content, | |
| profile=profile | |
| ) | |
| except Exception as e: | |
| logger.error(f"Personalization agent failed: {e}") | |
| return JSONResponse( | |
| status_code=500, | |
| content={ | |
| "success": False, | |
| "error": ErrorCodes.PERSONALIZATION_FAILED, | |
| "message": "Personalization service encountered an error", | |
| "retry_after": 5 | |
| } | |
| ) | |
| # Step 6: Build and return success response | |
| response = PersonalizeSuccessResponse( | |
| success=True, | |
| personalized_content=result.personalized_content, | |
| chapter_id=body.chapter_id, | |
| personalization_summary=PersonalizationSummary( | |
| experience_level=result.experience_level, | |
| programming_context=result.programming_context, | |
| hardware_context=HardwareContext(**result.hardware_context), | |
| adjustments_made=result.adjustments_made | |
| ), | |
| timestamp=datetime.utcnow().isoformat() + "Z" | |
| ) | |
| logger.info(f"Successfully personalized chapter '{body.chapter_id}' for user: {user_id}") | |
| return response | |
| except Exception as e: | |
| logger.error(f"Unexpected error in personalize endpoint: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return JSONResponse( | |
| status_code=500, | |
| content={ | |
| "success": False, | |
| "error": ErrorCodes.PERSONALIZATION_FAILED, | |
| "message": "Personalization service encountered an error", | |
| "retry_after": 5 | |
| } | |
| ) | |
| # ============================================================================= | |
| # DEBUG ENDPOINTS - Commented out for production | |
| # ============================================================================= | |
| # @router.get("/debug/profile") | |
| # async def debug_profile(session: AsyncSession = Depends(get_db_session)): | |
| # """ | |
| # DEBUG: View all profiles in database. | |
| # TODO: Remove this endpoint before production deploy! | |
| # """ | |
| # from sqlalchemy import text | |
| # result = await session.execute(text(""" | |
| # SELECT auth_user_id, experience_level, programming_languages, | |
| # frameworks_platforms, device_type, operating_system, system_capability | |
| # FROM user_profiles | |
| # """)) | |
| # rows = result.fetchall() | |
| # | |
| # profiles = [] | |
| # for row in rows: | |
| # profiles.append({ | |
| # "auth_user_id": row[0], | |
| # "experience_level": row[1], | |
| # "programming_languages": row[2], | |
| # "frameworks_platforms": row[3], | |
| # "device_type": row[4], | |
| # "operating_system": row[5], | |
| # "system_capability": row[6] | |
| # }) | |
| # | |
| # return {"profiles": profiles, "count": len(profiles)} | |
| # @router.get("/debug/update/{user_id}/{experience_level}") | |
| # async def debug_update_profile( | |
| # user_id: str, | |
| # experience_level: str, | |
| # session: AsyncSession = Depends(get_db_session) | |
| # ): | |
| # """ | |
| # DEBUG: Update a user's experience level. | |
| # TODO: Remove this endpoint before production deploy! | |
| # | |
| # Usage: PUT /api/debug/profile/PK6EryRXKtWazlmRifLU9OD7ZMSSTXT9?experience_level=beginner | |
| # """ | |
| # from sqlalchemy import text | |
| # | |
| # valid_levels = ["beginner", "intermediate", "advanced", "expert"] | |
| # if experience_level not in valid_levels: | |
| # return {"error": f"Invalid level. Must be one of: {valid_levels}"} | |
| # | |
| # await session.execute( | |
| # text("UPDATE user_profiles SET experience_level = :level WHERE auth_user_id = :user_id"), | |
| # {"level": experience_level, "user_id": user_id} | |
| # ) | |
| # await session.commit() | |
| # | |
| # return {"success": True, "user_id": user_id, "new_experience_level": experience_level} | |