Prolo — RFQ Matching Data Flow (Stages 5–6)

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
Stage / process step Yellow = AI-bearing (kNN · rerank · LLM) Store / external API (pipeline · RFQ · procurement) Human Review (cross-cutting) Solid = data flow · Dotted = escalation / sync

RFQ Matching — decomposed sub-pipeline (Stage 1, was 5)

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

Matching (Stage 1, was 5)

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[].

Submission (Stage 2, was 6)

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.

Human Review

No / ambiguous match (Stage 1) → review queue, using stored provenance + matchEvidence. Rejected submission (Stage 2) → review, corrected, resubmitted.