Add POI feature and imporve the overall backend implementation
This commit is contained in:
@@ -880,6 +880,9 @@ class EnhancedForecastingService:
|
||||
request: ForecastRequest
|
||||
) -> Dict[str, Any]:
|
||||
"""Prepare features with comprehensive fallbacks"""
|
||||
# Check for school holidays using external service
|
||||
is_holiday = await self._check_holiday(tenant_id, request.forecast_date)
|
||||
|
||||
features = {
|
||||
"date": request.forecast_date.isoformat(),
|
||||
"day_of_week": request.forecast_date.weekday(),
|
||||
@@ -889,7 +892,7 @@ class EnhancedForecastingService:
|
||||
"quarter": (request.forecast_date.month - 1) // 3 + 1,
|
||||
"week_of_year": request.forecast_date.isocalendar().week,
|
||||
"season": self._get_season(request.forecast_date.month),
|
||||
"is_holiday": self._is_spanish_holiday(request.forecast_date),
|
||||
"is_holiday": is_holiday,
|
||||
# CRITICAL FIX: Add tenant_id and inventory_product_id for historical feature enrichment
|
||||
"tenant_id": tenant_id,
|
||||
"inventory_product_id": request.inventory_product_id,
|
||||
@@ -977,6 +980,9 @@ class EnhancedForecastingService:
|
||||
weather_map: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Prepare features with comprehensive fallbacks using a pre-fetched weather map"""
|
||||
# Check for holidays using external service
|
||||
is_holiday = await self._check_holiday(tenant_id, request.forecast_date)
|
||||
|
||||
features = {
|
||||
"date": request.forecast_date.isoformat(),
|
||||
"day_of_week": request.forecast_date.weekday(),
|
||||
@@ -986,7 +992,7 @@ class EnhancedForecastingService:
|
||||
"quarter": (request.forecast_date.month - 1) // 3 + 1,
|
||||
"week_of_year": request.forecast_date.isocalendar().week,
|
||||
"season": self._get_season(request.forecast_date.month),
|
||||
"is_holiday": self._is_spanish_holiday(request.forecast_date),
|
||||
"is_holiday": is_holiday,
|
||||
# CRITICAL FIX: Add tenant_id and inventory_product_id for historical feature enrichment
|
||||
"tenant_id": tenant_id,
|
||||
"inventory_product_id": request.inventory_product_id,
|
||||
@@ -1041,11 +1047,44 @@ class EnhancedForecastingService:
|
||||
else:
|
||||
return 4 # Autumn
|
||||
|
||||
def _is_spanish_holiday(self, date_obj: date) -> bool:
|
||||
"""Check if a date is a major Spanish holiday"""
|
||||
async def _check_holiday(self, tenant_id: str, date_obj: date) -> bool:
|
||||
"""
|
||||
Check if a date is a holiday using external service calendar
|
||||
|
||||
Falls back to Spanish national holidays if:
|
||||
- Tenant has no calendar configured
|
||||
- External service is unavailable
|
||||
"""
|
||||
try:
|
||||
# Get tenant's calendar ID
|
||||
calendar_id = await self.data_client.get_tenant_calendar(tenant_id)
|
||||
|
||||
if calendar_id:
|
||||
# Check school holiday via external service
|
||||
is_school_holiday = await self.data_client.check_school_holiday(
|
||||
calendar_id=calendar_id,
|
||||
check_date=date_obj.isoformat(),
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
return is_school_holiday
|
||||
else:
|
||||
# Fallback to Spanish national holidays
|
||||
return self._is_spanish_national_holiday(date_obj)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Holiday check failed, falling back to national holidays",
|
||||
error=str(e),
|
||||
tenant_id=tenant_id,
|
||||
date=date_obj.isoformat()
|
||||
)
|
||||
return self._is_spanish_national_holiday(date_obj)
|
||||
|
||||
def _is_spanish_national_holiday(self, date_obj: date) -> bool:
|
||||
"""Check if a date is a major Spanish national holiday (fallback)"""
|
||||
month_day = (date_obj.month, date_obj.day)
|
||||
spanish_holidays = [
|
||||
(1, 1), (1, 6), (5, 1), (8, 15), (10, 12),
|
||||
(1, 1), (1, 6), (5, 1), (8, 15), (10, 12),
|
||||
(11, 1), (12, 6), (12, 8), (12, 25)
|
||||
]
|
||||
return month_day in spanish_holidays
|
||||
|
||||
106
services/forecasting/app/services/poi_feature_service.py
Normal file
106
services/forecasting/app/services/poi_feature_service.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
POI Feature Service for Forecasting
|
||||
|
||||
Fetches POI features for use in demand forecasting predictions.
|
||||
Ensures feature consistency between training and prediction.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
from typing import Dict, Any, Optional
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class POIFeatureService:
|
||||
"""
|
||||
POI feature service for forecasting.
|
||||
|
||||
Fetches POI context from External service to ensure
|
||||
prediction uses the same features as training.
|
||||
"""
|
||||
|
||||
def __init__(self, external_service_url: str = "http://external-service:8000"):
|
||||
"""
|
||||
Initialize POI feature service.
|
||||
|
||||
Args:
|
||||
external_service_url: Base URL for external service
|
||||
"""
|
||||
self.external_service_url = external_service_url.rstrip("/")
|
||||
self.poi_context_endpoint = f"{self.external_service_url}/poi-context"
|
||||
|
||||
async def get_poi_features(
|
||||
self,
|
||||
tenant_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get POI features for tenant.
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant UUID
|
||||
|
||||
Returns:
|
||||
Dictionary with POI features or empty dict if not available
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
f"{self.poi_context_endpoint}/{tenant_id}"
|
||||
)
|
||||
|
||||
if response.status_code == 404:
|
||||
logger.warning(
|
||||
"No POI context found for tenant",
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
return {}
|
||||
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
poi_context = data.get("poi_context", {})
|
||||
ml_features = poi_context.get("ml_features", {})
|
||||
|
||||
logger.info(
|
||||
"POI features retrieved for forecasting",
|
||||
tenant_id=tenant_id,
|
||||
feature_count=len(ml_features)
|
||||
)
|
||||
|
||||
return ml_features
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(
|
||||
"Failed to fetch POI features for forecasting",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e)
|
||||
)
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Unexpected error fetching POI features",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e),
|
||||
exc_info=True
|
||||
)
|
||||
return {}
|
||||
|
||||
async def check_poi_service_health(self) -> bool:
|
||||
"""
|
||||
Check if POI service is accessible.
|
||||
|
||||
Returns:
|
||||
True if service is healthy, False otherwise
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(
|
||||
f"{self.poi_context_endpoint}/health"
|
||||
)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"POI service health check failed",
|
||||
error=str(e)
|
||||
)
|
||||
return False
|
||||
Reference in New Issue
Block a user