"""Price advisor — wraps the existing presow_v4 model with loud failure on missing artefacts. Dead branches (weekly, district_v2, sellhold_v4, presow_v3) no longer pretend to work; they return a structured "model_unavailable" error instead of silently degrading to v4. """ from __future__ import annotations import logging import sys from pathlib import Path from typing import Optional _MANDI_DIR = Path(__file__).resolve().parents[1] / "data" sys.path.insert(0, str(_MANDI_DIR)) logger = logging.getLogger(__name__) def get_presow_signal(crop: str, state: str, sowing_month: Optional[int] = None) -> dict: """Return harvest-price quantile forecast for crop+state. Quotes presow_v4 (P25/P50/P75) which IS present and validated at MAPE 7.6%. """ try: from mandi_advisor.enterprise_engine_v2 import get_presow_signal as _impl return _impl(crop, state, sowing_month=sowing_month) if sowing_month \ else _impl(crop, state) except FileNotFoundError as e: return {"error": "model_unavailable", "detail": f"presow_v4 artefacts missing: {e}", "remediation": "Run pipelines/check_artefacts.py to verify model files."} except Exception as e: logger.exception("[price] presow signal failed") return {"error": "internal", "detail": str(e)} def get_signal(crop: str, state: str, district: str = "", current_price: Optional[float] = None) -> dict: """Sell/Hold signal — currently only the v4 fallback is reachable until the weekly/district_v2 models are restored. If you call this for a perishable crop (Tomato, Onion, Potato) we explicitly return model_unavailable so callers know the answer is a degraded fallback, not a confident weekly forecast. """ PERISHABLES = {"tomato", "onion", "potato", "cabbage", "cauliflower", "spinach", "okra", "chilli", "leafy"} try: from mandi_advisor.enterprise_engine_v2 import get_signal as _impl result = _impl(crop, state, district, current_price) except Exception as e: return {"error": "internal", "detail": str(e)} if crop.lower() in PERISHABLES and "weekly" not in (result.get("model_version") or ""): result["warning"] = ( "Perishable crop without a weekly model — using state-level fallback. " "Re-run pipelines/build_weekly_perishable.py to restore weekly resolution." ) result["confidence"] = "LOW" return result def check_artefacts() -> dict: """Liveness check — which model files are actually on disk?""" md = _MANDI_DIR / "mandi_advisor" expected = { "presow_v4_model.pkl": True, "presow_v4_meta.pkl": True, "feature_store.parquet": True, "weekly_perishable_model.pkl": False, "weekly_perishable_meta.pkl": False, "district_sellhold_v2_model.pkl": False, "sellhold_v4_model.pkl": False, "presow_v3_model.pkl": False, "mandi_data_clean.parquet": False, } out = {"present": {}, "missing": {}} for name, must_have in expected.items(): p = md / name bucket = "present" if p.exists() else "missing" out[bucket][name] = {"required": must_have, "size_mb": round(p.stat().st_size / 1e6, 1) if p.exists() else 0} out["healthy"] = not any(d["required"] for n, d in out["missing"].items() if d["required"]) return out