File size: 29,293 Bytes
ee7d7b9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
"""
🎮 PREDICTION PLAYGROUND API
============================

Interactive prediction playground:
- GET /api/v1/automl/playground/config - Get slider configurations
- POST /api/v1/automl/playground/predict - Real-time predictions

SECURED: Uses JWT authentication for user isolation
"""

from fastapi import APIRouter, HTTPException, Query, Header
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
import numpy as np
import logging

from utils.paths import get_user_paths

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/automl/playground", tags=["AutoML - Playground"])


def get_secure_user_id(
    form_user_id: str,
    x_user_id: Optional[str] = None,
    authorization: Optional[str] = None
) -> str:
    """Get secure user_id prioritizing JWT over form/query data."""
    if authorization and authorization.startswith("Bearer "):
        try:
            from core.auth import decode_jwt_token
            token = authorization[7:]
            payload = decode_jwt_token(token)
            user_id = payload.get("sub")
            if user_id:
                return user_id
        except:
            pass
    
    if x_user_id and x_user_id not in ["null", "undefined", "", "default"]:
        return x_user_id
    
    if form_user_id and form_user_id not in ["default", "null", "undefined", ""]:
        return form_user_id
    
    import hashlib, time
    return f"guest_{hashlib.sha256(f'guest_{time.time()}'.encode()).hexdigest()[:12]}"


class SliderConfig(BaseModel):
    name: str
    type: str  # 'numeric', 'categorical', 'text', or 'datetime'
    min: Optional[Any] = None  # float for numeric, ISO string for datetime
    max: Optional[Any] = None  # float for numeric, ISO string for datetime
    step: Optional[float] = None
    default: Optional[Any] = None
    options: Optional[List[str]] = None
    placeholder: Optional[str] = None  # For text/datetime inputs


class PlaygroundConfig(BaseModel):
    model_name: str
    task_type: str
    target_column: str
    sliders: List[SliderConfig]
    class_names: Optional[List[str]] = None
    model_type: Optional[str] = None  # 'traditional', 'nlp', 'deep_learning'


class PlaygroundPredictRequest(BaseModel):
    values: Dict[str, Any]
    user_id: str = "default"
    mode: str = "traditional"  # 'traditional', 'nlp', 'deep_learning', 'fast', 'ultra'
    text: Optional[str] = None  # For NLP models


class PlaygroundPredictResponse(BaseModel):
    success: bool
    prediction: Optional[Any] = None
    prediction_label: Optional[str] = None
    confidence: Optional[float] = None
    probabilities: Optional[Dict[str, float]] = None


@router.get("/config", response_model=PlaygroundConfig)
async def get_playground_config(
    user_id: str = Query(default="default"),
    mode: str = Query(default="traditional"),  # 'traditional', 'nlp', 'deep_learning', 'auto'
    x_user_id: Optional[str] = Header(None, alias="X-User-ID"),
    authorization: Optional[str] = Header(None, alias="Authorization")
):
    """🎮 Get playground configuration with slider settings. SECURED."""
    try:
        # SECURITY: Get verified user_id
        user_id = get_secure_user_id(user_id, x_user_id, authorization)
        
        # AUTO mode: Try all engines and return the first one with feature_metadata
        if mode == "auto":
            logger.info(f"[Playground] Auto-detecting best mode for user {user_id}")
            errors = []
            
            # Try each mode in order of preference
            for try_mode in ['traditional', 'nlp', 'deep_learning']:
                try:
                    if try_mode == 'traditional':
                        from ml.automl_engine import ProductionMLEngine
                        engine = ProductionMLEngine()
                        engine.load(user_id)
                        if engine.model and getattr(engine, 'feature_metadata', []):
                            logger.info(f"[Playground] Found traditional ML model with feature_metadata")
                            mode = 'traditional'
                            break
                    elif try_mode == 'nlp':
                        from ml.nlp_engine import NLPEngine
                        engine = NLPEngine()
                        engine.load(user_id)
                        if engine.model:
                            logger.info(f"[Playground] Found NLP model")
                            mode = 'nlp'
                            break
                    elif try_mode == 'deep_learning':
                        from ml.deep_learning_engine import DeepLearningEngine
                        engine = DeepLearningEngine()
                        engine.load(user_id)
                        if engine.model and getattr(engine, 'feature_metadata', []):
                            logger.info(f"[Playground] Found Deep Learning model with feature_metadata")
                            mode = 'deep_learning'
                            break
                except Exception as e:
                    errors.append(f"{try_mode}: {str(e)}")
                    continue
            else:
                # No mode worked
                raise HTTPException(status_code=404, detail=f"No trained model found with feature metadata. Train a model first. Errors: {errors}")
        
        # Handle NLP mode - return inputs from actual dataset columns
        if mode == "nlp" or mode == "fast":
            from ml.nlp_engine import NLPEngine
            
            nlp_engine = NLPEngine()
            try:
                nlp_engine.load(user_id)
            except Exception as e:
                logger.warning(f"No NLP model found: {e}")
                raise HTTPException(status_code=404, detail="No NLP model found. Train an NLP model first.")
            
            if nlp_engine.model is None:
                raise HTTPException(status_code=404, detail="No NLP model found. Train an NLP model first.")
            
            text_column = getattr(nlp_engine, 'text_column', 'text')
            target_column = getattr(nlp_engine, 'target_column', 'target')
            task_type = getattr(nlp_engine, 'task_type', 'classification')
            
            # Get feature_metadata from NLP engine - includes ALL columns from user's data
            feature_metadata = getattr(nlp_engine, 'feature_metadata', [])
            
            # Build sliders from actual feature_metadata
            sliders = []
            
            if feature_metadata:
                # Use saved feature_metadata which has ONLY the user's actual columns
                for meta in feature_metadata:
                    name = meta.get('name', 'Unknown')
                    feat_type = meta.get('type', 'text')
                    
                    if feat_type == 'text':
                        sliders.append(SliderConfig(
                            name=name,
                            type='text',
                            placeholder=meta.get('placeholder', f"Enter {name}..."),
                            default=""
                        ))
                    elif feat_type == 'numeric':
                        min_val = meta.get('min', 0)
                        max_val = meta.get('max', 100)
                        mean_val = meta.get('mean', (min_val + max_val) / 2)
                        range_val = max_val - min_val if max_val > min_val else 1
                        step = max(0.01, range_val / 100)
                        
                        sliders.append(SliderConfig(
                            name=name,
                            type='numeric',
                            min=float(min_val),
                            max=float(max_val),
                            step=float(step),
                            default=float(mean_val)
                        ))
                    elif feat_type == 'datetime':
                        # Date picker for datetime columns
                        from datetime import datetime as dt
                        today = dt.now().strftime('%Y-%m-%d')
                        sliders.append(SliderConfig(
                            name=name,
                            type='datetime',
                            min=meta.get('min'),
                            max=meta.get('max'),
                            placeholder=meta.get('placeholder', 'Select date...'),
                            default=today
                        ))
                    elif feat_type == 'categorical':
                        options = meta.get('options', [])
                        sliders.append(SliderConfig(
                            name=name,
                            type='categorical',
                            options=options[:50],
                            default=options[0] if options else None
                        ))
            else:
                # Fallback: just show text input for the text column
                sliders = [
                    SliderConfig(
                        name=text_column,
                        type='text',
                        placeholder=f"Enter {text_column} for prediction...",
                        default=""
                    )
                ]
            
            class_names = None
            if task_type == 'classification' and hasattr(nlp_engine, 'label_encoder') and nlp_engine.label_encoder:
                class_names = nlp_engine.label_encoder.classes_.tolist()
            
            return PlaygroundConfig(
                model_name=getattr(nlp_engine, 'algorithm', 'NLP Model'),
                task_type=task_type,
                target_column=target_column,
                sliders=sliders,
                class_names=class_names,
                model_type='nlp'
            )
        
        # Handle Deep Learning mode
        if mode == "deep_learning" or mode == "ultra":
            from ml.deep_learning_engine import DeepLearningEngine
            
            dl_engine = DeepLearningEngine()
            try:
                dl_engine.load(user_id)
            except Exception as e:
                logger.warning(f"No Deep Learning model found: {e}")
                raise HTTPException(status_code=404, detail="No Deep Learning model found. Train a Deep Learning model first.")
            
            if dl_engine.model is None:
                raise HTTPException(status_code=404, detail="No Deep Learning model found. Train a Deep Learning model first.")
            
            feature_metadata = getattr(dl_engine, 'feature_metadata', [])
            
            # Fallback: build metadata from feature_columns if feature_metadata is empty
            if not feature_metadata and hasattr(dl_engine, 'feature_columns') and dl_engine.feature_columns:
                logger.warning("Building feature metadata from feature_columns (legacy model)")
                numeric_cols = getattr(dl_engine, 'numeric_cols', [])
                categorical_cols = getattr(dl_engine, 'categorical_cols', [])
                
                for col in dl_engine.feature_columns:
                    # Skip one-hot encoded column names (contain prefix_)
                    if '_' in col and any(col.startswith(cat + '_') for cat in categorical_cols):
                        continue
                    
                    if col in numeric_cols:
                        feature_metadata.append({
                            'name': col,
                            'type': 'numeric',
                            'min': 0,
                            'max': 100,
                            'mean': 50
                        })
                    elif col in categorical_cols:
                        feature_metadata.append({
                            'name': col,
                            'type': 'categorical',
                            'options': []
                        })
                    else:
                        # Assume numeric for unknown
                        feature_metadata.append({
                            'name': col,
                            'type': 'numeric',
                            'min': 0,
                            'max': 100,
                            'mean': 50
                        })
            
            sliders = []
            for meta in feature_metadata:
                name = meta.get('name', 'Unknown')
                feat_type = meta.get('type', 'numeric')
                
                if feat_type == 'numeric':
                    min_val = meta.get('min', 0)
                    max_val = meta.get('max', 100)
                    mean_val = meta.get('mean', (min_val + max_val) / 2)
                    range_val = max_val - min_val if max_val > min_val else 1
                    step = max(0.01, range_val / 100)
                    
                    sliders.append(SliderConfig(
                        name=name,
                        type='numeric',
                        min=float(min_val),
                        max=float(max_val),
                        step=float(step),
                        default=float(mean_val)
                    ))
                else:
                    options = meta.get('options', [])
                    sliders.append(SliderConfig(
                        name=name,
                        type='categorical',
                        options=options[:50],
                        default=options[0] if options else None
                    ))
            
            class_names = None
            if hasattr(dl_engine, 'label_encoder') and dl_engine.label_encoder:
                try:
                    class_names = dl_engine.label_encoder.classes_.tolist()
                except:
                    pass
            
            return PlaygroundConfig(
                model_name=getattr(dl_engine, 'algorithm', 'Deep Learning Model'),
                task_type=getattr(dl_engine, 'task_type', 'classification'),
                target_column=getattr(dl_engine, 'target_column', 'target'),
                sliders=sliders,
                class_names=class_names,
                model_type='deep_learning'
            )
        
        # Traditional ML mode (default)
        from ml.automl_engine import ProductionMLEngine
        
        engine = ProductionMLEngine()
        engine.load(user_id)
        
        if engine.model is None:
            raise HTTPException(status_code=404, detail="No trained model found. Train a model first.")
        
        feature_metadata = getattr(engine, 'feature_metadata', [])
        
        # Fallback: build metadata from numeric_cols/categorical_cols/text_cols (original columns)
        # DO NOT use feature_columns - those are engineered features!
        if not feature_metadata:
            numeric_cols = getattr(engine, 'numeric_cols', [])
            categorical_cols = getattr(engine, 'categorical_cols', [])
            text_cols = getattr(engine, 'text_cols', [])
            
            logger.warning("Building feature metadata from column lists (legacy model)")
            
            # Build from original column lists
            for col in numeric_cols:
                feature_metadata.append({
                    'name': col,
                    'type': 'numeric',
                    'min': 0,
                    'max': 100,
                    'mean': 50
                })
            for col in categorical_cols:
                feature_metadata.append({
                    'name': col,
                    'type': 'categorical',
                    'options': []
                })
            for col in text_cols:
                feature_metadata.append({
                    'name': col,
                    'type': 'text',
                    'placeholder': f'Enter {col}...'
                })
        
        # Final fallback: If still no metadata, try to infer from feature_columns using smart detection
        if not feature_metadata:
            logger.warning("No column lists found, using feature_columns with smart detection")
            feature_cols = getattr(engine, 'feature_columns', [])
            target_col = getattr(engine, 'target_column', None)
            
            # Filter out engineered features (those with patterns like _tfidf_, *2, _chars, etc.)
            skip_patterns = ['_tfidf_', '_count_', '_chars', '_words', '_sents', '_punct', '_avg_', 
                            '*', 'unnamed', 'index', '_id']
            
            for col in feature_cols:
                col_lower = col.lower()
                # Skip engineered features
                if any(pattern in col_lower for pattern in skip_patterns):
                    continue
                # Skip target column
                if col == target_col:
                    continue
                # Skip ID columns
                if col_lower == 'id' or col_lower.startswith('unnamed'):
                    continue
                    
                # Detect text columns by name heuristics
                text_keywords = ['text', 'content', 'body', 'email', 'review', 'description', 
                    'summary', 'message', 'overview', 'title', 'name', 'comment', 'note', 'bio']
                is_text = any(kw in col_lower for kw in text_keywords)
                
                if is_text:
                    feature_metadata.append({
                        'name': col,
                        'type': 'text',
                        'placeholder': f'Enter {col}...'
                    })
                else:
                    feature_metadata.append({
                        'name': col,
                        'type': 'numeric',
                        'min': 0,
                        'max': 100,
                        'mean': 50
                    })
        
        if not feature_metadata:
            raise HTTPException(status_code=400, detail="No feature metadata available. Please retrain the model.")
        
        # Filter out ID/index columns from sliders
        skip_names = ['unnamed', 'index', 'id', '_id']
        
        sliders = []
        for meta in feature_metadata:
            name = meta.get('name', 'Unknown')
            name_lower = name.lower()
            
            # Skip ID/index columns
            if name_lower in skip_names or name_lower.startswith('unnamed'):
                continue
                
            feat_type = meta.get('type', 'numeric')
            
            if feat_type == 'numeric':
                min_val = meta.get('min', 0)
                max_val = meta.get('max', 100)
                mean_val = meta.get('mean', (min_val + max_val) / 2)
                range_val = max_val - min_val
                step = max(0.01, range_val / 100)
                
                sliders.append(SliderConfig(
                    name=name,
                    type='numeric',
                    min=float(min_val),
                    max=float(max_val),
                    step=float(step),
                    default=float(mean_val)
                ))
            elif feat_type == 'text':
                # Text input for long text columns
                sliders.append(SliderConfig(
                    name=name,
                    type='text',
                    placeholder=meta.get('placeholder', f"Enter {name}..."),
                    default=""
                ))
            elif feat_type == 'datetime':
                # Date picker for datetime columns
                from datetime import datetime
                today = datetime.now().strftime('%Y-%m-%d')
                sliders.append(SliderConfig(
                    name=name,
                    type='datetime',
                    min=meta.get('min'),  # ISO string or None
                    max=meta.get('max'),  # ISO string or None
                    placeholder=meta.get('placeholder', 'Select date...'),
                    default=today
                ))
            else:
                # Categorical dropdown
                options = meta.get('options', [])
                sliders.append(SliderConfig(
                    name=name,
                    type='categorical',
                    options=options[:50],
                    default=options[0] if options else None
                ))
        
        class_names = None
        if hasattr(engine, 'target_encoder') and engine.target_encoder:
            class_names = engine.target_encoder.classes_.tolist()
        
        return PlaygroundConfig(
            model_name=getattr(engine, 'model_name', 'Model'),
            task_type=getattr(engine, 'task_type', 'classification'),
            target_column=getattr(engine, 'target_column', 'target'),
            sliders=sliders,
            class_names=class_names,
            model_type='traditional'
        )
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Playground config error: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/predict", response_model=PlaygroundPredictResponse)
async def playground_predict(
    request: PlaygroundPredictRequest,
    x_user_id: Optional[str] = Header(None, alias="X-User-ID"),
    authorization: Optional[str] = Header(None, alias="Authorization")
):
    """⚡ Real-time prediction for playground. SECURED."""
    try:
        # SECURITY: Get verified user_id
        user_id = get_secure_user_id(request.user_id, x_user_id, authorization)
        
        mode = request.mode or "traditional"
        
        # Handle NLP mode prediction
        if mode in ["nlp", "fast"]:
            from ml.nlp_engine import NLPEngine
            
            nlp_engine = NLPEngine()
            loaded = nlp_engine.load(user_id)
            
            if not loaded or nlp_engine.model is None:
                raise HTTPException(status_code=404, detail="No NLP model found")
            
            # Get text from request - could be in 'text' field or in 'values' dict
            text_input = request.text
            if not text_input and request.values:
                # Try to find text in values
                text_column = getattr(nlp_engine, 'text_column', 'text')
                text_input = request.values.get(text_column) or list(request.values.values())[0]
            
            if not text_input:
                raise HTTPException(status_code=400, detail="Text input is required for NLP prediction")
            
            # Make prediction
            result = nlp_engine.predict(text_input)
            
            prediction = result.get("prediction")
            confidence = result.get("confidence")
            probabilities = result.get("probabilities")
            
            return PlaygroundPredictResponse(
                success=True,
                prediction=prediction,
                prediction_label=str(prediction) if prediction is not None else None,
                confidence=confidence,
                probabilities=probabilities
            )
        
        # Handle Deep Learning mode prediction
        if mode in ["deep_learning", "ultra"]:
            from ml.deep_learning_engine import DeepLearningEngine
            
            dl_engine = DeepLearningEngine()
            loaded = dl_engine.load(user_id)
            
            if not loaded or dl_engine.model is None:
                raise HTTPException(status_code=404, detail="No Deep Learning model found")
            
            # Make prediction
            result = dl_engine.predict(request.values)
            
            prediction = result.get("prediction")
            confidence = result.get("confidence")
            probability = result.get("probability")
            
            # Convert probability list to dict if we have class names
            probabilities = None
            if probability and hasattr(dl_engine, 'target_encoder') and dl_engine.target_encoder:
                try:
                    class_names = dl_engine.target_encoder.classes_.tolist()
                    probabilities = {str(name): float(prob) for name, prob in zip(class_names, probability)}
                except:
                    pass
            
            return PlaygroundPredictResponse(
                success=True,
                prediction=prediction,
                prediction_label=str(prediction) if prediction is not None else None,
                confidence=confidence,
                probabilities=probabilities
            )
        
        # Traditional ML mode (default)
        from ml.automl_engine import automl_engine
        
        # Load model for THIS user
        loaded = automl_engine.load(user_id)
        if not loaded or automl_engine.model is None:
            raise HTTPException(status_code=404, detail="No trained model found")
        
        # predict() returns {prediction, probability, confidence, model}
        result = automl_engine.predict(request.values)
        
        prediction = result.get("prediction")
        confidence = result.get("confidence")
        probability = result.get("probability")
        
        # Convert probability list to dict if we have class names
        probabilities = None
        if probability and hasattr(automl_engine, 'target_encoder') and automl_engine.target_encoder:
            try:
                class_names = automl_engine.target_encoder.classes_.tolist()
                probabilities = {str(name): float(prob) for name, prob in zip(class_names, probability)}
            except:
                pass
        
        prediction_label = str(prediction) if prediction is not None else None
        
        return PlaygroundPredictResponse(
            success=True,
            prediction=prediction,
            prediction_label=prediction_label,
            confidence=confidence,
            probabilities=probabilities
        )
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Playground predict error: {e}")
        import traceback
        traceback.print_exc()
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/sensitivity")
async def sensitivity_analysis(
    feature_name: str = Query(...),
    user_id: str = Query(default="default"),
    x_user_id: Optional[str] = Header(None, alias="X-User-ID"),
    authorization: Optional[str] = Header(None, alias="Authorization")
):
    """📈 Sensitivity analysis for a feature. SECURED."""
    try:
        # SECURITY: Get verified user_id from JWT
        secure_user_id = get_secure_user_id(user_id, x_user_id, authorization)
        
        from ml.automl_engine import ProductionMLEngine
        
        engine = ProductionMLEngine()
        engine.load(secure_user_id)
        
        if engine.model is None:
            raise HTTPException(status_code=404, detail="No trained model found")
        
        feature_metadata = getattr(engine, 'feature_metadata', [])
        feature_meta = next((m for m in feature_metadata if m.get('name') == feature_name), None)
        
        if not feature_meta:
            raise HTTPException(status_code=400, detail=f"Feature not found: {feature_name}")
        
        # Create base values from defaults
        base_values = {}
        for meta in feature_metadata:
            name = meta.get('name')
            if meta.get('type') == 'numeric':
                base_values[name] = meta.get('mean', 0)
            else:
                options = meta.get('options', [])
                base_values[name] = options[0] if options else ""
        
        results = []
        
        if feature_meta.get('type') == 'numeric':
            min_val = feature_meta.get('min', 0)
            max_val = feature_meta.get('max', 100)
            test_values = np.linspace(min_val, max_val, 10)
            
            for val in test_values:
                test_input = base_values.copy()
                test_input[feature_name] = float(val)
                
                try:
                    pred = engine.predict(test_input)
                    results.append({
                        "value": float(val),
                        "prediction": pred.get("prediction"),
                        "confidence": pred.get("confidence")
                    })
                except:
                    pass
        else:
            options = feature_meta.get('options', [])[:10]
            
            for opt in options:
                test_input = base_values.copy()
                test_input[feature_name] = opt
                
                try:
                    pred = engine.predict(test_input)
                    results.append({
                        "value": opt,
                        "prediction": pred.get("prediction"),
                        "confidence": pred.get("confidence")
                    })
                except:
                    pass
        
        return {"success": True, "feature": feature_name, "results": results}
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Sensitivity analysis error: {e}")
        raise HTTPException(status_code=500, detail=str(e))