> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vinmake.com/llms.txt
> Use this file to discover all available pages before exploring further.

# RFQ to Quote Lifecycle

> The full RFQ → quotation lifecycle as diagrams — what AI does, where humans approve, and why we build agents instead of wrapping a chatbot

# RFQ to Quote Lifecycle

This page documents the full client-RFQ to quote-sent lifecycle as a series of paired flowcharts + sequence diagrams. Each stage of the workflow has both a structural view (decisions, branches, data stores) and an interaction view (HTTP calls, table writes, LLM invocations).

The source of truth for these diagrams is [`vinmaketeam/brain-contracts/LIFECYCLE.mmd`](https://github.com/vinmaketeam/brain-contracts/blob/main/LIFECYCLE.mmd). This page renders them.

<Note>
  **Color legend.** Across every diagram: blue = AI step, yellow = human action, amber = human approval gate, green = data store, gray = external system, purple = input artifact.
</Note>

## Why we build an agent, not a thin wrapper

A pure model (ChatGPT.com, Claude.ai) applied to VinMake's work is like sending a baby to do a merchandiser's job. Smart on a single turn, useless across a hundred. We need professionals — consistent, dependable, accurate, high-agency.

The four traits a merchandiser must have, and a thin wrapper never does:

| Trait           | What it means                                                                                                                                 | Why it breaks with a thin wrapper                                                                                                                                   |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Consistent**  | Same input pattern → same output pattern, every time                                                                                          | Pure models vary turn to turn. Monday's BOM and Friday's BOM don't match for the same tech pack.                                                                    |
| **Dependable**  | Always reaches the workflow's end OR fails into a known, recoverable error                                                                    | Thin wrappers hand back half-finished answers that cost an hour to debug.                                                                                           |
| **Accurate**    | Per-line confidence with provenance                                                                                                           | Mai needs to know which page of which tech pack produced "shell: 100% cotton 280gsm" so she can correct it in 30 seconds. Wrappers give prose, not auditable facts. |
| **High agency** | Decides when to ask, when to act, when to wait. Picks the right tool, threads provenance, retries failures, defers blocking actions to humans | Wrappers wait to be told.                                                                                                                                           |

The agent route costs more engineering up front. What we get back is a system that grows from baby-grade to professional-grade over weeks — and stays there because the harness, contracts, and eval loop keep it there.

### Baby → Professional

```mermaid theme={null}
flowchart LR
    classDef baby fill:#fee2e2,stroke:#991b1b,color:#7f1d1d
    classDef pro fill:#dcfce7,stroke:#166534,color:#14532d
    classDef bridge fill:#e0e7ff,stroke:#3730a3,color:#312e81

    Baby["BABY — thin wrapper<br/>(ChatGPT / Claude.ai)<br/><br/>• different output each turn<br/>• forgets prior context<br/>• plausible-sounding prose<br/>• waits to be told what next<br/>• no provenance, no audit<br/>• hallucinates confidently<br/>• one prompt at a time"]:::baby

    Bridge["What gets us from baby to pro<br/><br/>1. Structured contracts (Pydantic)<br/>2. State + idempotency per step<br/>3. Tool calls, not just prose<br/>4. Provenance threaded through<br/>5. Eval harness + correction log<br/>6. Human-in-the-loop gates"]:::bridge

    Pro["PROFESSIONAL — agent + harness<br/>(Brain + BOM + KG)<br/><br/>• consistent across turns<br/>• state + memory in the loop<br/>• per-line confidence + sources<br/>• picks tool + asks when stuck<br/>• provenance on every artifact<br/>• reconfirms when guessing<br/>• runs the whole workflow"]:::pro

    Baby -- "where VinMake is today<br/>with ad-hoc Claude / ChatGPT use" --> Bridge
    Bridge -- "what Phase 1 ships:<br/>three services + contracts" --> Pro
```

### LangGraph workflows, not just .md skills

Inside the Baby → Pro bridge, "structured contracts" and "state per step" pull a specific weight: about **70% of Brain framework's work lives in explicit LangGraph + Pydantic AI workflows**, not in `.md` skill files inside a system prompt.

Skill files are fine for single-prompt deterministic operations — summarize a style, find duplicates, classify an email. But they break consistency the moment a workflow needs state, retries, idempotency, typed handoffs between steps, or human approval gates in the middle.

A BOM generation expressed as *"you are a BOM extraction assistant…"* in a system prompt produces a different shape every run. The same workflow expressed as LangGraph nodes with Pydantic outputs produces the same shape every run — and the eval harness can grade it.

**Rule of thumb.** If the work needs to look the same on Monday and Friday, it goes in a LangGraph node. If it's a one-shot transformation with no downstream consumers, a `.md` skill is fine.

| Type                        | Location               | Share     | When to use                                                                | Examples                                            |
| --------------------------- | ---------------------- | --------- | -------------------------------------------------------------------------- | --------------------------------------------------- |
| `.md` skill                 | Inside main agent      | \~15%     | Single-prompt, deterministic, no state                                     | `/summarize-style`, `/find-duplicates`              |
| **LangGraph + Pydantic AI** | **Inside main agent**  | **\~70%** | **Long-running, consistency-critical, typed I/O, mid-flow approval gates** | `/generate-bom`, `/draft-true-cost`, `/draft-quote` |
| LangGraph + Pydantic AI     | Outside (microservice) | \~10%     | Own lifecycle, scaling, or ownership                                       | BOM agent, KG service                               |
| `.md` multi-agent           | Outside                | \~5%      | LLM-as-a-judge / adversarial; must be uncorrelated with what it judges     | Eval grader for BOM output                          |

```mermaid theme={null}
flowchart TB
    classDef md fill:#fef3c7,stroke:#b45309,color:#7c2d12
    classDef lg fill:#dbeafe,stroke:#1e40af,color:#1e3a8a
    classDef title fill:#e0e7ff,stroke:#3730a3,color:#312e81

    Title["IMPLEMENTATION POV<br/>where each skill / workflow lives"]:::title

    subgraph Inside ["INSIDE main agent (Brain framework)"]
        direction TB
        InsideMD["~15% — .md skill file<br/><br/>simple deterministic prompts<br/>e.g. /summarize-style,<br/>/find-duplicates<br/><br/>cheap + fast<br/>but no state, no retries,<br/>output shape drifts run-to-run"]:::md
        InsideLG["~70% — LangGraph + Pydantic AI<br/><br/>explicit workflow with state<br/>e.g. /generate-bom,<br/>/draft-true-cost, /draft-quote<br/><br/>USE WHEN<br/>• long-running task<br/>• consistency across prompts<br/>• typed inputs and outputs<br/>• human approval gates mid-flow"]:::lg
    end

    subgraph Outside ["OUTSIDE main agent"]
        direction TB
        OutsideMD["~5% — multi-agent .md<br/><br/>LLM-as-a-judge / adversarial<br/>e.g. eval grader scoring<br/>BOM agent output<br/><br/>must be uncorrelated with<br/>the agent being judged"]:::md
        OutsideLG["~10% — microservice<br/><br/>multi-repo, separate host<br/>e.g. BOM agent (Ku Kue),<br/>KG service (Andy)<br/><br/>when service has its own<br/>lifecycle, scaling, ownership"]:::lg
    end

    Title --> Inside
    Title --> Outside
```

### Measuring consistency

You cannot ship a professional you cannot measure. Two axes split the measurement problem cleanly: **debug** (what just happened?) vs **judge** (was it any good?). Both have a NOW tool and a LATER tool — pick the cheap one until prod traffic justifies the spend.

```mermaid theme={null}
flowchart TB
    classDef now fill:#dbeafe,stroke:#1e40af,color:#1e3a8a
    classDef later fill:#f3f4f6,stroke:#4b5563,color:#1f2937
    classDef header fill:#fef3c7,stroke:#b45309,color:#7c2d12
    classDef techniques fill:#fce7f3,stroke:#9d174d,color:#831843

    Title["MEASURING CONSISTENCY"]:::header
    HDebug["DEBUG — observability<br/>what just happened?"]:::header
    HJudge["JUDGE — evaluation<br/>was it any good?"]:::header

    Title --> HDebug
    Title --> HJudge

    NowDebug["NOW — LangSmith<br/>full trace per workflow_run<br/>X-Session-Id propagates<br/>across Brain / BOM / KG<br/>(zero cost, already wired<br/>in deer-flow)"]:::now
    LaterDebug["LATER — Arize Phoenix<br/>self-host once data-<br/>residency clause lands<br/>or merchandiser #3 onboards<br/>(months out, not weeks)"]:::later

    NowJudge["NOW — code-defined evals<br/>JSONL ground-truth files +<br/>Python runner per service<br/>(BOM: 5 tech packs<br/>KG: 20 emails)"]:::now
    LaterJudge["LATER — Braintrust<br/>continuous eval at prod<br/>regression catching +<br/>shipped quality dashboard<br/>(month 2-3, when traffic<br/>justifies)"]:::later

    HDebug --> NowDebug
    HDebug --> LaterDebug
    HJudge --> NowJudge
    HJudge --> LaterJudge

    Techniques["EVAL TECHNIQUES — be creative<br/><br/>1. Predefined eval set<br/>   (BOM v0: 5 ground-truth tech packs,<br/>   1 Jaspal + 2 Lulus + 2 ASOS)<br/><br/>2. Passively collect human corrections<br/>   (Brain correction log in Supabase →<br/>   yesterday's edits become tomorrow's eval)<br/><br/>3. Adversarial judgement<br/>   (LLM-as-a-judge, second model grades the first)<br/><br/>4. AI proposes hard cases + human curates<br/>   (Claude generates edge-case scenarios,<br/>   Mai / Zean approve into the eval set)"]:::techniques

    NowJudge -.- Techniques
    LaterJudge -.- Techniques
```

## Lifecycle overview

Six stages, three services, two humans, one client. The sequence diagram is the narrative view; the tree is the branching view.

### Sequence

```mermaid theme={null}
sequenceDiagram
    autonumber
    actor C as Client
    participant G as Gmail
    participant KG as "KG service (Andy)"
    participant B as "Brain framework (Thai)"
    participant BOM as "BOM agent (Ku Kue)"
    participant CM as CutMake DB
    actor M as Mai
    actor Z as Zean

    Note over C,Z: STAGE 1 — Email intake
    C->>G: RFQ email + tech pack +<br/>POM + measurement
    KG->>G: poll history cursor<br/>every 30s, lag ≤ 90s
    KG->>KG: dedupe + OCR + chunk + embed<br/>build provenance_ids
    KG-->>B: GET /v0/emails (since cursor)
    B->>B: classify as RFQ<br/>extract client + style_ref
    B->>M: Inbox card + Task
    M->>B: confirm RFQ

    Note over C,Z: STAGE 2 — Style record
    B->>KG: fetch email body + attachments
    B->>B: regex + LLM extract fields
    B->>CM: SELECT clients WHERE domain=...
    B->>M: Style record draft
    M->>B: edits + approve
    B->>CM: INSERT styles row

    Note over C,Z: STAGE 3 — BOM
    B->>BOM: POST /v0/generate-bom<br/>style_id, urls, request_id
    BOM->>BOM: pdfplumber + pdf2image<br/>vision per page, cost-metered<br/>category template fill
    BOM-->>B: BomGenerationResponse<br/>lines + confidence + provenance
    B->>M: render BOM, flag LOW conf
    loop reconfirm low-confidence lines
        B->>M: AI is guessing — is X right?
        M->>B: confirm or correct
    end
    M->>B: approve full BOM
    B->>CM: INSERT bom_lines rows

    Note over C,Z: STAGE 4 — True cost
    B->>CM: read materials, fx_rates, moq
    B->>B: per-line cost + overheads<br/>anomaly check vs history
    B->>Z: cost breakdown + flags
    Z->>B: adjustments + approve
    B->>CM: INSERT true_costs row

    Note over C,Z: STAGE 5 — Quotation
    B->>CM: read client markup rules
    B->>B: apply markup<br/>render quotation PDF<br/>diff vs past quotes
    B->>Z: quotation + delta from history
    Z->>B: edits + approve
    B->>CM: INSERT quotations row

    Note over C,Z: STAGE 6 — Reply email
    B->>KG: POST /v0/retrieve<br/>scope: thread + style
    B->>B: detect tone<br/>draft reply<br/>attach quotation PDF
    B->>M: email draft + diff vs past replies
    M->>B: edits + approve
    B->>G: send via Gmail API<br/>on original thread
    G->>C: deliver
    KG->>G: re-index outbound message
    Note over C,Z: Lifecycle complete — awaiting client response
```

### Tree

```mermaid theme={null}
flowchart TD
    classDef ai fill:#dbeafe,stroke:#1e40af,color:#1e3a8a
    classDef human fill:#fef3c7,stroke:#b45309,color:#7c2d12
    classDef gate fill:#fde68a,stroke:#92400e,color:#7c2d12
    classDef store fill:#d1fae5,stroke:#047857,color:#064e3b
    classDef ext fill:#f3f4f6,stroke:#4b5563,color:#1f2937

    Client((Client)):::ext
    Gmail[Gmail mailbox]:::ext
    Client -- "RFQ + tech pack +<br/>POM + measurement" --> Gmail

    Gmail --> S1

    subgraph S1 ["Stage 1 — Email intake"]
        direction TB
        I1[AI KG: pull + dedupe + index<br/>build provenance IDs]:::ai
        I2[AI Brain: classify RFQ<br/>extract client + style_ref]:::ai
        I3[AI Brain: Inbox card + Task]:::ai
        I1 --> I2 --> I3
    end

    S1 --> G1{Mai: is this an RFQ?}:::gate
    G1 -- "file only" --> FileOnly[AI Brain: file<br/>no action]:::ai
    G1 -- "spam" --> Drop[AI Brain: drop]:::ai
    G1 -- "yes" --> S2

    subgraph S2 ["Stage 2 — Style record"]
        direction TB
        T1[AI Brain: regex + LLM<br/>extract fields]:::ai
        T2[AI Brain: lookup client<br/>+ draft Style]:::ai
        T1 --> T2
    end

    S2 --> G2{Mai: approve style?}:::gate
    G2 -- "edits" --> S2
    G2 -- "approve" --> S3

    subgraph S3 ["Stage 3 — BOM generation"]
        direction TB
        B1[AI BOM: parse PDF + images]:::ai
        B2[AI BOM: vision —<br/>visible BOM]:::ai
        B3[AI BOM: template —<br/>invisible BOM]:::ai
        B4[AI BOM: aggregate +<br/>per-line confidence]:::ai
        B1 --> B2 --> B4
        B1 --> B3 --> B4
    end

    S3 --> G3{"Mai: approve BOM?<br/>reconfirm LOW conf lines"}:::gate
    G3 -- "edits / reconfirms" --> S3
    G3 -- "approve" --> S4

    subgraph S4 ["Stage 4 — True cost"]
        direction TB
        C1[AI Brain: match supplier<br/>prices per line]:::ai
        C2[AI Brain: per-garment cost<br/>+ labor + freight + duty]:::ai
        C1 --> C2
    end

    S4 --> G4{Zean: approve cost?}:::gate
    G4 -- "adjust" --> S4
    G4 -- "approve" --> S5

    subgraph S5 ["Stage 5 — Quotation"]
        direction TB
        Q1[AI Brain: apply client<br/>markup rule]:::ai
        Q2[AI Brain: render PDF +<br/>sanity check vs history]:::ai
        Q1 --> Q2
    end

    S5 --> G5{Zean: approve quote?}:::gate
    G5 -- "adjust" --> S5
    G5 -- "approve" --> S6

    subgraph S6 ["Stage 6 — Reply email"]
        direction TB
        E1[AI KG: pull thread context]:::ai
        E2[AI Brain: draft reply +<br/>attach quote PDF]:::ai
        E1 --> E2
    end

    S6 --> G6{Mai: approve + send?}:::gate
    G6 -- "edits" --> S6
    G6 -- "send" --> Sent[AI Brain: send via<br/>Gmail API]:::ai
    Sent --> ClientEnd((Client)):::ext
```

## Stage 1 — Email intake

KG service polls Gmail every 30s, dedupes by `gmail_message_id`, downloads attachments to a Supabase bucket, runs OCR/embedding/entity-extraction. Brain framework pulls indexed emails, classifies (heuristic → LLM fallback), posts an Inbox card. Mai's gate decides Task vs File vs Spam.

<Note>
  **Task vs File.** A *Task* is a blocking action that writes to CutMake (e.g. create Style row). A *File* is an indexed-but-no-action archive — still retrievable, no human gate.
</Note>

### Flowchart

```mermaid theme={null}
flowchart LR
    classDef ai fill:#dbeafe,stroke:#1e40af
    classDef human fill:#fef3c7,stroke:#b45309
    classDef gate fill:#fde68a,stroke:#92400e
    classDef store fill:#d1fae5,stroke:#047857
    classDef ext fill:#f3f4f6,stroke:#4b5563

    Mai[Mai]:::human
    Zean[Zean]:::human
    MB["mai@vinmake.com"]:::ext
    ZB["zean@vinmake.com"]:::ext

    Mai --> MB
    Zean --> ZB

    MB --> Poll
    ZB --> Poll

    Poll["AI KG: Gmail poll<br/>users.history.list<br/>cursor=last_historyId<br/>every 30s"]:::ai
    Poll --> Fetch["AI KG: fetch full message<br/>users.messages.get<br/>format=full"]:::ai
    Fetch --> Dedup["AI KG: dedupe by<br/>gmail_message_id<br/>SELECT messages_indexed"]:::ai
    Dedup --> Skip{Already<br/>indexed?}:::gate
    Skip -- "yes" --> Done1[Skip, advance cursor]:::ai
    Skip -- "no" --> Attach

    Attach["AI KG: extract attachments<br/>download base64 → bucket<br/>sha256 each blob"]:::ai
    Attach --> Buck[(Supabase Storage<br/>bucket: rfq_attachments)]:::store
    Attach --> ProvIDs["AI KG: assign provenance_ids<br/>email:gmail-msg-{id}<br/>pdf:sha256-{hex}<br/>xlsx:sha256-{hex}<br/>image:sha256-{hex}"]:::ai
    ProvIDs --> Artifacts[(artifacts table<br/>provenance_id PK)]:::store

    Attach --> Text["AI KG: extract text<br/>pdfplumber → PDF text<br/>openpyxl → xlsx<br/>tesseract → image OCR"]:::ai
    Text --> Chunk["AI KG: chunk text<br/>≈512 token windows<br/>64 token overlap<br/>preserve page boundaries"]:::ai
    Chunk --> Embed["AI KG: embed<br/>text-embedding-3-large<br/>3072 dim"]:::ai
    Embed --> VStore[(pgvector chunks table<br/>ivfflat index, cos sim)]:::store

    Text --> Ents["AI KG: entity extract<br/>LLM tool-use:<br/>Person, Client, Style,<br/>Thread, Attachment"]:::ai
    Ents --> KGStore[(KG nodes + edges:<br/>Email-FROM-Person<br/>Email-ABOUT-Style<br/>Style-FOR-Client)]:::store

    ProvIDs -.-> MarkIndexed
    VStore -.-> MarkIndexed
    KGStore -.-> MarkIndexed
    MarkIndexed["AI KG: mark indexed<br/>INSERT messages_indexed<br/>UPDATE ingest cursor"]:::ai
    MarkIndexed --> Events[(ingestion_events queue<br/>indexed event)]:::store

    Events --> BPoll
    BPoll["AI Brain: subscribe to<br/>GET /v0/emails<br/>since=last_seen"]:::ai
    BPoll --> Heur["AI Brain: heuristic classify<br/>sender domain in clients?<br/>subject ∈ rfq|quote|inquiry?<br/>tech pack attached?<br/>has style_ref pattern?"]:::ai
    Heur --> Conf{heuristic<br/>confidence ≥ 0.8?}:::gate
    Conf -- "no" --> LLMClass["AI Brain: LLM classify<br/>Claude Sonnet, tool-use<br/>output: rfq|followup|<br/>internal|spam"]:::ai
    Conf -- "yes" --> Extract
    LLMClass --> Extract

    Extract["AI Brain: extract fields<br/>client_id, style_ref,<br/>requested_qty, due_date,<br/>incoterms hints"]:::ai
    Extract --> Lookup[(SELECT clients<br/>WHERE domain or fuzzy_name)]:::store
    Lookup --> Card["AI Brain: post Inbox card<br/>structured summary +<br/>open Task affordance +<br/>provenance link to email"]:::ai

    Card --> Gate{Mai: classification<br/>correct?}:::gate
    Gate -- "yes — RFQ" --> Task["Create Task workflow_run<br/>idempotent on gmail_message_id<br/>blocking: writes Style row"]:::ai
    Gate -- "no — file only" --> File["AI Brain: file<br/>indexed only, no workflow"]:::ai
    Gate -- "no — spam" --> Drop["AI Brain: drop<br/>mark thread mute<br/>propagate to KG"]:::ai
    Gate -- "wrong fields" --> Override["AI Brain: re-extract<br/>with override hint"]:::ai
    Override --> Card

    Task --> Trigger["Trigger Stage 2:<br/>Style record draft<br/>workflow_run_id propagates"]:::ai
```

### Sequence

```mermaid theme={null}
sequenceDiagram
    autonumber
    actor C as Client
    participant G as Gmail API
    participant KG as "KG service (Andy)"
    participant V as "pgvector (Supabase)"
    participant KGDB as "KG tables (Supabase)"
    participant Buck as "Storage bucket"
    participant A as "Anthropic API"
    participant B as "Brain framework (Thai)"
    actor M as Mai

    Note over C,M: Polling loop (every 30s)
    KG->>G: users.history.list<br/>?startHistoryId=cursor
    G-->>KG: history entries + new IDs
    loop per new gmail_message_id
        KG->>G: users.messages.get<br/>?format=full&id={id}
        G-->>KG: message + attachments (base64)
        KG->>KGDB: SELECT FROM messages_indexed<br/>WHERE gmail_message_id={id}
        alt already indexed
            KGDB-->>KG: hit (skip)
        else new
            KGDB-->>KG: miss
            KG->>KG: extract attachments,<br/>sha256 each blob
            KG->>Buck: PUT rfq_attachments/{sha}
            KG->>KG: assign provenance_ids<br/>email:gmail-msg-{id}<br/>pdf:sha256-{hex}
            KG->>KGDB: INSERT artifacts rows
            KG->>KG: extract text via<br/>pdfplumber / openpyxl / OCR
            KG->>KG: chunk at 512 tokens<br/>w/ 64 overlap
            KG->>A: POST /v1/embeddings<br/>model=text-embedding-3-large
            A-->>KG: 3072-dim vectors
            KG->>V: INSERT chunks (vector + metadata)
            KG->>A: messages.create<br/>tool: extract_entities<br/>model=claude-sonnet-4-6
            A-->>KG: Person, Client, Style,<br/>Thread, Attachment
            KG->>KGDB: INSERT nodes + edges
            KG->>KGDB: INSERT messages_indexed<br/>UPDATE ingest cursor
            KG->>KGDB: INSERT indexing_events<br/>(type=indexed)
        end
    end

    Note over C,M: Brain pickup
    B->>KG: GET /v0/emails?since={cursor}<br/>X-Session-Id={trace}
    KG-->>B: IndexedEmailResponse<br/>(emails, indexing_lag_seconds)
    loop per IndexedEmail
        B->>B: heuristic classify<br/>sender + subject + attachments
        alt heuristic conf < 0.8
            B->>A: messages.create<br/>tool: classify_email<br/>model=claude-sonnet-4-6
            A-->>B: classification + confidence
        end
        B->>B: extract client_id, style_ref,<br/>qty, due_date, incoterms
        B->>B: render Inbox card payload
        B->>M: push Inbox card via SSE
    end
    M->>B: confirm RFQ / file / spam<br/>(workflow_action POST)
    alt confirmed RFQ
        B->>B: create Task workflow_run<br/>(idempotent on gmail_message_id)
        B-->>B: emit stage_2_trigger event
    else file only
        B->>KGDB: UPDATE threads SET state='filed'
    else spam
        B->>KGDB: UPDATE threads SET state='muted'
        B->>KG: PUT /v0/threads/{id}/mute
    end
```

## Stage 2 — Style record creation

Brain extracts fields via regex (hard refs) and LLM tool-use (prose), matches the client (exact domain → fuzzy name → past style\_refs), drafts a Style record, asks Mai for any missing pieces, then writes to CutMake on approval. Brain owns this stage end to end; KG is read-only.

### Flowchart

```mermaid theme={null}
flowchart LR
    classDef ai fill:#dbeafe,stroke:#1e40af
    classDef human fill:#fef3c7,stroke:#b45309
    classDef gate fill:#fde68a,stroke:#92400e
    classDef store fill:#d1fae5,stroke:#047857
    classDef io fill:#e0e7ff,stroke:#3730a3

    Task["Task from Stage 1:<br/>new RFQ for client X<br/>workflow_run_id"]:::ai

    Task --> Fetch["AI Brain: fetch via<br/>GET /v0/emails/{id}<br/>+ attachments list"]:::ai
    Fetch --> KGRead[("KG service:<br/>IndexedEmail +<br/>Provenance refs")]:::store

    KGRead --> Rule["AI Brain: regex extract<br/>style_ref: [A-Z]{2,3}[0-9]{3,6}<br/>dates: ISO + natural<br/>qty: numeric + units"]:::ai
    KGRead --> LLM["AI Brain: LLM extract<br/>Claude Sonnet, tool-use<br/>fabric mentions, special notes,<br/>shipping address, incoterms,<br/>special instructions"]:::ai

    Rule --> Merge["AI Brain: merge fields<br/>regex wins on hard refs<br/>LLM wins on prose"]:::ai
    LLM --> Merge

    Merge --> CMLookup
    CMLookup["AI Brain: client lookup<br/>1. SELECT WHERE domain=sender_domain<br/>2. fuzzy match on display_name<br/>3. fuzzy match on past style_refs"]:::ai
    CMLookup --> ClientTbl[(CutMake.clients<br/>id, domain, name, aliases)]:::store
    ClientTbl --> Match{Client<br/>matched?}:::gate
    Match -- "yes — exact" --> AssignClient["AI Brain: assign client_id"]:::ai
    Match -- "yes — fuzzy" --> ConfirmClient{"Mai: confirm<br/>client = X?"}:::gate
    Match -- "no" --> NewClient["AI Brain: stage new client<br/>(pending Mai approval)"]:::ai
    ConfirmClient -- "yes" --> AssignClient
    ConfirmClient -- "no — pick" --> PickClient["Mai: search + pick<br/>or create new"]:::ai
    PickClient --> AssignClient
    NewClient --> AssignClient

    AssignClient --> StyleDraft["AI Brain: Style record draft<br/>style_ref, season, qty range,<br/>category_hint, due_date,<br/>special_notes, provenance_ids"]:::ai
    StyleDraft --> MissingCheck{All required<br/>fields populated?}:::gate
    MissingCheck -- "no — missing tech pack" --> AskTP["AI Brain: ask Mai<br/>do you have the tech pack?<br/>upload or skip"]:::ai
    MissingCheck -- "no — missing qty" --> AskQty["AI Brain: ask Mai for qty"]:::ai
    MissingCheck -- "yes" --> Render

    AskTP --> Upload["Mai: upload tech pack<br/>or confirm skip"]:::ai
    Upload --> KGIndex["AI KG: index uploaded<br/>tech pack (out-of-band)"]:::ai
    KGIndex --> StyleDraft
    AskQty --> StyleDraft

    Render["AI Brain: render Style draft<br/>in chat with edit affordance"]:::ai
    Render --> Approval{Mai: approve<br/>or edit?}:::gate
    Approval -- "edits" --> StyleDraft
    Approval -- "wrong client" --> CMLookup
    Approval -- "approve" --> Validate["AI Brain: pydantic validate<br/>vs Style schema"]:::ai

    Validate --> Write[("CutMake DB:<br/>INSERT styles row<br/>style_id returned")]:::store
    Write --> Link["AI Brain: link email →<br/>style_id in KG"]:::ai
    Link --> LinkKG[("KG: INSERT<br/>Email-ABOUT-Style edge")]:::store
    Link --> Trigger["Trigger Stage 3:<br/>BOM generation<br/>(workflow_run_id propagates)"]:::ai
```

### Sequence

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant B as "Brain framework (Thai)"
    participant KG as "KG service (Andy)"
    participant A as "Anthropic API"
    participant CM as "CutMake DB"
    actor M as Mai

    Note over B,M: Triggered by Stage 1 Task confirmation
    B->>KG: GET /v0/emails/{id}<br/>+ provenance refs
    KG-->>B: IndexedEmail + attachments

    Note over B,M: Field extraction (parallel)
    par regex pass
        B->>B: regex extract<br/>style_ref, dates, qty
    and LLM pass
        B->>A: messages.create<br/>tool: extract_style_fields<br/>model=claude-sonnet-4-6
        A-->>B: structured fields
    end
    B->>B: merge regex + LLM

    Note over B,M: Client matching
    B->>CM: SELECT clients WHERE domain=...
    CM-->>B: candidates
    B->>B: fuzzy match if no exact hit
    alt fuzzy match found
        B->>M: confirm client = X?
        M-->>B: yes / pick another / new
    else no match
        B->>M: client unknown — pick or create
        M-->>B: selection
    end

    Note over B,M: Style draft + missing-data loop
    B->>B: assemble Style record draft
    alt tech pack missing
        B->>M: do you have the tech pack?
        M-->>B: upload file
        B->>KG: POST /v0/ingest<br/>(out-of-band upload)
        KG-->>B: provenance_id
    end
    B->>M: render Style draft in chat

    loop until approved
        M-->>B: edits / approve
        alt edits
            B->>B: re-render with edits
        else approve
            B->>B: pydantic validate vs Style schema
        end
    end

    B->>CM: INSERT styles row
    CM-->>B: style_id
    B->>KG: PUT /v0/edges<br/>Email-ABOUT-Style {style_id}
    B-->>B: emit stage_3_trigger event
```

## Stage 3 — BOM generation

This is the workflow with the most LLM-heavy reasoning and the tightest cost discipline. BOM agent owns the work; Brain orchestrates and runs Mai's reconfirm loop.

<Warning>
  **Hard cost cap.** BOM agent enforces a per-request vision budget (default \$2.00). Pathological PDFs with hundreds of images return `partial: true` with `skipped_pages` populated rather than running away with cost. Brain renders the partial BOM and asks Mai to fill the gaps.
</Warning>

<Note>
  **Reconfirm rule.** Every LOW-confidence line gates on Mai before the BOM can be approved. HIGH and MEDIUM lines do not gate. "If AI is guessing → always reconfirm" is a hard rule, not a heuristic.
</Note>

### Flowchart

```mermaid theme={null}
flowchart LR
    classDef ai fill:#dbeafe,stroke:#1e40af
    classDef human fill:#fef3c7,stroke:#b45309
    classDef gate fill:#fde68a,stroke:#92400e
    classDef store fill:#d1fae5,stroke:#047857
    classDef io fill:#e0e7ff,stroke:#3730a3

    Style["Approved Style record:<br/>style_id, style_ref,<br/>category_hint, attachments"]:::ai
    TP[/Tech pack PDF/]:::io
    POM[/POM xlsx/]:::io
    Meas[/Measurement PDF/]:::io

    Style --> TP
    Style --> POM
    Style --> Meas

    Style --> Request["AI Brain: build<br/>BomGenerationRequest<br/>+ request_id (idempotency)"]:::ai
    Request --> IdmpCache{"BOM agent:<br/>request_id in cache?"}:::gate
    IdmpCache -- "yes" --> Cached[Return cached<br/>BomGenerationResponse]:::ai
    IdmpCache -- "no" --> Parse

    Parse["AI BOM: parse PDFs<br/>pdfplumber → text per page<br/>pdf2image → 150dpi page images"]:::ai
    Parse --> ParseX["AI BOM: parse POM xlsx<br/>openpyxl<br/>POM table → dict"]:::ai
    Parse --> ParseM["AI BOM: parse measurement<br/>pdfplumber → measurement<br/>chart"]:::ai

    ParseX --> Visible
    ParseM --> Visible

    Parse --> Prio["AI BOM: prioritize pages<br/>first N + pages with<br/>detected garment images"]:::ai
    Prio --> Budget1{"Vision budget<br/>remaining?"}:::gate
    Budget1 -- "yes" --> Vision
    Budget1 -- "no, hit cap" --> SkipPage[Skip page,<br/>record in skipped_pages]:::ai
    SkipPage --> NextPage{More<br/>pages?}:::gate
    NextPage -- "yes" --> Budget1
    NextPage -- "no" --> Visible

    Vision["AI BOM: per-page vision call<br/>Anthropic SDK<br/>model=claude-sonnet-4-6<br/>cost meter wraps each call"]:::ai
    Vision --> CostMeter["AI BOM: cost meter<br/>accumulate USD per request<br/>raises BudgetExceeded at cap"]:::ai
    CostMeter --> NextPage

    Vision --> Garment["AI BOM: classify garment<br/>jacket / pants / dress /<br/>shirt / skirt / blazer"]:::ai
    Vision --> Visible["AI BOM: extract visible BOM<br/>shell, lining, buttons, zipper,<br/>contrast, trims, embellishments"]:::ai

    Garment --> TemplLookup
    TemplLookup["AI BOM: look up category template<br/>e.g. blazer_v0.3<br/>(template_version recorded)"]:::ai
    TemplLookup --> TemplStore[(category templates<br/>blazer, jacket, pants,<br/>dress, shirt)]:::store

    Style --> Client["Client record from Style"]:::ai
    Client --> Pack["AI BOM: client packaging<br/>conventions<br/>(if known)"]:::ai

    TemplLookup --> Hidden["AI BOM: fill invisible BOM<br/>interlining, fusing, thread,<br/>main label, care label,<br/>price tag, polybag, carton"]:::ai
    Pack --> Hidden

    Visible --> Score["AI BOM: per-line confidence<br/>HIGH if direct text match<br/>MED if vision-extracted<br/>LOW if template-guessed or<br/>image-inferred"]:::ai
    Hidden --> Score

    Score --> Prov["AI BOM: assign provenance_id<br/>per line<br/>(point into KG: pdf:sha-{hex}#page{N})"]:::ai
    Prov --> Aggregate["AI BOM: aggregate BOM<br/>lines + cost_usd +<br/>template_version + partial flag"]:::ai

    Aggregate --> Response["BomGenerationResponse<br/>(see brain_contracts/bom_agent.py)"]:::ai
    Response --> WriteIdmp[("AI BOM: write to<br/>idempotency cache<br/>{request_id: response}")]:::store

    Response --> RenderUI["AI Brain: render BOM in chat<br/>group by visible/invisible<br/>color by confidence"]:::ai
    Cached --> RenderUI

    RenderUI --> LowConfCheck{Any LOW conf<br/>lines?}:::gate
    LowConfCheck -- "yes" --> Reconfirm["AI Brain: per LOW line<br/>show source page (provenance) +<br/>ask: is X correct?"]:::ai
    Reconfirm --> MaiReply{Mai: confirm,<br/>correct, or skip}:::gate
    MaiReply -- "confirm" --> UpdateConf["AI Brain: mark line<br/>confidence=HIGH (Mai approved)"]:::ai
    MaiReply -- "correct" --> EditLine["AI Brain: apply edit<br/>(material, qty, consumption)"]:::ai
    MaiReply -- "skip line" --> Skip2[AI Brain: drop line,<br/>flag for manual add]:::ai
    UpdateConf --> RenderUI
    EditLine --> RenderUI
    Skip2 --> RenderUI

    LowConfCheck -- "no" --> Final{Mai: approve<br/>full BOM?}:::gate
    Final -- "edit" --> EditLine
    Final -- "regenerate" --> Request
    Final -- "approve" --> Validate["AI Brain: pydantic-ai<br/>validate vs BomLine schema<br/>(strict)"]:::ai

    Validate --> ValidOK{Valid?}:::gate
    ValidOK -- "no" --> Manual["AI Brain: surface error,<br/>Mai fixes inline"]:::ai
    Manual --> RenderUI
    ValidOK -- "yes" --> Event["AI Brain: emit<br/>BOM_APPROVED event<br/>workflow_run_id, style_id, lines"]:::ai

    Event --> WriteBOM[("CutMake DB:<br/>INSERT bom_lines rows<br/>style_id FK")]:::store
    WriteBOM --> Trigger["Trigger Stage 4:<br/>True cost computation"]:::ai
```

### Sequence

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant B as "Brain framework (Thai)"
    participant BOM as "BOM agent (Ku Kue)"
    participant A as "Anthropic API"
    participant KG as "KG service (Andy)"
    participant CM as "CutMake DB"
    actor M as Mai

    Note over B,M: Triggered by Stage 2 Style approval
    B->>B: build BomGenerationRequest<br/>(style_id, urls, request_id,<br/>vision_budget_usd=2.0)
    B->>BOM: POST /v0/generate-bom<br/>X-Session-Id={trace}

    BOM->>BOM: SELECT idempotency cache<br/>WHERE request_id=...
    alt cache hit
        BOM-->>B: cached BomGenerationResponse
    else cache miss
        BOM->>BOM: pdfplumber text per page<br/>pdf2image @ 150dpi<br/>openpyxl POM
        BOM->>BOM: prioritize pages by<br/>garment image detector
        loop per prioritized page (budget-gated)
            BOM->>BOM: cost_meter.remaining()?
            alt remaining > page_cost_estimate
                BOM->>A: messages.create (vision)<br/>model=claude-sonnet-4-6<br/>image + extraction prompt
                A-->>BOM: visible parts JSON
                BOM->>BOM: cost_meter.add(usd)
            else budget hit
                BOM->>BOM: add to skipped_pages
            end
        end
        BOM->>BOM: classify garment category
        BOM->>BOM: lookup category template<br/>(blazer_v0.3 etc.)
        BOM->>BOM: fill invisible BOM<br/>+ client packaging conventions
        BOM->>BOM: per-line confidence score
        BOM->>BOM: assign provenance_id<br/>pdf:sha-{hex}#page{N}
        BOM->>BOM: pydantic-validate response
        BOM->>BOM: write to idempotency cache
        BOM-->>B: BomGenerationResponse<br/>(lines, cost_usd, partial,<br/>template_version)
    end

    Note over B,M: Brain renders + reconfirm loop
    B->>B: group lines by visible/invisible<br/>color by confidence
    B->>M: render BOM with source links

    loop while LOW-conf lines remain
        B->>KG: GET /v0/provenance/lookup<br/>{provenance_ids: [...]}
        KG-->>B: Provenance refs (URL + snippet)
        B->>M: per LOW line: show source +<br/>"is X correct?"
        M-->>B: confirm / correct / skip
        alt confirm
            B->>B: mark line confidence=HIGH
        else correct
            B->>B: apply edit
        else skip
            B->>B: drop line + flag manual
        end
    end

    M-->>B: approve full BOM
    B->>B: pydantic-ai validate strict
    alt validation fails
        B->>M: surface error inline
        M-->>B: fix
    else valid
        B->>CM: INSERT bom_lines rows
        CM-->>B: bom_line_ids
        B-->>B: emit BOM_APPROVED event<br/>trigger Stage 4
    end
```

## Stage 4 — True cost computation

Brain matches each BOM line to a supplier price (exact code → fuzzy on type/color/gsm), converts to base currency, adds MOQ surcharges, layers in overheads (labor SAM, freight zone, HS-code duty, defect allowance), then runs an anomaly check vs the prior 50 styles in the same category for the same client. Zean reviews.

### Flowchart

```mermaid theme={null}
flowchart LR
    classDef ai fill:#dbeafe,stroke:#1e40af
    classDef human fill:#fef3c7,stroke:#b45309
    classDef gate fill:#fde68a,stroke:#92400e
    classDef store fill:#d1fae5,stroke:#047857

    BOM["Approved BOM lines<br/>from Stage 3"]:::ai
    Qty["Order quantity +<br/>size breakdown<br/>from Style"]:::ai

    BOM --> Match["AI Brain: match each BOM line<br/>to supplier price entry<br/>by material code or fuzzy<br/>on (type, color, gsm)"]:::ai
    Match --> Materials[(materials price book<br/>CutMake.materials<br/>supplier_id, currency, price)]:::store
    Match --> MatchResult{All lines<br/>matched?}:::gate
    MatchResult -- "no — unmatched" --> Stage["AI Brain: stage unmatched<br/>lines as MED conf cost<br/>(uses category average)"]:::ai
    MatchResult -- "yes" --> Calc1
    Stage --> Calc1

    Calc1["AI Brain: per-line raw cost<br/>= unit_price × consumption × qty<br/>(unit-aware, e.g. m, kg, pc)"]:::ai
    Calc1 --> FX[(fx_rates table<br/>quoted_currency → THB/USD)]:::store
    FX --> Calc2["AI Brain: convert to base currency<br/>USD or THB per company config"]:::ai
    Calc1 --> Calc2

    Calc2 --> MOQ[(moq_table<br/>per material<br/>moq, lead_time_days)]:::store
    MOQ --> Surcharge["AI Brain: MOQ surcharge<br/>if order_qty < material_moq<br/>add (moq - order_qty)<br/>× unit_price"]:::ai
    Calc2 --> Surcharge

    Surcharge --> Overhead["AI Brain: add overheads<br/>labor (per minute SAM × rate)<br/>freight (per kg, lookup zone)<br/>duty (per HS code)<br/>defect allowance (% of materials)"]:::ai
    Overhead --> Overheads[(overheads config<br/>labor_rate, freight_zones,<br/>hs_duty_table)]:::store
    Overhead --> Total["AI Brain: per-garment true cost<br/>+ line-level breakdown<br/>+ percentage attribution"]:::ai

    Total --> Anom["AI Brain: anomaly detection<br/>z-score vs prior 50 styles<br/>same category + client"]:::ai
    Anom --> History[(historical_costs<br/>same category + client)]:::store
    Anom --> Flags["AI Brain: flag lines with<br/>z > 2 (high) or z < -2 (low)<br/>+ explain delta vs prior"]:::ai

    Flags --> Render["AI Brain: render cost<br/>breakdown to Zean<br/>line-level + total +<br/>anomaly flags + explanations"]:::ai

    Render --> Gate{Zean: approve<br/>cost?}:::gate
    Gate -- "adjust line cost" --> EditLine["Zean: edit line<br/>(unit_price, consumption,<br/>or material swap)"]:::ai
    Gate -- "swap supplier" --> SwapSup["Zean: pick alternate<br/>supplier from price book"]:::ai
    Gate -- "change qty" --> Qty
    Gate -- "explain flag" --> Explain["AI Brain: show prior<br/>styles + price history"]:::ai
    Gate -- "approve" --> Validate["AI Brain: pydantic validate<br/>vs TrueCost schema"]:::ai

    EditLine --> Calc1
    SwapSup --> Match
    Explain --> Render

    Validate --> Write[("CutMake DB:<br/>INSERT true_costs row<br/>+ true_cost_lines rows")]:::store
    Write --> Event["AI Brain: emit<br/>COST_APPROVED event"]:::ai
    Event --> Trigger["Trigger Stage 5:<br/>Quotation drafting"]:::ai
```

### Sequence

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant B as "Brain framework (Thai)"
    participant CM as "CutMake DB"
    participant A as "Anthropic API"
    actor Z as Zean

    Note over B,Z: Triggered by Stage 3 BOM approval
    B->>CM: SELECT bom_lines<br/>WHERE style_id=...
    CM-->>B: BOM lines

    Note over B,Z: Match + price
    loop per BOM line
        B->>CM: SELECT materials<br/>WHERE code=line.material_code<br/>OR fuzzy match on (type, color, gsm)
        CM-->>B: candidates
        alt no match
            B->>A: tool: fuzzy_material_match
            A-->>B: best candidate or null
            B->>B: stage as MED conf<br/>(category avg if null)
        end
    end

    B->>CM: SELECT fx_rates<br/>WHERE quoted_currency IN (...)
    CM-->>B: rates
    B->>B: convert to base currency
    B->>CM: SELECT moq_table<br/>WHERE material_id IN (...)
    CM-->>B: moq + lead_time
    B->>B: compute MOQ surcharge

    B->>CM: SELECT overheads_config
    CM-->>B: labor_rate, freight, duty
    B->>B: add overheads to total

    Note over B,Z: Anomaly check
    B->>CM: SELECT historical_costs<br/>WHERE category=... AND client_id=...<br/>ORDER BY created_at DESC LIMIT 50
    CM-->>B: prior costs
    B->>B: compute z-score per line<br/>flag |z| > 2

    B->>Z: render cost breakdown +<br/>anomaly flags

    loop until approved
        Z-->>B: adjust / swap / qty / approve
        alt adjust line
            B->>B: recompute affected lines
        else swap supplier
            B->>CM: SELECT materials WHERE supplier_id=...
            B->>B: re-match + recompute
        else explain flag
            B->>Z: show prior styles + history
        else approve
            B->>B: pydantic-validate vs TrueCost
        end
    end

    B->>CM: INSERT true_costs +<br/>true_cost_lines
    CM-->>B: true_cost_id
    B-->>B: emit COST_APPROVED event<br/>trigger Stage 5
```

## Stage 5 — Quotation drafting

Brain applies the client's markup rule, fills terms (incoterms, payment, lead time, validity), renders the quotation PDF via Jinja+WeasyPrint, sanity-checks the unit price against the past 12 months of quotations to the same client. Zean reviews.

### Flowchart

```mermaid theme={null}
flowchart LR
    classDef ai fill:#dbeafe,stroke:#1e40af
    classDef human fill:#fef3c7,stroke:#b45309
    classDef gate fill:#fde68a,stroke:#92400e
    classDef store fill:#d1fae5,stroke:#047857

    Cost["Approved true cost<br/>from Stage 4"]:::ai
    Client["Client record"]:::ai

    Cost --> Rules["AI Brain: load markup rule<br/>per client + category<br/>(default: target_margin %)"]:::ai
    Client --> Rules
    Rules --> RulesTbl[(markup_rules<br/>client_id, category,<br/>margin_pct, currency)]:::store

    Rules --> Markup["AI Brain: apply markup<br/>quote_unit_price =<br/>cost / (1 - margin)"]:::ai
    Markup --> Terms["AI Brain: fill terms<br/>incoterms (default FOB),<br/>payment (30% deposit + 70% BL),<br/>lead time (from MOQ table),<br/>validity (30 days)"]:::ai
    Client --> Terms

    Markup --> Render["AI Brain: render quotation<br/>weasyprint or reportlab<br/>template: quote_v1.html"]:::ai
    Terms --> Render
    Render --> Templ[(quotation templates<br/>jinja2 + CSS)]:::store

    Render --> Past[(past quotations<br/>same client + category<br/>last 12 months)]:::store
    Past --> Sanity["AI Brain: sanity check<br/>compare to median past unit_price<br/>flag if delta > ±15%"]:::ai
    Render --> Sanity

    Sanity --> Explain["AI Brain: if flagged,<br/>generate explanation<br/>(cost driver vs prior)"]:::ai

    Render --> RenderUI["AI Brain: show to Zean<br/>PDF preview + cost-to-quote diff +<br/>sanity flags + explanation"]:::ai
    Explain --> RenderUI

    RenderUI --> Gate{Zean: approve<br/>price + terms?}:::gate
    Gate -- "adjust margin" --> Rules
    Gate -- "edit terms" --> Terms
    Gate -- "edit line price" --> EditLine["Zean: override<br/>specific line price"]:::ai
    Gate -- "approve" --> Validate["AI Brain: pydantic validate<br/>vs Quotation schema"]:::ai
    EditLine --> Markup

    Validate --> Write[("CutMake DB:<br/>INSERT quotations row<br/>+ quotation_lines rows")]:::store
    Write --> StorePDF[(Storage bucket:<br/>quotations/{quote_id}.pdf)]:::store
    Write --> Event["AI Brain: emit<br/>QUOTE_APPROVED event"]:::ai
    Event --> Trigger["Trigger Stage 6:<br/>Reply email drafting"]:::ai
```

### Sequence

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant B as "Brain framework (Thai)"
    participant CM as "CutMake DB"
    participant Buck as "Storage bucket"
    actor Z as Zean

    Note over B,Z: Triggered by Stage 4 cost approval
    B->>CM: SELECT true_costs WHERE id=...
    CM-->>B: true cost + lines
    B->>CM: SELECT markup_rules<br/>WHERE client_id AND category
    CM-->>B: margin_pct
    B->>B: apply markup<br/>quote_price = cost / (1 - margin)

    B->>CM: SELECT clients WHERE id=...
    CM-->>B: incoterms, payment_terms,<br/>default_lead_time
    B->>B: fill quotation terms

    B->>B: render via jinja2 + weasyprint
    B->>Buck: PUT quotations/{id}.pdf (preview)

    B->>CM: SELECT quotations<br/>WHERE client_id AND category<br/>AND created_at > now() - 365d
    CM-->>B: past quotations
    B->>B: compute delta vs median<br/>flag if > ±15%

    B->>Z: render PDF preview + diff + flags

    loop until approved
        Z-->>B: adjust / edit / approve
        alt adjust margin
            B->>B: recompute quote_price
        else edit terms
            B->>B: re-render PDF
        else edit line price
            B->>B: override + re-render
        else approve
            B->>B: pydantic-validate vs Quotation schema
        end
    end

    B->>CM: INSERT quotations + quotation_lines
    CM-->>B: quote_id
    B->>Buck: PUT quotations/{quote_id}.pdf (final)
    B-->>B: emit QUOTE_APPROVED event<br/>trigger Stage 6
```

## Stage 6 — Client reply email

Brain pulls recent thread context via KG retrieval, detects tone (formal / casual / urgent), composes the reply with anchors the client used (style\_ref, dates, specific questions), attaches the quote, shows Mai a draft. On send, Gmail outbound is re-indexed and linked to the RFQ thread.

### Flowchart

```mermaid theme={null}
flowchart LR
    classDef ai fill:#dbeafe,stroke:#1e40af
    classDef human fill:#fef3c7,stroke:#b45309
    classDef gate fill:#fde68a,stroke:#92400e
    classDef store fill:#d1fae5,stroke:#047857
    classDef ext fill:#f3f4f6,stroke:#4b5563

    Quote["Approved quotation PDF<br/>from Stage 5"]:::ai
    Thread["Original RFQ thread<br/>(gmail thread_id)"]:::ai

    Thread --> Retrieve["AI Brain: POST /v0/retrieve<br/>scope: thread_id + style_id<br/>max_results=20"]:::ai
    Retrieve --> KGRet[(KG: cross-source retrieval<br/>prior emails, prior quotes,<br/>past replies same client)]:::store

    KGRet --> Tone["AI Brain: LLM detect tone<br/>formal / casual / urgent<br/>output: tone label + signals"]:::ai
    KGRet --> Anchors["AI Brain: extract anchors<br/>style_ref, qty, dates,<br/>specific questions client asked"]:::ai

    Quote --> Compose["AI Brain: draft email body<br/>structured prompt:<br/>greet by name (anchor) +<br/>reference style_ref +<br/>state quote price + lead time +<br/>answer asked questions +<br/>close in detected tone"]:::ai
    Tone --> Compose
    Anchors --> Compose

    Compose --> Templ[(reply templates<br/>per language: en, th, vi)]:::store
    Templ --> Compose

    Compose --> Attach["AI Brain: bind attachments<br/>quotation PDF + addenda<br/>(size chart, fabric swatches<br/>if previously discussed)"]:::ai
    Quote --> Attach

    Attach --> DiffPast["AI Brain: diff vs<br/>past replies to same client<br/>(structure + phrasing only)"]:::ai
    KGRet --> DiffPast

    DiffPast --> RenderUI["AI Brain: show draft to Mai<br/>body + attachments + diff +<br/>To/Cc auto-filled from thread"]:::ai

    RenderUI --> Gate{Mai: approve<br/>+ send?}:::gate
    Gate -- "edit body" --> EditBody["Mai: edit prose<br/>or regenerate"]:::ai
    Gate -- "edit recipients" --> EditTo["Mai: add/remove<br/>To/Cc"]:::ai
    Gate -- "different attachment" --> Attach
    Gate -- "regenerate" --> Compose
    Gate -- "send" --> Send["AI Brain: send via<br/>users.messages.send<br/>threadId={thread_id}<br/>In-Reply-To header set"]:::ai
    EditBody --> RenderUI
    EditTo --> RenderUI

    Send --> Gmail[(Gmail API)]:::ext
    Gmail --> Client((Client)):::ext
    Send --> KGReindex["AI KG: re-index outbound<br/>(picked up on next poll)<br/>link to RFQ thread"]:::ai
    KGReindex --> KGUpdate[(KG: Email-REPLY_TO-Email<br/>edge added)]:::store
    Send --> Write[("CutMake DB:<br/>INSERT email_sent row<br/>quote_id FK")]:::store
    Write --> Event["AI Brain: emit<br/>QUOTE_SENT event"]:::ai
    Event --> Done([Lifecycle complete —<br/>workflow_run state = QUOTED<br/>awaiting client response]):::ai
```

### Sequence

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant B as "Brain framework (Thai)"
    participant KG as "KG service (Andy)"
    participant A as "Anthropic API"
    participant G as "Gmail API"
    participant CM as "CutMake DB"
    actor M as Mai
    actor C as Client

    Note over B,C: Triggered by Stage 5 Quote approval
    B->>KG: POST /v0/retrieve<br/>{question: "what did client<br/>previously ask?",<br/>scope: {thread_id, style_id,<br/>max_results: 20}}
    KG-->>B: RetrievalResponse<br/>(prior emails, quotes, replies)

    par tone detection
        B->>A: messages.create<br/>tool: detect_tone<br/>input: thread history
        A-->>B: tone label + signals
    and anchor extraction
        B->>A: messages.create<br/>tool: extract_anchors<br/>input: original RFQ
        A-->>B: style_ref, qty, dates,<br/>questions asked
    end

    B->>B: select reply template<br/>by language (en/th/vi)
    B->>A: messages.create<br/>tool: compose_reply<br/>input: tone + anchors +<br/>quote_summary + template
    A-->>B: drafted email body

    B->>B: bind attachments<br/>(quotation PDF + addenda)
    B->>KG: retrieve past replies to same<br/>client for structure diff
    KG-->>B: 3-5 past replies
    B->>B: compute diff (structural)

    B->>M: render draft +<br/>attachments + diff + To/Cc

    loop until approved
        M-->>B: edit / regenerate / approve
        alt edit body
            B->>B: apply Mai edits
        else regenerate
            B->>A: re-compose with feedback
            A-->>B: new draft
        else edit recipients
            B->>B: update To/Cc
        else approve
            B->>B: pydantic-validate Email schema
        end
    end

    B->>G: users.messages.send<br/>threadId={thread_id}<br/>In-Reply-To: {message_id}
    G-->>B: sent message_id
    G->>C: deliver email
    B->>CM: INSERT email_sent +<br/>UPDATE workflow_runs<br/>SET state='QUOTED'
    B-->>B: emit QUOTE_SENT event

    Note over KG,G: Outbound re-indexed
    KG->>G: users.history.list (next poll)
    G-->>KG: outbound message picked up
    KG->>KG: index + link to thread<br/>Email-REPLY_TO-Email edge
    Note over B,C: Lifecycle complete — awaiting client response
```

## Related

* [Aggregator + Workflow Registry](/brain/staging/architecture/aggregator-and-workflows) — how chat, slash, and Inbox clicks all route to the workflows in this lifecycle
* [Implementation Tiers](/brain/staging/architecture/tiers) — when each stage is a skill, workflow, or full agent
* [Observability](/brain/staging/architecture/observability) — the LangSmith → Langfuse migration and the correction log
* [Data Integration](/brain/staging/architecture/data-integration) — Brain Supabase vs CutMake Supabase boundary
