scouting_loop.md 3.9 KB

Scouting Loop

The scouting loop drives each BomRow from pending to confirmed through a user-in-the-loop cycle: AI infers an LCSC subcategory → user confirms or overrides it → parametric search within that subcategory → user picks a part. Design source of truth: docs/architecture.md (§5 "Scouting Loop", "User Flow", "System Flow"). Build status source of truth: docs/backlog.md.

Current reality: only the first hop (pending → scouting, i.e. category inference) is implemented, via GET /session/{id}/row/{row_id}/scout. The rest of the loop (confirm-category, search, confirm, export) is designed but intentionally deferred until inference itself is trusted through interactive testing (see docs/backlog.md → API section, "deferred" notes).

State diagram

RowState per bom_assistant/session/models.py. Solid edges are built, dashed edges are designed but not yet implemented.

Source: diagrams/scouting_loop_state.puml

Note: today's orchestrator.scout_row always transitions to scouting, even when resolve() returns zero candidates (it does not auto-flag the row) — the empty-result case is left as scout_candidates: [] for the UI to show "no candidates found," rather than kicking the row to flagged. Auto-flagging on empty results, and a flag_reason field to distinguish that from normalizer-level flagging, was scoped out as part of the deferred loop work.

Sequence diagram

Actors: User, API (bom_assistant/api/routes.py), Orchestrator (bom_assistant/scouting/orchestrator.py), LcscCategoryResolver (bom_assistant/suppliers/lcsc/category_resolver.py), LcscAdapter (bom_assistant/suppliers/lcsc/lcsc.py). Dashed arrows = not yet built.

Source: diagrams/scouting_loop_sequence.puml

Walkthrough

  1. Upload (built) — user uploads a BOM file. POST /upload runs parse_bommap_columnsnormalize, returns a BomSession with all rows pending (or flagged for DNP/non-electronic rows).
  2. Scout (built) — for a pending row, GET .../scout calls LcscCategoryResolver.resolve(row) (file-cached, falls back to a Claude Haiku call) and returns up to 3 ranked LCSC subcategory candidates. Row moves to scouting. Can be re-triggered freely.
  3. Confirm category (deferred) — user accepts the top candidate or picks an alternative; row locks in resolved_category_id and moves to filtering.
  4. Facet query + Search (data layer built, not yet wired to an endpoint)LcscAdapter.query_facets(category_id, filters) calls LCSC's real facet API (wmsc.lcsc.com/ftps/wm/product/query/param/group) to get manufacturer/package/param filter groups scoped to the category and whatever's already selected; LcscAdapter.search(category_id, params, filters, page) calls the matching product-list endpoint. Both were reverse-engineered from live traffic and verified end-to-end against real category-resolver output — see docs/backlog.md. No GET .../facets or POST .../search route exists yet.
  5. Confirm pick (deferred) — user selects a specific part; row locks confirmed_pick and moves to confirmed.
  6. Retry (deferred) — if the user rejects all search results, the row can go back to scouting with adjusted parameters rather than being a dead end.
  7. Export (deferred) — once rows are confirmed (or explicitly skipped), GET .../export collates them into an order-ready BOM.

Steps 3–7 are why the API currently exposes only /upload and /scout: the loop is built incrementally, and the next slice only gets built once category inference (step 2) has been exercised enough through the HTMX test page to trust building on top of it.

See also

  • docs/architecture.md — full intended module design
  • docs/backlog.md — current Done/Todo status per component