categories.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. from __future__ import annotations
  2. import json
  3. from pathlib import Path
  4. from bom_assistant.session.models import ComponentCategory
  5. _CATEGORIES_PATH = Path(__file__).parent / "categories.json"
  6. # Explicit subcategory name lists for categories where the Nuxt flat list mixes
  7. # group headers (e.g. "Capacitors" id:495) with searchable leaves (id:1140+).
  8. # None = fall back to _TOP_LEVEL_CATS lookup (all subcategories from that section).
  9. _SUBCATEGORY_NAMES: dict[ComponentCategory, list[str] | None] = {
  10. ComponentCategory.capacitor: [
  11. "Aluminum - Polymer Capacitors",
  12. "Aluminum Electrolytic Capacitors",
  13. "Capacitor Networks, Arrays",
  14. "Ceramic Capacitors",
  15. "Electric Double Layer Capacitors (EDLC), Supercapacitors",
  16. "Film Capacitors",
  17. "Mica and PTFE Capacitors",
  18. "Motor Start, Motor Run Capacitors (AC)",
  19. "Niobium Oxide Capacitors",
  20. "Silicon Capacitors",
  21. "Tantalum - Polymer Capacitors",
  22. "Tantalum Capacitors",
  23. "Thin Film Capacitors",
  24. "Trimmers, Variable Capacitors",
  25. ],
  26. ComponentCategory.resistor: [
  27. "Chassis Mount Resistors",
  28. "Chip Resistor - Surface Mount",
  29. "Current Sense Resistors",
  30. "Resistor Networks, Arrays",
  31. "Specialized Resistors",
  32. "Through Hole Resistors",
  33. "Adjustable Power Resistor",
  34. "Rotary Potentiometers, Rheostats",
  35. "Slide Potentiometers",
  36. "Trimmer Potentiometers",
  37. ],
  38. ComponentCategory.inductor: [
  39. "Fixed Inductors",
  40. "Adjustable Inductors",
  41. "Arrays, Signal Transformers",
  42. "Wireless Charging Coils",
  43. "Ferrite Beads and Chips",
  44. "Common Mode Chokes",
  45. ],
  46. ComponentCategory.crystal: [
  47. "Crystals",
  48. "Oscillators",
  49. "Pin Configurable/Selectable Oscillators",
  50. "Programmable Oscillators",
  51. "Resonators",
  52. "VCOs (Voltage Controlled Oscillators)",
  53. ],
  54. ComponentCategory.diode: [
  55. "Diodes",
  56. "Bridge Rectifiers",
  57. "Rectifiers",
  58. "RF Diodes",
  59. "Variable Capacitance (Varicaps, Varactors)",
  60. "Zener",
  61. "Current Regulation - Diodes, Transistors",
  62. ],
  63. ComponentCategory.led: [
  64. "LED Addressable, Specialty",
  65. "LED Character and Numeric",
  66. "LED COBs, Engines, Modules, Strips",
  67. "LED Color Lighting",
  68. "LED Emitters - Infrared, UV, Visible",
  69. "LED Indication - Discrete",
  70. "LED White Lighting",
  71. "Circuit Board Indicators, Arrays, Light Bars, Bar Graphs",
  72. ],
  73. ComponentCategory.mosfet: [
  74. "Transistors",
  75. "FETs, MOSFETs",
  76. "IGBTs",
  77. "Bipolar (BJT)",
  78. "Power Driver Modules",
  79. "Special Purpose Transistors",
  80. ],
  81. # These have many subcategories — return all from the top-level section
  82. ComponentCategory.ic: None,
  83. ComponentCategory.connector: None,
  84. ComponentCategory.sensor: None,
  85. ComponentCategory.other: [],
  86. }
  87. # Top-level LCSC category names to scan when _SUBCATEGORY_NAMES entry is None
  88. _TOP_LEVEL_CATS: dict[ComponentCategory, list[str]] = {
  89. ComponentCategory.ic: ["Integrated Circuits (ICs)", "Isolators"],
  90. ComponentCategory.connector: ["Connectors, Interconnects"],
  91. ComponentCategory.sensor: ["Sensors, Transducers"],
  92. }
  93. def load_categories() -> list[dict]:
  94. if not _CATEGORIES_PATH.exists():
  95. raise FileNotFoundError(
  96. f"LCSC category file not found: {_CATEGORIES_PATH}\n"
  97. "Run: python -m bom_assistant.suppliers.lcsc.fetch_categories"
  98. )
  99. with open(_CATEGORIES_PATH, encoding="utf-8") as f:
  100. return json.load(f)
  101. def get_subcategories_for(internal_cat: ComponentCategory) -> list[dict]:
  102. """Return [{name, id}] dicts relevant to internal_cat for the LLM prompt."""
  103. names = _SUBCATEGORY_NAMES.get(internal_cat)
  104. if names is not None:
  105. if not names:
  106. return []
  107. name_set = set(names)
  108. cats = load_categories()
  109. result: list[dict] = []
  110. seen: set[int] = set()
  111. for cat in cats:
  112. for sub in cat.get("subcategories", []):
  113. if sub["name"] in name_set and sub["id"] not in seen:
  114. seen.add(sub["id"])
  115. result.append(sub)
  116. return result
  117. # Top-level category lookup
  118. target_tops = _TOP_LEVEL_CATS.get(internal_cat, [])
  119. if not target_tops:
  120. return []
  121. cats = load_categories()
  122. result = []
  123. seen = set()
  124. for cat in cats:
  125. if cat["category"] in target_tops:
  126. for sub in cat.get("subcategories", []):
  127. if sub["id"] not in seen:
  128. seen.add(sub["id"])
  129. result.append(sub)
  130. return result