boq-api / predictor.py
gabcares's picture
Upload predictor.py
0998aa2 verified
Raw
History Blame Contribute Delete
4.95 kB
import logging
import pandas as pd
from typing import List
from models.artifacts_loader import get_artifacts
from schemas.boq import BoQRequest, BoQResponse, BoQPrediction
from core.exceptions import ModelPredictionError
from utils.inference import predict_with_cascade
logger = logging.getLogger(__name__)
def _safe_float(value) -> float | None:
"""
Safely coerce a value to float, returning None for any uncoercible input.
Handles Excel overflow artifacts like '########' (column too narrow to
display the number), empty strings, and any other non-numeric garbage
that may arrive from poorly-formatted spreadsheets.
"""
if value is None:
return None
if isinstance(value, float) and pd.isna(value):
return None
try:
return float(value)
except (ValueError, TypeError):
return None
def predict_boq_batch_service(request: BoQRequest) -> BoQResponse:
"""
Core service logic to predict NRM categories for a batch of BoQ items.
"""
model = get_artifacts().model
if not model:
raise ModelPredictionError("ML model is currently unavailable.")
fallback_engine = get_artifacts().fallback_engine
if not fallback_engine:
raise ModelPredictionError("Fallback engine is currently unavailable.")
try:
# 1. Convert JSON payload to DataFrame
df_raw = pd.DataFrame(request.items)
# 2. Delegate EVERYTHING to the Hermetic Production Object and Fallback Engine
df_results = predict_with_cascade(
df_raw=df_raw, ml_model=model, fallback_engine=fallback_engine
)
# 3. Format the response
predictions_list: List[BoQPrediction] = []
for _, row in df_results.iterrows():
predictions_list.append(
BoQPrediction(
predicted_category=str(row.get("Predicted_Category", "Unknown")),
confidence=float(row.get("Prediction_Confidence", 0.0)),
prediction_source=str(row.get("Prediction_Source", "ML_Model")),
predicted_ge=str(row["Predicted_ge"]) if "Predicted_ge" in df_results.columns else str(row.get("ge", "")),
predicted_group_element=str(row["Predicted_group_element"]) if "Predicted_group_element" in df_results.columns else str(row.get("group_element", "")),
predicted_e=str(row["Predicted_e"]) if "Predicted_e" in df_results.columns else str(row.get("e", "")),
predicted_element=str(row["Predicted_element"]) if "Predicted_element" in df_results.columns else str(row.get("element", "")),
ge=str(row["ge"]) if "Predicted_ge" in df_results.columns and pd.notna(row["ge"]) else None,
group_element=str(row["group_element"]) if "Predicted_group_element" in df_results.columns and pd.notna(row["group_element"]) else None,
e=str(row["e"]) if "Predicted_e" in df_results.columns and pd.notna(row["e"]) else None,
element=str(row["element"]) if "Predicted_element" in df_results.columns and pd.notna(row["element"]) else None,
description=str(row.get("description", "Unknown")),
item=str(row.get("item")) if pd.notna(row.get("item")) else None,
notes=str(row.get("notes")) if pd.notna(row.get("notes")) else None,
qty=_safe_float(row.get("qty")),
rate_base_date=_safe_float(row.get("rate_base_date")),
rate_q1_2025=_safe_float(row.get("rate_q1_2025")),
uom=str(row.get("uom")) if pd.notna(row.get("uom")) else None,
project=str(row.get("project")) if pd.notna(row.get("project")) else None,
typology=str(row.get("typology")) if pd.notna(row.get("typology")) else None,
location=str(row.get("location")) if pd.notna(row.get("location")) else None,
base_date=str(row.get("base_date")) if pd.notna(row.get("base_date")) else None,
package=str(row.get("package")) if pd.notna(row.get("package")) else None,
rate_scope=str(row.get("rate_scope")) if pd.notna(row.get("rate_scope")) else None,
client_location=str(row.get("client_location")) if pd.notna(row.get("client_location")) else None,
basket_of_goods=str(row.get("basket_of_goods")) if pd.notna(row.get("basket_of_goods")) else None
)
)
return BoQResponse(
model_version=model.model_version,
model_name=model.model_name,
predictions=predictions_list,
)
except Exception as e:
logger.error(f"Inference error in service: {e}", exc_info=True)
raise ModelPredictionError(f"Internal inference error: {str(e)}") from e