Spaces:
Sleeping
Sleeping
File size: 12,177 Bytes
5059de5 | 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 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 | """
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
"""
|