""" PriceOye Phone Recommendation AI ================================= Scoring Engine — Professional ML Benchmark System Benchmarks (hidden from user, used internally only): - Android: Samsung Galaxy S26 Ultra - iOS: iPhone 17 Pro Max Each phone is scored across 10 categories × multiple sub-dimensions. Final score = weighted sum of category averages per user priority profile. """ from dataclasses import dataclass, field from typing import Optional import math # ───────────────────────────────────────────── # DATA STRUCTURES # ───────────────────────────────────────────── @dataclass class SubDimension: score: float # 0.0 – 10.0 note: str # Human-readable justification @dataclass class PhoneDimensions: """All benchmark sub-dimensions for a phone.""" # Camera (9 sub-dims) camera_main_sensor: SubDimension camera_aperture: SubDimension camera_optical_zoom: SubDimension camera_ultrawide: SubDimension camera_video: SubDimension camera_night_mode: SubDimension camera_front: SubDimension camera_lens_quality: SubDimension camera_ois: SubDimension # Performance (5 sub-dims) perf_cpu: SubDimension perf_gpu: SubDimension perf_ram_type: SubDimension perf_thermal: SubDimension perf_ai_chip: SubDimension # Display (6 sub-dims) disp_resolution: SubDimension disp_brightness: SubDimension disp_color_accuracy: SubDimension disp_refresh_rate: SubDimension disp_technology: SubDimension disp_touch_sampling: SubDimension # Battery (4 sub-dims) batt_capacity: SubDimension batt_real_world_sot: SubDimension batt_efficiency: SubDimension batt_wireless: SubDimension # Charging (4 sub-dims) charg_wired_speed: SubDimension charg_wireless_speed: SubDimension charg_reverse: SubDimension charg_inbox_charger: SubDimension # RAM (3 sub-dims) ram_capacity: SubDimension ram_type: SubDimension ram_os_management: SubDimension # Storage (3 sub-dims) stor_capacity: SubDimension stor_speed: SubDimension stor_expandable: SubDimension # Build (4 sub-dims) build_frame: SubDimension build_ip_rating: SubDimension build_front_glass: SubDimension build_form_factor: SubDimension # Software (4 sub-dims) soft_update_policy: SubDimension soft_bloatware: SubDimension soft_ai_features: SubDimension soft_ecosystem: SubDimension # Audio (3 sub-dims) audio_speakers: SubDimension audio_headphone_jack: SubDimension audio_bt_codecs: SubDimension @dataclass class Phone: id: str name: str brand: str os: str # 'android' | 'ios' price_pkr: int price_label: str priceoye_url: str whatmobile_url: str emoji: str tags: list[str] highlights: dict[str, str] dims: PhoneDimensions available_on_priceoye: bool = True @dataclass class UserPreferences: budget: Optional[int] = None os_preference: Optional[str] = None # 'android' | 'ios' | 'any' priority: Optional[str] = None # see WEIGHT_PROFILES keys brand_preference: Optional[str] = None # e.g. 'Samsung', 'Apple' brand_avoid: list[str] = field(default_factory=list) session_memory: dict = field(default_factory=dict) # cross-turn memory # ───────────────────────────────────────────── # CATEGORY AVERAGES # ───────────────────────────────────────────── CATEGORY_GROUPS = { "camera": [ "camera_main_sensor", "camera_aperture", "camera_optical_zoom", "camera_ultrawide", "camera_video", "camera_night_mode", "camera_front", "camera_lens_quality", "camera_ois", ], "performance": [ "perf_cpu", "perf_gpu", "perf_ram_type", "perf_thermal", "perf_ai_chip", ], "display": [ "disp_resolution", "disp_brightness", "disp_color_accuracy", "disp_refresh_rate", "disp_technology", "disp_touch_sampling", ], "battery": [ "batt_capacity", "batt_real_world_sot", "batt_efficiency", "batt_wireless", ], "charging": [ "charg_wired_speed", "charg_wireless_speed", "charg_reverse", "charg_inbox_charger", ], "ram": ["ram_capacity", "ram_type", "ram_os_management"], "storage": ["stor_capacity", "stor_speed", "stor_expandable"], "build": [ "build_frame", "build_ip_rating", "build_front_glass", "build_form_factor", ], "software": [ "soft_update_policy", "soft_bloatware", "soft_ai_features", "soft_ecosystem", ], "audio": [ "audio_speakers", "audio_headphone_jack", "audio_bt_codecs", ], } def get_category_scores(phone: Phone) -> dict[str, float]: """Return average score per category.""" scores = {} for cat, fields in CATEGORY_GROUPS.items(): vals = [getattr(phone.dims, f).score for f in fields] scores[cat] = round(sum(vals) / len(vals), 2) return scores def get_sub_scores(phone: Phone, category: str) -> list[dict]: """Return detailed sub-dimension breakdown for a category.""" fields = CATEGORY_GROUPS.get(category, []) result = [] for f in fields: dim: SubDimension = getattr(phone.dims, f) label = f.replace(f.split("_")[0] + "_", "").replace("_", " ").title() result.append({ "label": label, "score": dim.score, "note": dim.note, }) return result # ───────────────────────────────────────────── # WEIGHT PROFILES # Must sum to 1.0 per profile # ───────────────────────────────────────────── WEIGHT_PROFILES: dict[str, dict[str, float]] = { "photography": { "camera": 0.45, "display": 0.15, "storage": 0.10, "performance": 0.10, "battery": 0.06, "build": 0.05, "software": 0.04, "charging": 0.02, "ram": 0.02, "audio": 0.01, }, "gaming": { "performance": 0.30, "display": 0.20, "ram": 0.15, "battery": 0.12, "charging": 0.08, "audio": 0.05, "build": 0.05, "camera": 0.03, "software": 0.01, "storage": 0.01, }, "battery": { "battery": 0.30, "charging": 0.25, "performance": 0.15, "display": 0.10, "ram": 0.07, "camera": 0.06, "build": 0.04, "software": 0.02, "storage": 0.01, "audio": 0.00, }, "value": { "camera": 0.18, "performance": 0.18, "battery": 0.15, "display": 0.12, "charging": 0.10, "storage": 0.10, "build": 0.07, "software": 0.05, "ram": 0.03, "audio": 0.02, }, "business": { "software": 0.25, "performance": 0.20, "display": 0.15, "build": 0.12, "camera": 0.10, "battery": 0.08, "ram": 0.05, "charging": 0.03, "storage": 0.01, "audio": 0.01, }, "balanced": { "camera": 0.18, "performance": 0.17, "battery": 0.14, "display": 0.13, "charging": 0.10, "build": 0.09, "software": 0.08, "ram": 0.05, "storage": 0.04, "audio": 0.02, }, "ios": { "software": 0.25, "camera": 0.22, "performance": 0.18, "build": 0.12, "display": 0.10, "battery": 0.07, "ram": 0.03, "charging": 0.02, "storage": 0.01, "audio": 0.00, }, } # ───────────────────────────────────────────── # CORE SCORING FUNCTION # ───────────────────────────────────────────── IPHONE_MIN_PRICE = 280000 # Minimum realistic iPhone price in Pakistan (PKR) def score_phone( phone: Phone, prefs: UserPreferences, phone_db: list["Phone"], ) -> float: """ Returns a float score 0–10 for this phone against user preferences. Returns -1 if the phone is ineligible. """ if not phone.available_on_priceoye: return -1.0 # OS filter if prefs.os_preference == "ios" and phone.os != "ios": return -1.0 if prefs.os_preference == "android" and phone.os != "android": return -1.0 # Budget filter — auto-exclude iPhones if budget too low if prefs.budget: if phone.os == "ios" and prefs.budget < IPHONE_MIN_PRICE: return -1.0 if phone.price_pkr > prefs.budget * 1.12: # 12% tolerance return -1.0 # Brand preference boost brand_boost = 0.0 if prefs.brand_preference and phone.brand.lower() == prefs.brand_preference.lower(): brand_boost = 0.3 if phone.brand in prefs.brand_avoid: return -1.0 # Get weights weights = WEIGHT_PROFILES.get(prefs.priority or "balanced", WEIGHT_PROFILES["balanced"]) # Compute weighted score cat_scores = get_category_scores(phone) total = sum(cat_scores[cat] * w for cat, w in weights.items()) # Savings bonus (mild — specs should dominate) if prefs.budget and prefs.budget > 0: savings_ratio = max(0.0, 1.0 - phone.price_pkr / prefs.budget) total += savings_ratio * 0.15 return round(min(10.0, total + brand_boost), 2) def recommend( prefs: UserPreferences, phone_db: list["Phone"], count: int = 1, ) -> list[tuple["Phone", float]]: """ Returns top-N phones scored for the given preferences. Each item is (phone, final_score). """ scored = [] for phone in phone_db: s = score_phone(phone, prefs, phone_db) if s > 0: scored.append((phone, s)) scored.sort(key=lambda x: x[1], reverse=True) return scored[:count] def find_closest_alternative( target_phone_name: str, prefs: UserPreferences, phone_db: list["Phone"], ) -> Optional["Phone"]: """ If a benchmarked phone is not on PriceOye, find the closest available alternative based on category score similarity. """ # Try to find target in DB target = next( (p for p in phone_db if target_phone_name.lower() in p.name.lower()), None ) if target and target.available_on_priceoye: return target if not target: # Fall back to top recommendation results = recommend(prefs, phone_db, count=1) return results[0][0] if results else None # Find most similar by category scores target_cats = get_category_scores(target) best_match = None best_dist = float("inf") for phone in phone_db: if not phone.available_on_priceoye: continue if phone.id == target.id: continue if prefs.os_preference and phone.os != prefs.os_preference: continue phone_cats = get_category_scores(phone) dist = math.sqrt( sum((target_cats[c] - phone_cats[c]) ** 2 for c in target_cats) ) if dist < best_dist: best_dist = dist best_match = phone return best_match # ───────────────────────────────────────────── # PRIORITY REASON GENERATOR # ───────────────────────────────────────────── def get_priority_reason(phone: Phone, priority: str) -> str: cats = get_category_scores(phone) reasons = { "photography": ( f"Camera score {cats['camera']:.1f}/10 — " f"{phone.dims.camera_lens_quality.note}" ), "gaming": ( f"Gaming performance {cats['performance']:.1f}/10 — " f"{phone.dims.perf_cpu.note}" ), "battery": ( f"Battery {cats['battery']:.1f}/10 · " f"Charging {cats['charging']:.1f}/10 — " f"{phone.dims.batt_real_world_sot.note}" ), "value": ( f"{phone.price_label} mein best value — " f"Camera {cats['camera']:.1f} · Performance {cats['performance']:.1f}" ), "business": ( f"Software {cats['software']:.1f}/10 · " f"Build {cats['build']:.1f}/10 — " f"{phone.dims.soft_update_policy.note}" ), "balanced": ( f"Koi bhi weak point nahi — " f"Camera {cats['camera']:.1f} · " f"Performance {cats['performance']:.1f} · " f"Battery {cats['battery']:.1f}" ), "ios": ( f"iOS ecosystem {cats['software']:.1f}/10 — " f"{phone.dims.soft_update_policy.note}" ), } return reasons.get(priority, f"Tamam categories mein strong performance: {sum(cats.values())/len(cats):.1f}/10 average")