diff --git a/backend/ai/spare_parts_whitelist.py b/backend/ai/spare_parts_whitelist.py new file mode 100644 index 00000000..f3dc2996 --- /dev/null +++ b/backend/ai/spare_parts_whitelist.py @@ -0,0 +1,163 @@ +""" +Spare-parts classification module for AI-powered item extraction. + +This module provides functions to classify items as spare parts or consumables +using fuzzy matching against a predefined whitelist. +""" + +from typing import Optional +from fuzzywuzzy import fuzz + + +SPARE_PART_CATEGORIES = [ + "RAM", "DRAM", "DDR3", "DDR4", "DDR5", "SODIMM", "DIMM", + "SSD", "NVME", "M.2", "SATA", "HDD", "HARD DRIVE", "SOLID STATE DRIVE", + "CPU", "PROCESSOR", "APU", "GPU", "GRAPHICS CARD", "DISCRETE GPU", + "PSU", "POWER SUPPLY UNIT", "ADAPTER", "POWER MODULE", + "PCIE", "PCI", "RAID CONTROLLER", "NETWORK CARD", "NIC", + "HEATSINK", "CPU COOLER", "THERMAL SOLUTION", + "MOTHERBOARD", "BIOS", "CHIPSET" +] + +CONSUMABLE_KEYWORDS = [ + "CABLE", "CORD", "FASTENER", "SCREW", "WASHER", "BOLT", "STANDOFF", + "ADHESIVE", "THERMAL PASTE", "THERMAL PAD", "TAPE", + "CONNECTOR", "PLUG", "SOCKET", "ADAPTER" +] + +POWER_SUPPLY_CONSUMABLE_KEYWORDS = [ + "CABLE", "CORD", "GENERIC", "POWER CORD", "AC CORD" +] + +# Regex patterns for matching common categories +MEMORY_PATTERNS = ["DDR", "DRAM", "RAM", "SODIMM", "DIMM"] +STORAGE_PATTERNS = ["SSD", "NVME", "SATA", "HDD", "M.2"] +PROCESSOR_PATTERNS = ["CPU", "GPU", "PROCESSOR", "CORE", "APU"] +PSU_PATTERNS = ["PSU", "POWER SUPPLY", "POWER UNIT"] + + +def classify_as_spare_part(category: str) -> bool: + """ + Classify an item as a spare part or consumable based on category string. + + Uses a scoring algorithm combining exact matching, fuzzy matching, and + exclusion patterns to classify items. + + Args: + category: Item category string (e.g., "Kingston DDR4 RAM", "6ft SATA Cable") + + Returns: + True if item is classified as a spare part, False if consumable. + + Examples: + >>> classify_as_spare_part("Kingston DDR4 RAM") + True + >>> classify_as_spare_part("6ft SATA Cable") + False + >>> classify_as_spare_part("CPU Mounting Hardware Kit") + False + >>> classify_as_spare_part("Corsair RM850x 850W PSU") + True + """ + if not category: + return False + + # Normalize input + normalized = category.upper().strip() + + # Check exact match in spare parts categories + for spare_part in SPARE_PART_CATEGORIES: + if spare_part == normalized: + return True + + score = 0 + + # Check regex patterns for common categories + for pattern in MEMORY_PATTERNS: + if pattern in normalized: + score += 50 + break + + for pattern in STORAGE_PATTERNS: + if pattern in normalized: + score += 50 + break + + for pattern in PROCESSOR_PATTERNS: + if pattern in normalized: + score += 50 + break + + for pattern in PSU_PATTERNS: + if pattern in normalized: + score += 50 + break + + # Check fuzzy match against spare parts categories + best_fuzzy_score = 0 + for spare_part in SPARE_PART_CATEGORIES: + fuzzy_score = fuzz.token_set_ratio(normalized, spare_part) + if fuzzy_score > best_fuzzy_score: + best_fuzzy_score = fuzzy_score + + if best_fuzzy_score >= 80: + score += 50 + elif best_fuzzy_score >= 70: + score += 30 + + # Check exclusion patterns (consumables) — override other scores + for keyword in CONSUMABLE_KEYWORDS: + if keyword in normalized: + score -= 100 + + # Special case: power supply is spare part, but power cable is consumable + if ("POWER SUPPLY" in normalized or "PSU" in normalized): + for consumable_keyword in POWER_SUPPLY_CONSUMABLE_KEYWORDS: + if consumable_keyword in normalized: + return False + + # Final decision + return score >= 40 + + +def is_consumable(category: str) -> bool: + """ + Determine if an item is a consumable (inverse of classify_as_spare_part). + + Args: + category: Item category string + + Returns: + True if item is a consumable, False if a spare part. + """ + return not classify_as_spare_part(category) + + +def get_spare_part_type(category: str) -> Optional[str]: + """ + Return the normalized spare-part type for a given category, or None if not a spare part. + + Used for building web search queries with the specific part type. + + Args: + category: Item category string + + Returns: + Normalized spare-part type (e.g., "RAM", "SSD", "CPU") or None. + """ + if not classify_as_spare_part(category): + return None + + normalized = category.upper().strip() + + # Try to find best matching spare part type + best_match = None + best_score = 0 + + for spare_part in SPARE_PART_CATEGORIES: + fuzzy_score = fuzz.token_set_ratio(normalized, spare_part) + if fuzzy_score > best_score: + best_score = fuzzy_score + best_match = spare_part + + return best_match if best_match else None diff --git a/backend/requirements.txt b/backend/requirements.txt index d07f5240..e950e654 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -20,3 +20,6 @@ httpx>=0.27.0 opencv-python>=4.8.0 piexif>=1.1.3 python-magic>=0.4.27 +fuzzywuzzy==0.18.0 +beautifulsoup4>=4.12.0 +aiohttp>=3.9.0