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}