feat(4.1-01): create spare-parts classification whitelist module with fuzzy matching
This commit is contained in:
163
backend/ai/spare_parts_whitelist.py
Normal file
163
backend/ai/spare_parts_whitelist.py
Normal file
@@ -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
|
||||||
@@ -20,3 +20,6 @@ httpx>=0.27.0
|
|||||||
opencv-python>=4.8.0
|
opencv-python>=4.8.0
|
||||||
piexif>=1.1.3
|
piexif>=1.1.3
|
||||||
python-magic>=0.4.27
|
python-magic>=0.4.27
|
||||||
|
fuzzywuzzy==0.18.0
|
||||||
|
beautifulsoup4>=4.12.0
|
||||||
|
aiohttp>=3.9.0
|
||||||
|
|||||||
Reference in New Issue
Block a user