from __future__ import annotations import json import threading from pathlib import Path from typing import Any from src.aspect_sentiment.probability_fusion import fuse_probabilities REPO_ROOT = Path(__file__).resolve().parents[2] MODEL_DIR = REPO_ROOT / "models" CONVERSION_MODEL_PATH = MODEL_DIR / "sales_conversion_model.pkl" MODEL_FEATURES_PATH = MODEL_DIR / "sales_conversion_features.pkl" MODEL_METRICS_PATH = MODEL_DIR / "sales_conversion_metrics.json" _MODEL_LOCK = threading.Lock() _CONVERSION_MODEL: Any | None = None _MODEL_FEATURES: list[str] | None = None def load_model_metrics() -> dict[str, Any]: if not MODEL_METRICS_PATH.exists(): return {} try: payload = json.loads(MODEL_METRICS_PATH.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return {} return payload if isinstance(payload, dict) else {} def get_model_features() -> list[str]: global _MODEL_FEATURES if _MODEL_FEATURES is None: with _MODEL_LOCK: if _MODEL_FEATURES is None: import joblib _MODEL_FEATURES = list(joblib.load(MODEL_FEATURES_PATH)) return _MODEL_FEATURES def get_conversion_model() -> Any: global _CONVERSION_MODEL if _CONVERSION_MODEL is None: with _MODEL_LOCK: if _CONVERSION_MODEL is None: import joblib _CONVERSION_MODEL = joblib.load(CONVERSION_MODEL_PATH) return _CONVERSION_MODEL def build_conversion_row(extraction: dict[str, Any]) -> tuple[Any, Any]: import pandas as pd row: dict[str, Any] = { "budget": 0, "sentiment_score": extraction.get("sentiment_score", 0), "confidence_score": extraction.get("confidence_score", 0), "hesitation_score": extraction.get("hesitation_score", 0), "delay_flag": extraction.get("delay_flag", 0), "feature_count": extraction.get("feature_count", 0), "brand_count": extraction.get("brand_count", 0), "interaction_length": extraction.get("interaction_length", 0), } from src.aspect_sentiment.mapping_engine import process_extractions normalized = process_extractions(extraction.get("raw_features", [])) for key, value in normalized.to_xgboost_dict().items(): row[key] = value explanation_row = pd.DataFrame([row]) model_row = explanation_row.copy() model_features = get_model_features() for column in model_features: if column not in model_row.columns: model_row[column] = 0 return model_row[model_features], explanation_row def predict_with_trained_model(extraction: dict[str, Any], text: str, agent_text: str = "") -> dict[str, Any]: model_row, _explanation_row = build_conversion_row(extraction) xgboost_prob = float(get_conversion_model().predict_proba(model_row)[0][1]) raw_features = extraction.get("raw_features", []) sentiment_score = float(extraction.get("sentiment_score", 0)) return fuse_probabilities( xgboost_prob=xgboost_prob, transcript=text, raw_features=raw_features, sentiment_score=sentiment_score, agent_transcript=agent_text, )