Multi-Stage Pipeline
Three stages — exact, normalized, and RLM — to maximize accuracy and minimize cost.
Matcher links supplier (DealerProduct) items to your Oscar catalog. It progressively narrows candidates through cheap deterministic stages before invoking expensive LLM reasoning, so only the hardest cases reach the AI.
Multi-Stage Pipeline
Three stages — exact, normalized, and RLM — to maximize accuracy and minimize cost.
RLM Deep Matching
Mercury-2 Recursive Language Model processes 50k+ products in a single recursive pass.
Gardener Integration
Works on pre-normalized data from Gardener for normalized-vs-normalized comparison.
MCP Tools
find_match_candidates, link_products, unlink_product, bulk_link_products for AI-driven matching workflows.
After Gardener normalizes both sides, Matcher runs three stages:
Stage 1: Exact Match ← EAN, manufacturer_sku, SKU FREEStage 2: Normalized Match ← brand + model + type + color FREEStage 3: RLM Deep Match ← Mercury-2 recursive reasoning PAIDExpected: Stages 1–2 resolve ~60-70% of matches. Only the remaining 30-40% go to RLM.
Deterministic exact-key comparison:
| Key | Example | Confidence |
|---|---|---|
| EAN-13 barcode | 889169539893 ↔ 889169539893 | 1.0 |
| Manufacturer SKU | 41200 ↔ 41200 | 0.95 |
| EAN + brand | Barcode + brand name | 1.0 |
Uses Gardener-normalized fields for structured comparison:
score = 0.0if dealer.brand == oscar.brand: score += 0.30if fuzzy(dealer.model, oscar.model) > 85: score += 0.30if dealer.product_type == oscar.type: score += 0.15if dealer.category == oscar.category: score += 0.10if dealer.mfr_sku == oscar.mfr_sku: score += 0.15# threshold: ≥0.7 → auto-match, 0.5–0.7 → candidateFor products that Stage 1 and 2 couldn’t resolve, the Matcher invokes Mercury-2 (or another configured RLM model) for recursive reasoning.
How RLM works:
matcher = RLMBulkMatcher( rlm_model="mercury-2", # Bound via manifest tenant_id=tenant_id,)result = await matcher.match_all( supplier_products=supplier_list, site_products=oscar_list, confidence_threshold=0.7, taxonomies=taxonomy_data, manual_matches=confirmed_pairs, wrong_pairs=rejected_pairs,)Inputs to RLM:
Each match produces:
{ "supplier_id": "12345", "site_id": "oscar-678", "confidence": 0.92, "match_type": "rlm_deep", # exact_ean | exact_sku | normalized | rlm_deep "reasoning": "Same brand, model name, similar price range"}| Status | Meaning |
|---|---|
matched | Confirmed match (auto or manual) |
pending_review | High confidence but needs operator review |
not_matched | No match found |
pending | Not yet processed |
| Tool | Description |
|---|---|
find_match_candidates | Multi-strategy candidate search: SKU → EAN → name → brand+type |
heuristic_semantic_match | Fast heuristic matching with brand pre-index, O(N×B) complexity |
| Tool | Description |
|---|---|
link_products | Link DealerProduct to Oscar Product, optionally sync price/stock |
unlink_product | Remove DealerProduct → Oscar Product link |
bulk_link_products | Batch link/review multiple matches |
| Tool | Description |
|---|---|
export_unmatched_products | Export up to 50k unmatched products for RLM batch |
export_products | Export Oscar products for matching context |
The Matcher relies on the rlm_bulk_matcher node defined in the project manifest. The model and its secrets are explicitly bound in contextunity.project.yaml:
graphs: nodes: - id: rlm_bulk_matcher model: "mercury-2" model_secret_ref: INCEPTION_API_KEYAPI key resolution order:
<tenant>/api_keys/inceptionINCEPTION_API_KEYasync def run_matcher_for_brand(brand: str, tenant_id: str): async with RouterClient(token=token) as client: # Step 1a: Normalize dealer products await client.execute_agent("gardener", { "brand": brand, "source": "dealer", "only_new": True, }) # Step 1b: Normalize oscar products await client.execute_agent("gardener", { "brand": brand, "source": "oscar", "only_new": True, }) # Step 2: Match normalized data await client.execute_agent("rlm_bulk_matcher", { "target_brand": brand, })The PIM Matcher view at /pim/matcher/ shows:
# Router — matching graphcu/router/cortex/graphs/commerce/matcher/├── rlm_bulk/│ ├── matcher.py # RLMBulkMatcher class│ ├── node.py # LangGraph node entry point│ ├── prompts.py # Prompt builder for RLM│ ├── parser.py # Response parser + deduplication│ ├── fallback.py # Chunked fallback if RLM unavailable│ └── types.py # BulkMatchResult, MatchItem types
# Commerce — MCP tools + UIcu/commerce/src/mcp/tools/├── matching/│ ├── query.py # find_match_candidates, heuristic_semantic_match│ └── mutate.py # link_products, unlink_product, bulk_link_products└── suppliers/ └── export.py # export_unmatched_products