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, filters: dict[str, Any] | None = None, 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):
lcsc.com/api/products/search scraper endpoint does not exist and was never actually verified working). No CSRF/session dance required, plain POST with User-Agent/Origin/Referer is sufficient:
POST https://wmsc.lcsc.com/ftps/wm/product/query/list — paginated product search. Request: {catalogIdList:[id], brandIdList, encapValueList, paramNameValueMap, isStock, ..., currentPage, pageSize}. Response items carry LCSC part number (productCode), manufacturer (brandNameEn), package (encapStandard), tiered pricing (productPriceList), stock, per-part spec list (paramVOList), datasheet URL.POST https://wmsc.lcsc.com/ftps/wm/product/query/param/group — facet/filter query, same request shape (minus pagination). Response: {Package: [...], Manufacturer: [...], Packaging: [...], paramNameValueMap: {<param name>: {paramDetailList, unitList, unitConversionMap}}}, scoped to the category and any filters already applied — this is the source for building filter chips each scouting round, no separate scraping or client-side aggregation needed.categories.json/category_resolver.py's subcategory ids (e.g. 1142 = "Ceramic Capacitors") are directly usable as catalogIdList values against both endpoints — no id remapping layer required.api_token provided, use official LCSC API (https://ips.lcsc.com/rest/wmsc2agent/) with HMAC-signed requests — not yet implemented (NotImplementedError).Drives each BomRow from pending to confirmed through an iterative user-guided process (bom_assistant/scouting/orchestrator.py).
pending
→ scouting (LLM infers LCSC subcategory, user confirms or picks different) — built
→ filtering (facet query + search within confirmed subcategory, user narrows/browses) — built (Package + Manufacturer filters only)
→ confirmed (user picks a specific part) — built
→ flagged (DNP / non-electronic / user skips) — built (ingestion-time only)
Not yet built: per-component-param filters (Capacitance/Tolerance/Voltage Rating/... — paramNameValueMap request shape unverified live), pagination past page 1, removing/toggling off an applied filter, and the retry path (user rejects all results → back to scouting with modified parameters).
FastAPI HTTP interface (bom_assistant/api/routes.py).
POST /upload — accept BOM file, run ingestion, return BomSession (built)GET /session/{id}/row/{row_id}/scout — trigger LLM category inference, sets row.scout_candidates (built)POST /session/{id}/row/{row_id}/confirm-category — user accepts/overrides category → state: filtering, populates facets + initial search results (built)POST /session/{id}/row/{row_id}/apply-filter — apply a Package/Manufacturer facet value, re-run facets + search (built)POST /session/{id}/row/{row_id}/confirm — lock part pick, advance state to confirmed (built)GET /session/{id} — current session state (not yet built)GET /session/{id}/export — collate confirmed picks → CSV/XLSX download (not yet built)React 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
→ [LcscCategory("Alum. Electrolytic...", 1140), ...] (best-first, up to 3)
row.scout_candidates = [{name, id}, ...]
row.state → scouting
POST /session/{id}/row/{row_id}/confirm-category {category_id: 1140, category_name: "..."}
row.resolved_category_id = 1140
row.resolved_category_name = "..."
row.applied_filters = {} ← reset; kept strictly as an LCSC filter payload fragment
row.state → filtering
_refresh_filtering(row):
row.facet_groups = lcsc.query_facets(1140, row.applied_filters)
result = lcsc.search(1140, row.normalized_params, row.applied_filters, page=1)
row.search_results = result.items
row.search_meta = {count, page, page_size, error}
POST /session/{id}/row/{row_id}/apply-filter {group: "Package"|"Manufacturer", value: ...}
row.applied_filters[<encapValueList|brandIdList>].append(value)
_refresh_filtering(row) ← same as above, narrows facets + results
POST /session/{id}/row/{row_id}/confirm {product_code: "C602037"}
row.confirmed_pick = <item from row.search_results matching product_code>
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 (live wmsc.lcsc.com API + token mode stub)
│ │
│ ├── scouting/
│ │ ├── ingest.py — bytes → BomSession (parser → column_mapper → normalizer)
│ │ ├── orchestrator.py — scout_row, confirm_category, apply_filter, confirm_pick
│ │ └── errors.py — SessionNotFoundError, RowNotFoundError, ScoutingValidationError
│ │
│ └── api/
│ ├── main.py — FastAPI app + exception handlers
│ ├── routes.py — HTTP routes (HTMX-rendered partials)
│ ├── store.py — in-memory SessionStore
│ └── templates/ — Jinja2 partials for the HTMX test page
│
├── frontend/ (not yet built — React SPA; HTMX test page above covers interim needs)
│ └── src/
│
├── examples/
│ ├── full/ — real BOM test files (CSV, XLSX, XLS)
│ └── reduced/ — same files, trimmed
│
└── requirements.txt