Shared data schemas used across all other modules.
ComponentCategory enum: capacitor, resistor, inductor, diode, led, ic, connector, crystal, mosfet, sensor, otherRowState enum: pending → scouting → filtering → confirmed | flaggedNormalizedParams: package, value (SI float), value_str, unit, voltage_rating, current_rating, power_rating, tolerance, part_number, manufacturerBomRow: row_id, designators, quantity, raw_value, footprint, category, normalized_params, state, applied_filters, facet_groups, confirmed_pickBomSession: session_id, created_at, filename, preferred_suppliers, rowsConverts raw file bytes into a list of BomRow objects.
Parser — detects format by extension, handles:
, vs ;), footer stripping, blank row skippingColumn Mapper — maps raw column names to canonical fields:
column_synonyms.json (exact normalized match)Comment and a richer value column, Comment → part_number (unless values look like passive component values)value field is still unmappedcolumn_synonyms.json to avoid future API calls_raw_ prefixNormalizer — per-row processing:
~ rowsC1-5 → [C1, C2, C3, C4, C5])BomRowDetermines component category and extracts normalized parameters.
Classifier — (designators, raw_value) → ComponentCategory:
C→capacitor, R/RV→resistor, L→inductor, D→diode, U/IC→ic, J/P/CON/USB→connector, Q/T→mosfet, X/Y→crystal, NTC/RT→sensorParam Extractor — (raw_value, footprint, category) → NormalizedParams:
"CAP CER 0.1UF 100V X7R 0603")known_footprints dictpackage_patterns.jsonknown_footprintsAdapter layer for electronic component marketplaces. All adapters implement the abstract SupplierAdapter base class, which supports both scraper mode (default, no credentials) and token mode (official API).
class SupplierAdapter(ABC):
def __init__(self, api_token: str | None = None): ...
def search(self, category_id: int, params: NormalizedParams, page: int = 1) -> SearchResult: ...
Before searching, each BomRow must be mapped to a specific LCSC subcategory (name + integer ID). Generic keyword search is unreliable — LCSC expects category-scoped queries.
One-time setup (lcsc/fetch_categories.py):
lcsc/categories.json — committed to repo, refreshed quarterlyCategory JSON format:
[
{
"group": "Passives",
"category": "Capacitors",
"category_id": 11,
"subcategories": [
{"name": "Aluminum Electrolytic Capacitors", "id": 1140},
{"name": "Multilayer Ceramic Capacitors (MLCC)", "id": 1142},
{"name": "Film Capacitors", "id": 1144},
{"name": "Tantalum Capacitors", "id": 1150}
]
}
]
LLM Category Resolver (lcsc/category_resolver.py):
f"{internal_category}|{package}|{value_str}" → check lcsc/category_cache.jsoncategory_cache.json (same file-backed learning pattern as package_patterns.json)Example: category=capacitor, package=6.3x5.8, voltage=25V → LLM picks "Aluminum Electrolytic Capacitors" → looked up → id 1140
LCSC Search (lcsc/lcsc.py):
https://lcsc.com/api/products/search with catalog_id=<subcategory_id> + parameter filtersapi_token provided, use official LCSC API (https://ips.lcsc.com/rest/wmsc2agent/) with HMAC-signed requestsDrives each BomRow from pending to confirmed through an iterative user-guided process.
pending
→ scouting (LLM infers LCSC subcategory, user confirms or picks different)
→ filtering (search within confirmed subcategory + param filters, user browses results)
→ confirmed (user picks a specific part)
→ flagged (DNP / non-electronic / user skips)
Retry path: user rejects all results → back to scouting with modified parameters.
FastAPI HTTP interface.
POST /upload — accept BOM file, run ingestion, return BomSessionGET /session/{id} — current session stateGET /session/{id}/row/{row_id}/scout — trigger LLM category inference, return {category, alternatives[]}POST /session/{id}/row/{row_id}/confirm-category — user accepts/overrides category → state: filteringPOST /session/{id}/row/{row_id}/search — search within confirmed category + params, return page 1POST /session/{id}/row/{row_id}/confirm — lock part pick, advance state to confirmedGET /session/{id}/export — collate confirmed picks → CSV/XLSX downloadReact SPA.
POST /uploadUser uploads BOM file
└─ Ingestion pipeline runs
├─ Parser reads file → raw rows
├─ Column Mapper normalizes column names
└─ Normalizer classifies + extracts params → BomSession
For each pending BomRow:
└─ Scout
├─ LLM infers LCSC subcategory from component info
│ (cache hit → instant; miss → Claude Haiku call → cached)
├─ UI shows inferred subcategory + dropdown of all alternatives
├─ User confirms or picks a different subcategory
├─ Search within confirmed subcategory + param filters
├─ User browses results, adjusts filters if needed
└─ User confirms pick ──→ row state: confirmed
└─ rejects ──→ retry with different subcategory/filters
All rows confirmed
└─ Export → order-ready BOM (CSV/XLSX)
POST /upload
parse_bom(bytes, filename)
→ map_columns(raw_rows)
→ normalize(mapped_rows)
→ BomSession stored in memory
GET /session/{id}/row/{row_id}/scout
lcsc_category_resolver.resolve(row) ← cache hit or Haiku call
→ {subcategory: "Alum. Electrolytic...", id: 1140, alternatives: [...]}
row.state → scouting
POST /session/{id}/row/{row_id}/confirm-category {subcategory_id: 1140}
row.applied_filters["lcsc_category_id"] = 1140
row.state → filtering
POST /session/{id}/row/{row_id}/search {filters: {voltage_min: 25, ...}, page: 1}
lcsc.search(category_id=1140, params=row.normalized_params, filters=..., page=1)
→ {count: N, results: [...]}
POST /session/{id}/row/{row_id}/confirm {pick: {...}}
row.confirmed_pick = pick
row.state → confirmed
GET /session/{id}/export
[confirmed rows] → assemble BOM → return file
instant-bom/
├── bom_assistant/
│ ├── session/
│ │ └── models.py — Pydantic schemas
│ │
│ ├── ingestion/
│ │ ├── parser.py — raw bytes → list[dict[str, str]]
│ │ ├── column_mapper.py — raw keys → canonical keys
│ │ ├── column_synonyms.json — file-backed synonym table (auto-updated by AI)
│ │ └── normalizer.py — mapped rows → list[BomRow]
│ │
│ ├── classification/
│ │ ├── classifier.py — designator prefix + value heuristics → ComponentCategory
│ │ ├── param_extractor.py — value string + footprint → NormalizedParams
│ │ └── package_patterns.json — file-backed regex + known_footprints (auto-updated by AI)
│ │
│ ├── suppliers/
│ │ ├── base.py — abstract SupplierAdapter
│ │ └── lcsc/
│ │ ├── fetch_categories.py — one-time CLI: scrape LCSC → categories.json
│ │ ├── categories.json — committed static file (refresh quarterly)
│ │ ├── categories.py — load + filter category tree
│ │ ├── category_resolver.py — LLM category inference + file-backed cache
│ │ ├── category_cache.json — auto-updated by resolver
│ │ └── lcsc.py — search adapter (scraper + token mode)
│ │
│ └── api/ (not yet built)
│ └── main.py — FastAPI app + routes
│
├── frontend/ (not yet built)
│ └── src/
│
├── examples/
│ └── bom/ — 12 real BOM test files (CSV, XLSX, XLS)
│
└── requirements.txt