How VG captures vendor price quotes, turns them into pre-priced scope bundles, and uses them to estimate rehab costs before — and during — a deal.
Before this system, every rehab estimate started from memory. Quotes arrived via WhatsApp and were never stored. There was no "factory default" budget for a standard scope — no reliable way to say "a house like this should cost approximately X."
The Assembly & Estimating System fixes that. It captures vendor price quotes from WhatsApp (and manual entry), organizes them into named scope bundles called assemblies, and uses those bundles to automatically populate property budgets and produce estimate reports.
buyer_initial pricesAn assembly is a named, pre-priced scope bundle for one type of construction work. Each assembly maps to exactly one VG Segment and Work Order. Examples: Standard AV & Security Package (Seg 65 / P2W05), Rough Plumbing (Seg 61 / P2W02).
Assemblies accumulate vendor quotes over time — they're not single quotes, they're benchmarks. When a new quote comes in, it extends the price range if it falls outside it; the range never narrows.
Every assembly (and its Work Order) is classified as either Standard or Optional.
Auto-included in every property estimate. Covers scopes that appear in every VG rehab — plumbing, electrical, HVAC, drywall, framing, etc. You don't need to select these per property.
Must be explicitly selected for each property via selected_assemblies: in properties.yaml. Covers scopes that don't occur on every property — security cameras, exterior masonry, specialty trim, EV/solar, pool, etc.
Each assembly stores a list of all vendor quotes that backed its price range. Quotes carry the vendor name, amount, date noted, document type (quote / PO / receipt / SOW), and source (PM verbal, WA ingest, etc.). The assembly's price range is VG's benchmark — not just the most recent quote.
Assembly-derived budgets are written to budgets.yaml with source: buyer_initial — a mid-range benchmark intended for early deal underwriting. The source system has a maturity order: admin_placeholder → buyer_initial → buyer_topdown → detailed_estimate → committed. Assembly prices never overwrite anything at detailed_estimate or higher. They fill gaps only.
Two paths feed assemblies.yaml: (A) WhatsApp ingest with --extract-quotes, and (B) manual entry. Once in assemblies.yaml, one generator populates budgets; two generators produce HTML outputs.
| File | Role | Where |
|---|---|---|
assemblies.yaml |
Canonical scope + price library. One entry per named scope variant. Never delete entries — update and extend. | reference/cost-codes/ |
quotes-pending.yaml |
Ingest staging area. LLM writes here; you review with approve_quotes.py. Approved entries promote to assemblies.yaml. |
reference/cost-codes/ |
work-orders.yaml |
All 54 WOs with optional: flag added. Standard WOs auto-include in every estimate; optional WOs require per-property selection. |
reference/cost-codes/ |
properties.yaml |
Each active property has a selected_assemblies: [] list for optional scopes selected for that deal. |
workspaces/actualsboard/config/ |
budgets.yaml |
Per-property, per-segment budget amounts. Assembly prices enter here with source: buyer_initial. |
workspaces/actualsboard/config/ |
Run this after any WhatsApp ingest with --extract-quotes. It walks through every pending quote in quotes-pending.yaml and asks you to approve, reject, edit, or skip each one.
| Command | What it does |
|---|---|
a — Approve | Appends to assemblies.yaml vendor_quotes[]; extends price range if needed; marks status: approved |
r — Reject | Marks status: rejected; stays in file for audit; never touches assemblies.yaml |
e security_av_standard — Edit | Changes the assembly_guess field before approving — use when the LLM guessed wrong |
s — Skip | Leaves as pending; will appear again next time you run |
q — Quit | Saves progress so far and exits cleanly |
Reads assemblies.yaml + work-orders.yaml + properties.yaml → writes buyer_initial midpoint prices to budgets.yaml. Standard WOs are included for every property; optional WOs are included only if selected in that property's selected_assemblies: list.
The script skips any segment that already has source: buyer_topdown, detailed_estimate, or committed. Assembly prices only fill gaps — they don't overwrite real numbers. Always run with --dry-run first to review what would change.
Reads assemblies.yaml and produces the Scope Library portal page, showing all assemblies grouped by segment with Standard/Optional badges, price ranges, vendor quote counts, and scope notes.
Run this any time you add or update an assembly. The portal sync daemon picks up the change automatically.
Reads assemblies + properties + budgets and produces a deal-level HTML cost summary for each active property. Standard WOs appear with their budgeted amounts; optional selected scopes are marked with ★.
A [skip] message means no assembly-derived budget entries exist for that property yet. Run generate_budgets.py first, or manually add budget entries to budgets.yaml.
Add --extract-quotes to any WhatsApp ingest run to activate Stage 8. The pipeline scans new messages for price signals (dollar amounts, scope words, or attached documents) and calls the local LLM to extract structured quote data. Results land in quotes-pending.yaml for your review.
Attached PDFs, purchase orders, receipts, and SOWs are the authoritative source — the LLM reads document text first and treats the chat message as secondary context. If a quote comes with an attached document, the document content drives the extraction. Higher accuracy than text-only messages.
When you receive a quote outside WhatsApp (email, phone, on-site), add it directly to assemblies.yaml. Open the file and append to the vendor_quotes: list of the relevant assembly:
Then regenerate the Scope Library page so the updated quote count shows on the portal:
When a property will carry an optional scope (e.g., security cameras), add the assembly key to that property's selected_assemblies: list in workspaces/actualsboard/config/properties.yaml:
Then run generate_budgets.py to write the assembly price to budgets.yaml:
When you encounter a new scope type that doesn't exist in assemblies.yaml yet, add it as a new entry. One assembly per named scope variant.
reference/cost-codes/work-orders.yaml for the WO that covers this scope. The assembly's segment: must match that WO's cost codes' leading digits (e.g., WO with cost_codes: [65.0] → segment: "65").
fireplace_gas_insert). Fill in all required fields: name, segment, segment_name, work_order, optional, scope, price_low, price_high, unit, vendor_quotes, last_updated.
optional: correctly. If this scope doesn't occur on every property, set optional: true. If it's part of every VG rehab, set optional: false — it will auto-include in every estimate.
python tools/generate_scope_library.py so the new assembly appears on the portal.
python tools/generate_budgets.py --dry-run to see what gets written, then run without --dry-run to apply.
--extract-quotes after receiving new chat exports.python tools/approve_quotes.py — review each extracted quote. Approve good ones; reject noise.python tools/generate_scope_library.py to update the Scope Library page with the new quote count.selected_assemblies: and run generate_budgets.py --property "...".properties.yaml with status: active-construction and selected_assemblies: [].selected_assemblies:.python tools/generate_budgets.py --property "Property Name" --dry-run to preview. Then run without --dry-run.python tools/generate_estimate.py --property "Property Name" to generate the deal estimate HTML.reference/cost-codes/assemblies.yaml. Find the matching assembly or create a new one.vendor_quotes:. Update price_low/price_high if the new quote extends the range.python tools/generate_scope_library.py to update the portal.The Scope Library page shows all assemblies grouped by Segment number. Each card shows:
The range reflects all vendor quotes on file, from the lowest to the highest. It's not a single quote — it's the observed market range for this scope at VG's typical property size. More quotes = tighter, more reliable range. The midpoint is used when writing buyer_initial budgets.
Regenerate the page whenever assemblies change: python tools/generate_scope_library.py. The sync daemon pushes the change to the portal within seconds.
| Task | Command |
|---|---|
| Review pending quotes from WA ingest | python tools/approve_quotes.py |
| Populate budgets from assemblies (preview) | python tools/generate_budgets.py --dry-run |
| Populate budgets from assemblies (write) | python tools/generate_budgets.py |
| Populate budgets for one property | python tools/generate_budgets.py --property "185 Fernwood" |
| Regenerate Scope Library portal page | python tools/generate_scope_library.py |
| Generate estimate reports (all active properties) | python tools/generate_estimate.py |
| Generate estimate for one property | python tools/generate_estimate.py --property "185 Fernwood" |
| Ingest WhatsApp + extract quotes | python run.py --adapter vg_iha --zip <path> --workspace <vg> --extract-quotes |
| Validate budgets.yaml for errors | python tools/validate_budgets.py |