""" API Integration Guide - Applying Dynamic Validation to COGNEXA ML Endpoints This guide shows how to integrate the DynamicRequestValidator from dynamic_api_handler.py into your FastAPI endpoints to fix 422 validation errors. PROBLEM: The following endpoints return 422 errors due to strict Pydantic validation: - /api/personality/analyze-responses - /api/productivity/forecast - /api/personalization/predict-engagement SOLUTION: Replace Pydantic BaseModel with flexible dynamic validation """ # ============================================================================ # STEP 1: Import the Dynamic Validator # ============================================================================ from fastapi import FastAPI, Body from fastapi.responses import JSONResponse app = FastAPI() from ML.dynamic_api_handler import DynamicRequestValidator, FlexibleRequestHandler # Helper function stubs (API integration guide only) def analyze_personality_scores(responses: dict, response_scale_max: int): return {"responses": responses, "scale": response_scale_max} def generate_forecast(user_id: str, historical_data: list, forecast_days: int, personality: dict = None): return {"user_id": user_id, "forecast_days": forecast_days, "data_points": len(historical_data)} def predict_user_engagement(user_id: str, task_id: str, task: dict, user_profile: dict = None): return 0.5 def run_prediction(user_id: str, complexity: float, count: int): return {"user_id": user_id, "complexity": complexity, "count": count} # ============================================================================ # STEP 2: Update Personality Endpoint # ============================================================================ # BEFORE (Strict Pydantic - will fail on "4.5" instead of 4.5): """ from pydantic import BaseModel class PersonalityResponse(BaseModel): responses: dict response_scale_max: int @app.post("/api/personality/analyze-responses") async def analyze_responses(request: PersonalityResponse): # If user sends {"q1": "4"} instead of {"q1": 4}, this fails with 422 ... """ # AFTER (Flexible Dynamic Validation): from fastapi import Body from fastapi.responses import JSONResponse @app.post("/api/personality/analyze-responses") async def analyze_responses(request: dict = Body(...)): """ Flexible endpoint that coerces types and handles missing fields. Accepts: - {"q1": 4, "q2": "3.5", ...} ✅ Coerces string to float - {"q1": 4} ✅ Fills missing fields with defaults - {"q1": "abc"} ✅ Returns helpful error message instead of 422 """ # Validate and coerce the request try: validated = DynamicRequestValidator.validate_personality_responses(request) # Proceed with analysis result = analyze_personality_scores( responses=validated['responses'], response_scale_max=validated['response_scale_max'] ) return JSONResponse( status_code=200, content={"status": "success", "analysis": result} ) except ValueError as e: return JSONResponse( status_code=400, # Bad Request (not 422) content={"error": str(e)} ) # ============================================================================ # STEP 3: Update Productivity Forecast Endpoint # ============================================================================ # BEFORE: """ class ProductivityForecast(BaseModel): user_id: str forecast_days: int historical_data: list @app.post("/api/productivity/forecast") async def forecast_productivity(request: ProductivityForecast): # Strict validation fails on type mismatches ... """ # AFTER: @app.post("/api/productivity/forecast") async def forecast_productivity(request: dict = Body(...)): """ Flexible forecast endpoint with intelligent type coercion. Accepts: - Different data formats - Missing optional fields (uses defaults) - String numbers ("7" → 7) """ try: validated = DynamicRequestValidator.validate_productivity_forecast(request) # Proceed with forecasting forecast = generate_forecast( user_id=validated['user_id'], historical_data=validated['historical_data'], forecast_days=validated['forecast_days'], personality=validated.get('personality') ) return JSONResponse( status_code=200, content={"status": "success", "forecast": forecast} ) except ValueError as e: return JSONResponse( status_code=400, content={"error": str(e)} ) # ============================================================================ # STEP 4: Update Engagement Prediction Endpoint # ============================================================================ # BEFORE: """ class EngagementPrediction(BaseModel): user_id: str task_id: str task: dict @app.post("/api/personalization/predict-engagement") async def predict_engagement(request: EngagementPrediction): # Fails on any type mismatch ... """ # AFTER: @app.post("/api/personalization/predict-engagement") async def predict_engagement(request: dict = Body(...)): """ Flexible engagement prediction with flexible input handling. Accepts: - Various input formats - Flexible data structures - Intelligent field coercion """ try: validated = DynamicRequestValidator.validate_engagement_prediction(request) # Proceed with prediction engagement_score = predict_user_engagement( user_id=validated['user_id'], task_id=validated['task_id'], task=validated['task'], user_profile=validated.get('user_profile', {}) ) return JSONResponse( status_code=200, content={ "status": "success", "engagement_prediction": engagement_score } ) except ValueError as e: return JSONResponse( status_code=400, content={"error": str(e)} ) # ============================================================================ # STEP 5: Alternative - Use FlexibleRequestHandler for Complex Validation # ============================================================================ @app.post("/api/complex/predict") async def complex_prediction(request: dict = Body(...)): """ For more complex validation logic, use FlexibleRequestHandler. """ handler = FlexibleRequestHandler() try: # Coerce individual fields user_id = handler.coerce_value(request.get('user_id'), str, required=True) complexity = handler.coerce_value(request.get('complexity'), float) # "4.5" → 4.5 count = handler.coerce_value(request.get('count'), int, default=0) # "5" → 5 # Proceed with prediction result = run_prediction( user_id=user_id, complexity=complexity, count=count ) return {"status": "success", "result": result} except (ValueError, TypeError) as e: return JSONResponse( status_code=400, content={"error": f"Invalid input: {str(e)}"} ) # ============================================================================ # STEP 6: Migration Checklist # ============================================================================ """ CHECKLIST FOR MIGRATING ENDPOINTS: 1. ✅ Identify problematic endpoints (search for Pydantic BaseModel usage) - /api/personality/analyze-responses - /api/productivity/forecast - /api/personalization/predict-engagement 2. ✅ Replace endpoint signature: OLD: def endpoint(request: PydanticModel): NEW: def endpoint(request: dict = Body(...)): 3. ✅ Add DynamicRequestValidator call: validated = DynamicRequestValidator.validate_*(request) 4. ✅ Update field access: OLD: request.field_name NEW: validated['field_name'] 5. ✅ Add error handling: try: # validation & processing except ValueError as e: return JSONResponse(status_code=400, content={"error": str(e)}) 6. ✅ Test with various input types: - {"field": 5} # integer - {"field": "5"} # string number - {"field": 5.5} # float - {"field": "5.5"} # string float - {} # missing (should use defaults) - {"field": "invalid"} # invalid (should return 400, not 422) 7. ✅ Verify 422 errors → 400 errors: - Invalid input now returns 400 Bad Request - Type coercion handles edge cases - Helpful error messages provided 8. ✅ Test all endpoints after migration: python test_api_endpoints.py """ # ============================================================================ # STEP 7: Example Requests (cURL) # ============================================================================ """ # Test Personality Analysis Endpoint curl -X POST http://localhost:8000/api/personality/analyze-responses \\ -H "Content-Type: application/json" \\ -d '{ "responses": { "q1": 4, "q2": "3.5", "q3": 5 }, "response_scale_max": 5 }' # Expected Response: { "status": "success", "analysis": { ... } } # Test Productivity Forecast Endpoint curl -X POST http://localhost:8000/api/productivity/forecast \\ -H "Content-Type: application/json" \\ -d '{ "user_id": "user123", "forecast_days": "7", "historical_data": [ {"date": "2026-03-20", "completed_tasks": "5"} ] }' # Expected Response: { "status": "success", "forecast": { ... } } # Test Engagement Prediction Endpoint curl -X POST http://localhost:8000/api/personalization/predict-engagement \\ -H "Content-Type: application/json" \\ -d '{ "user_id": "user123", "task_id": "task456", "task": { "title": "Review notes", "category": "WORK", "priority": "MEDIUM" } }' # Expected Response: { "status": "success", "engagement_prediction": 0.78 } """ # ============================================================================ # STEP 8: Validation Schemas (Reference) # ============================================================================ """ PERSONALITY RESPONSE VALIDATION: - responses: dict (required) - Keys: string (question IDs) - Values: int/float (scores, will be coerced) - response_scale_max: int (default: 5) - Will coerce string to int PRODUCTIVITY FORECAST VALIDATION: - user_id: str (required) - historical_data: list (required) - List of dicts with productivity data - forecast_days: int (default: 7) - Will coerce string to int - personality: dict (optional) - Big Five personality scores ENGAGEMENT PREDICTION VALIDATION: - user_id: str (required) - task_id: str (required) - task: dict (required) - Must contain task details - user_profile: dict (optional) - Optional user profile data """ # ============================================================================ # RESOURCES # ============================================================================ """ Files to reference: - /ML/dynamic_api_handler.py - Full validation implementation - /ML/test_api_endpoints.py - Comprehensive API tests - /ML/api_patches.md - This file - /ML/test_ml_service.py - Unit tests for validators Commands: - Test validators: python test_ml_service.py - Test endpoints: python test_api_endpoints.py - Run server: python main.py Common Issues & Solutions: 1. "422 Unprocessable Entity" → Use DynamicRequestValidator instead of strict Pydantic 2. "400 Bad Request" with helpful message → This is expected for invalid data (better than 422) 3. String numbers not coercing → Ensure coerce_value() is being called → Check type hints in validation method 4. Missing fields causing errors → Set defaults in validation method → Use .get() with defaults in validated dict """