Files
goodgo-platform/libs/ai-services/app/routers/avm_v2.py
Ho Ngoc Hai 729afe2db6 feat(ai-services): dedicated GET /avm/v2/feature-importance endpoint (TEC-2760)
Exposes ensemble feature importance as a standalone endpoint per R5.1 spec.
Aggregates XGBoost (0.4) + LightGBM (0.35) + CatBoost (0.25) gain when trained
boosters are loaded; falls back to the curated heuristic ranking otherwise, so
callers can depend on the endpoint during scaffold/heuristic-only runs.

- Factored heuristic drivers into a shared constant (_HEURISTIC_DRIVERS)
- Added AVMv2FeatureImportanceResponse model (model_version + source + drivers)
- Added service.get_feature_importance() public method
- Added tests/test_avm_v2.py::test_feature_importance_heuristic (24 total pass)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-18 15:27:30 +07:00

86 lines
3.0 KiB
Python

"""AVM v2 ensemble router — residential property valuation."""
from fastapi import APIRouter, HTTPException
from app.models.avm_v2 import (
ABComparisonRequest,
ABComparisonResponse,
AVMv2FeatureImportanceResponse,
AVMv2ModelInfo,
AVMv2PredictRequest,
AVMv2PredictResponse,
AVMv2RollbackRequest,
AVMv2TrainRequest,
AVMv2TrainResponse,
)
from app.services.avm_v2_service import avm_v2_service
router = APIRouter(prefix="/avm/v2", tags=["AVM v2 Ensemble"])
@router.post("/predict", response_model=AVMv2PredictResponse)
def predict_v2(req: AVMv2PredictRequest) -> AVMv2PredictResponse:
"""Predict residential property price using the multi-model ensemble.
Ensemble: XGBoost (0.4) + LightGBM (0.35) + CatBoost (0.25).
Falls back to heuristic when trained models are not available.
"""
return avm_v2_service.predict(req)
@router.post("/train", response_model=AVMv2TrainResponse)
def train_v2(req: AVMv2TrainRequest) -> AVMv2TrainResponse:
"""Trigger model retraining with Optuna hyperparameter optimization.
Loads training data from the model directory, runs Optuna for each
model in the ensemble, saves versioned artifacts, and registers
the new version in the model registry.
"""
return avm_v2_service.train(req)
@router.post("/compare-v1", response_model=ABComparisonResponse)
def compare_v1(req: ABComparisonRequest) -> ABComparisonResponse:
"""Compare v1 (single-model) vs v2 (ensemble) predictions side by side.
Runs both models on the same property and returns price difference,
confidence delta, and a recommendation on which to prefer.
"""
return avm_v2_service.compare_v1(req)
@router.get("/model-info", response_model=AVMv2ModelInfo)
def model_info_v2() -> AVMv2ModelInfo:
"""Get current active ensemble model information."""
return avm_v2_service.get_model_info()
@router.get("/feature-importance", response_model=AVMv2FeatureImportanceResponse)
def feature_importance_v2() -> AVMv2FeatureImportanceResponse:
"""Global feature importance for the active ensemble.
Aggregates XGBoost gain (0.4) + LightGBM gain (0.35) + CatBoost importance (0.25)
when trained boosters are loaded. Falls back to a curated heuristic ranking when
the service is running without artifacts.
"""
return avm_v2_service.get_feature_importance()
@router.get("/versions", response_model=list[AVMv2ModelInfo])
def list_versions() -> list[AVMv2ModelInfo]:
"""List all registered model versions with their metrics and status."""
return avm_v2_service.list_versions()
@router.post("/rollback", response_model=AVMv2ModelInfo)
def rollback(req: AVMv2RollbackRequest) -> AVMv2ModelInfo:
"""Rollback to a previously trained model version.
Copies the target version's artifacts to the active model directory,
reloads models, and updates the registry.
"""
try:
return avm_v2_service.rollback(req.target_version)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))