Spaces:
Runtime error
Runtime error
File size: 9,104 Bytes
a5ddf46 9f77adc a5ddf46 9f77adc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | """
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"])
@router.post(
"/personalize",
response_model=Union[PersonalizeSuccessResponse, PersonalizeErrorResponse],
responses={
200: {"model": PersonalizeSuccessResponse, "description": "Personalization successful"},
400: {"model": PersonalizeErrorResponse, "description": "Invalid request or incomplete profile"},
401: {"model": PersonalizeErrorResponse, "description": "Authentication required"},
404: {"model": PersonalizeErrorResponse, "description": "Profile not found"},
429: {"model": PersonalizeErrorResponse, "description": "Rate limited"},
500: {"model": PersonalizeErrorResponse, "description": "Personalization failed"}
}
)
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}
|