Journey of a quote after ingestion: from the ExtractedQuote in the pipeline store → matched to a customer RFQ → submitted to the core system. Renumbered locally — Stage 1 = matching (was 5), Stage 2 = submit (was 6). This is the logical flow; matching internals are in rfq-matching.md. Yellow = AI-bearing.
flowchart TB
DB[("Pipeline store
ExtractedQuote (orphan)")]
RFQ[("RFQ read-model
synced from core + embeddings")]
subgraph M["1 · RFQ MATCHING"]
direction TB
NARROW["SQL narrow
supplier · date · qty/total ranges"]
KNN["embedding kNN (recall)
quote line ↔ RFQ line"]
RERANK["cross-encoder rerank (precision)"]
LLM["LLM disambiguate
bundling · units · confidence"]
NARROW --> KNN --> RERANK --> LLM
end
P["2 · SUBMIT TO CORE
update system of record"]
DB -->|"read new quote's lines (EXTRACTED)"| M
RFQ -->|candidate RFQ lines| M
M -->|"per-line matches (line↔rfq_line, evidence)
→ roll up status = MATCHED"| DB
DB -->|read matched| P
P -->|"submit / update (P2)"| API[(Internal procurement API)]
API -.->|periodic sync| RFQ
M -.no/ambiguous match.-> H
P -.rejected.-> H
H["HUMAN REVIEW
cross-cutting"]
H -.corrected.-> P
classDef ai fill:#fde047,stroke:#ca8a04,color:#000
class KNN,RERANK,LLM ai
%% node / zone colours
style DB fill:#eef1f4,stroke:#57606a,stroke-width:1.5px,color:#1f2328
style RFQ fill:#eef1f4,stroke:#57606a,stroke-width:1.5px,color:#1f2328
style API fill:#eef1f4,stroke:#57606a,stroke-width:1.5px,color:#1f2328
style NARROW fill:#eef4ff,stroke:#3b7ddd,stroke-width:1.5px,color:#1f2328
style P fill:#eef4ff,stroke:#3b7ddd,stroke-width:1.5px,color:#1f2328
style H fill:#fdeef0,stroke:#d1242f,stroke-width:1.5px,color:#1f2328
Matching is not one LLM call — it's a retrieve-then-rerank funnel that turns a new
quote's line items into durable line_item_matches:
SQL narrow → kNN recall → cross-encoder rerank → LLM disambiguate → confidence → write.
Embeddings are precomputed at Stage 4; the cheap rerank clears easy pairs, the LLM only handles the
hard/bundled ones. Yellow = AI-bearing. Diamonds are decisions.
flowchart TB
IN["new ExtractedQuote
(lines + precomputed embeddings)"]
RFQ[("RFQ read-model
(synced, embedded)")]
NARROW["SQL NARROW
supplier · date · qty/total ranges"]
CAND{"any candidate
RFQ lines?"}
KNN["kNN RECALL (pgvector)
top-K RFQ lines per quote line"]
RERANK["CROSS-ENCODER RERANK
reorder pairs by precision"]
THRESH{"top pair
confidently clear?"}
LLM["LLM DISAMBIGUATE (Bedrock)
bundling · units · quantity · rationale"]
CONF{"match_confidence
≥ threshold?"}
WRITE["WRITE line_item_matches
(line ↔ rfq_line, evidence)"]
ROLL["ROLL UP quote → status = MATCHED
enqueue to submit (P2)"]
H["HUMAN REVIEW"]
IN --> NARROW
RFQ -->|candidate lines| NARROW
NARROW --> CAND
CAND -- no --> H
CAND -- yes --> KNN --> RERANK --> THRESH
THRESH -- yes --> CONF
THRESH -- "no (hard/bundled)" --> LLM --> CONF
CONF -- yes --> WRITE --> ROLL
CONF -- "no / ambiguous" --> H
classDef ai fill:#fde047,stroke:#ca8a04,color:#000
class KNN,RERANK,LLM ai
%% node colours
style IN fill:#f2f0ff,stroke:#8250df,stroke-width:1.5px,color:#1f2328
style RFQ fill:#eef1f4,stroke:#57606a,stroke-width:1.5px,color:#1f2328
style NARROW fill:#eef4ff,stroke:#3b7ddd,stroke-width:1.5px,color:#1f2328
style WRITE fill:#eef4ff,stroke:#3b7ddd,stroke-width:1.5px,color:#1f2328
style ROLL fill:#eef4ff,stroke:#3b7ddd,stroke-width:1.5px,color:#1f2328
style CAND fill:#fff8e6,stroke:#bf8700,stroke-width:1.5px,color:#1f2328
style THRESH fill:#fff8e6,stroke:#bf8700,stroke-width:1.5px,color:#1f2328
style CONF fill:#fff8e6,stroke:#bf8700,stroke-width:1.5px,color:#1f2328
style H fill:#fdeef0,stroke:#d1242f,stroke-width:1.5px,color:#1f2328
The hard part: there's no RFQ identifier in the email. Two layers —
SQL narrow (date window · supplier · SKUs · totals) to shrink the search space, then
LLM / semantic disambiguate among the narrowed candidates to match fuzzy line-item
descriptions that share no key → emits rfqId, matchConfidence,
matchEvidence[].
Reads the matched row and calls the internal procurement API to submit/update the
SupplierQuoteObject. Crossing into a system you don't own, so
idempotency, revisions, and API-down are handled here; on success the row flips to
submitted.
No / ambiguous match (Stage 1) → review queue, using stored provenance +
matchEvidence. Rejected submission (Stage 2) → review, corrected,
resubmitted.