# Match a message to a spare part

Extract the order and product from a support message, detect what the customer wants, and match the exact SKU.

Category: Handle customer messages. Live demo: https://levanto.ai/examples/support-intent-sku

A customer writes in about a broken part. To help, you need three things: the order, what they want, and the exact part number your shop sells. This guide gets all three with **two Sage2 calls**: extract the facts, then choose the intent and the matching SKU **from your own catalog**.

When nothing fits, you get `null` or `none`, and a person takes the message.

## Setup

Base URL: `https://sage.levanto.ai`. Send `Authorization: Bearer $SAGE_API_KEY` on every call. Get a key, free, at https://platform.levanto.ai. Keep the key on your server.

A `null` answer is not an error: Sage2 isn't sure. Each step says what to do with it.

## Step by step

### 1. Pull out the order and the product

Send the message to `/v1/extract` with three fields, each described in a few words: `order_number`, `product` and `problem`.

The answers arrive in `values`. If the customer forgot the order number, `order_number` comes back `null` and you know to ask for it.

```python
message = "Hi, order NB-48213. I dropped my Burrly 2 and the hopper lid cracked. Can I buy just the lid? Please don't make me buy another grinder."

found = sage("/v1/extract", {
    "content": message,
    "extract": {
        "order_number": "the order number",
        "product": "the product the customer owns",
        "problem": "what went wrong",
    },
})["values"]
```

```typescript
const message = "Hi, order NB-48213. I dropped my Burrly 2 and the hopper lid cracked. Can I buy just the lid? Please don't make me buy another grinder.";

const found = (await sage("/v1/extract", {
  content: message,
  extract: {
    order_number: "the order number",
    product: "the product the customer owns",
    problem: "what went wrong",
  },
})).values;
```

Response, abridged (a real run):

```json
{
  "values": {
    "order_number": "NB-48213",
    "product": "Burrly 2",
    "problem": "hopper lid cracked"
  },
  "results": {
    "order_number": {
      "value": "NB-48213",
      "text": "NB-48213",
      "start": 10,
      "end": 18,
      "probability": 0.9815,
      "probabilities": [
        {
          "value": "NB-48213",
          "text": "NB-48213",
          "start": 10,
          "end": 18,
          "probability": 0.9815
        },
        {
          "value": null,
          "probability": 0.0185
        }
      ]
    },
    "product": {
      "value": "Burrly 2",
      "text": "Burrly 2",
      "start": 33,
      "end": 41,
      "probability": 0.9988,
      "probabilities": [
        {
          "value": "Burrly 2",
          "text": "Burrly 2",
          "start": 33,
          "end": 41,
          "probability": 0.9988
        },
        {
          "value": null,
          "probability": 0.0012
        }
      ]
    },
    "…": "one entry per field"
  },
  "meta": {
    "model": "levanto-sage-2",
    "usage": {
      "input_tokens": 218,
      "output_tokens": 314
    },
    "latency_ms": 731.6
  }
}
```

### 2. Decide the intent and the part

Ask two Choice questions about the same message in one `/decide/batch` call:

- `intent`: a spare part, an order update, a refund, or help using a product.
- `sku`: **your catalog as `options`**. Each `option` is a SKU and its `description` is the product name. A `none` option catches a part that isn't in your catalog, or no part at all.

Sage2 returns each pick in `chosen`, with its `probability`. The `answers` helper turns the batch response into a dictionary by question `id`. A question that failed is left out, so the code treats it as unsure.

```python
def answers(out, group=0):
    # Each answer is a full decision. Failed ones (ok is false) are left out: treat a missing id as "not sure".
    return {a["result"]["id"]: a["result"]["result"] for a in out["results"][group]["answers"] if a["ok"]}

a = answers(sage("/decide/batch", {
    "requests": [
        {
            "content": message,
            "questions": [
                {
                    "id": "intent",
                    "kind": "choice",
                    "instructions": "What does the customer want?",
                    "options": [
                        {"option": "spare_part", "description": "A replacement part"},
                        {
                            "option": "order_status",
                            "description": "To know where an order is",
                        },
                        {"option": "refund", "description": "Their money back"},
                        {"option": "how_to", "description": "Help using a product"},
                    ],
                },
                {
                    "id": "sku",
                    "kind": "choice",
                    "instructions": "Which of these spare parts does the message need? Choose none if no part on the list fits.",
                    "options": [
                        {"option": "CP3-STEAM", "description": "Steam wand for Crema Pro 3"},
                        {"option": "CP3-TRAY", "description": "Drip tray for Crema Pro 3"},
                        {"option": "CM-JUG", "description": "Milk jug for Crema Mini"},
                        {"option": "BR2-LID", "description": "Hopper lid for Burrly 2"},
                        {"option": "MM-SEAL", "description": "Lid seal for MilkMate"},
                        {"option": "none", "description": "None of these parts"},
                    ],
                },
            ],
        },
    ],
}))
```

```typescript
// Each answer is a full decision. Failed ones (ok is false) are left out: treat a missing id as "not sure".
const answers = (out: any, group = 0): Record<string, any> =>
  Object.fromEntries(out.results[group].answers.filter((a: any) => a.ok).map((a: any) => [a.result.id, a.result.result]));

const a = answers(await sage("/decide/batch", {
  requests: [
    {
      content: message,
      questions: [
        {
          id: "intent",
          kind: "choice",
          instructions: "What does the customer want?",
          options: [
            { option: "spare_part", description: "A replacement part" },
            { option: "order_status", description: "To know where an order is" },
            { option: "refund", description: "Their money back" },
            { option: "how_to", description: "Help using a product" },
          ],
        },
        {
          id: "sku",
          kind: "choice",
          instructions: "Which of these spare parts does the message need? Choose none if no part on the list fits.",
          options: [
            { option: "CP3-STEAM", description: "Steam wand for Crema Pro 3" },
            { option: "CP3-TRAY", description: "Drip tray for Crema Pro 3" },
            { option: "CM-JUG", description: "Milk jug for Crema Mini" },
            { option: "BR2-LID", description: "Hopper lid for Burrly 2" },
            { option: "MM-SEAL", description: "Lid seal for MilkMate" },
            { option: "none", description: "None of these parts" },
          ],
        },
      ],
    },
  ],
}));
```

Response, abridged (a real run):

```json
{
  "results": [
    {
      "answers": [
        {
          "ok": true,
          "result": {
            "id": "intent",
            "kind": "choice",
            "result": {
              "chosen": "spare_part",
              "probability": 0.9998,
              "probabilities": [
                {
                  "option": "spare_part",
                  "probability": 0.9998
                },
                {
                  "option": "order_status",
                  "probability": 0.0001
                },
                {
                  "option": "refund",
                  "probability": 0
                },
                {
                  "option": "how_to",
                  "probability": 0
                }
              ]
            },
            "meta": {
              "model": "levanto-sage-2"
            }
          },
          "error": null
        },
        {
          "ok": true,
          "result": {
            "id": "sku",
            "kind": "choice",
            "result": {
              "chosen": "BR2-LID",
              "probability": 0.9999,
              "probabilities": [
                {
                  "option": "CP3-STEAM",
                  "probability": 0
                },
                {
                  "option": "CP3-TRAY",
                  "probability": 0
                },
                {
                  "option": "CM-JUG",
                  "probability": 0
                },
                {
                  "option": "BR2-LID",
                  "probability": 0.9999
                },
                {
                  "option": "MM-SEAL",
                  "probability": 0
                },
                {
                  "option": "none",
                  "probability": 0
                }
              ]
            },
            "meta": {
              "model": "levanto-sage-2"
            }
          },
          "error": null
        }
      ]
    }
  ],
  "meta": {
    "model": "levanto-sage-2",
    "request_count": 1,
    "question_count": 2,
    "latency_ms": 144.4,
    "usage": {
      "billed_input_tokens": 263,
      "image_count": 0,
      "image_tokens": 0,
      "input_tokens": 263,
      "output_tokens": 0
    }
  }
}
```

### 3. Fill the cart, or hand it over

A spare-part request whose SKU clears your bar (0.9 here) goes straight to the cart. If the order number is missing, the reply asks for it.

Everything else **goes to a person**: `none`, a weak match, and a `null` when Sage2 isn't sure.

```python
BAR = 0.9  # your bar: test it on past messages
intent = a.get("intent", {}).get("chosen")
part = a.get("sku", {})
sku = part.get("chosen") if (part.get("probability") or 0) >= BAR else None

if intent == "spare_part" and sku not in (None, "none"):
    if found["order_number"]:
        print(f"Order {found['order_number']}: add {sku} to the cart and reply")
    else:
        print(f"Add {sku} to the cart and ask for the order number")
elif intent == "spare_part" and sku == "none":
    print("Hand it to a person: the part isn't in the catalog")
else:
    print("Hand it to a person:", intent or "intent unclear")
```

```typescript
const BAR = 0.9;  // your bar: test it on past messages
const intent = a.intent?.chosen ?? null;
const part = a.sku ?? {};
const sku = (part.probability ?? 0) >= BAR ? part.chosen : null;

if (intent === "spare_part" && sku !== null && sku !== "none") {
  if (found.order_number) {
    console.log(`Order ${found.order_number}: add ${sku} to the cart and reply`);
  } else {
    console.log(`Add ${sku} to the cart and ask for the order number`);
  }
} else if (intent === "spare_part" && sku === "none") {
  console.log("Hand it to a person: the part isn't in the catalog");
} else {
  console.log("Hand it to a person:", intent ?? "intent unclear");
}
```

## Before you ship

- **Confirm the part fits.** A vague message, such as "my machine is broken", can still match a part with a high `probability`. Check in code that the part fits the customer's product and the order exists before you send a payment link.
- **Set your own bar.** Test it on a few hundred past messages. Fill the cart only above a `probability` you trust; a person checks the rest.
- **Build `options` from your product database**, so a new part works without changing any code.
- **Fast and small.** Each of the two calls answers in about 0.1 s and reads a few hundred tokens.

## Full recipe (Python, `pip install requests`)

```python
import os
import requests

def sage(path, body):
    r = requests.post(
        "https://sage.levanto.ai" + path,
        headers={"Authorization": f"Bearer {os.environ['SAGE_API_KEY']}"},
        json=body,
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

message = "Hi, order NB-48213. I dropped my Burrly 2 and the hopper lid cracked. Can I buy just the lid? Please don't make me buy another grinder."

found = sage("/v1/extract", {
    "content": message,
    "extract": {
        "order_number": "the order number",
        "product": "the product the customer owns",
        "problem": "what went wrong",
    },
})["values"]

def answers(out, group=0):
    # Each answer is a full decision. Failed ones (ok is false) are left out: treat a missing id as "not sure".
    return {a["result"]["id"]: a["result"]["result"] for a in out["results"][group]["answers"] if a["ok"]}

a = answers(sage("/decide/batch", {
    "requests": [
        {
            "content": message,
            "questions": [
                {
                    "id": "intent",
                    "kind": "choice",
                    "instructions": "What does the customer want?",
                    "options": [
                        {"option": "spare_part", "description": "A replacement part"},
                        {
                            "option": "order_status",
                            "description": "To know where an order is",
                        },
                        {"option": "refund", "description": "Their money back"},
                        {"option": "how_to", "description": "Help using a product"},
                    ],
                },
                {
                    "id": "sku",
                    "kind": "choice",
                    "instructions": "Which of these spare parts does the message need? Choose none if no part on the list fits.",
                    "options": [
                        {"option": "CP3-STEAM", "description": "Steam wand for Crema Pro 3"},
                        {"option": "CP3-TRAY", "description": "Drip tray for Crema Pro 3"},
                        {"option": "CM-JUG", "description": "Milk jug for Crema Mini"},
                        {"option": "BR2-LID", "description": "Hopper lid for Burrly 2"},
                        {"option": "MM-SEAL", "description": "Lid seal for MilkMate"},
                        {"option": "none", "description": "None of these parts"},
                    ],
                },
            ],
        },
    ],
}))

BAR = 0.9  # your bar: test it on past messages
intent = a.get("intent", {}).get("chosen")
part = a.get("sku", {})
sku = part.get("chosen") if (part.get("probability") or 0) >= BAR else None

if intent == "spare_part" and sku not in (None, "none"):
    if found["order_number"]:
        print(f"Order {found['order_number']}: add {sku} to the cart and reply")
    else:
        print(f"Add {sku} to the cart and ask for the order number")
elif intent == "spare_part" and sku == "none":
    print("Hand it to a person: the part isn't in the catalog")
else:
    print("Hand it to a person:", intent or "intent unclear")
```

## Full recipe (TypeScript, Node 22+, nothing to install: save as `recipe.mts`, run `npx tsx recipe.mts`)

```typescript
// Node 22+. Save as recipe.mts and run npx tsx recipe.mts
async function sage(path: string, body: unknown): Promise<any> {
  const r = await fetch("https://sage.levanto.ai" + path, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.SAGE_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(30_000),
  });
  if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
  return r.json();
}

const message = "Hi, order NB-48213. I dropped my Burrly 2 and the hopper lid cracked. Can I buy just the lid? Please don't make me buy another grinder.";

const found = (await sage("/v1/extract", {
  content: message,
  extract: {
    order_number: "the order number",
    product: "the product the customer owns",
    problem: "what went wrong",
  },
})).values;

// Each answer is a full decision. Failed ones (ok is false) are left out: treat a missing id as "not sure".
const answers = (out: any, group = 0): Record<string, any> =>
  Object.fromEntries(out.results[group].answers.filter((a: any) => a.ok).map((a: any) => [a.result.id, a.result.result]));

const a = answers(await sage("/decide/batch", {
  requests: [
    {
      content: message,
      questions: [
        {
          id: "intent",
          kind: "choice",
          instructions: "What does the customer want?",
          options: [
            { option: "spare_part", description: "A replacement part" },
            { option: "order_status", description: "To know where an order is" },
            { option: "refund", description: "Their money back" },
            { option: "how_to", description: "Help using a product" },
          ],
        },
        {
          id: "sku",
          kind: "choice",
          instructions: "Which of these spare parts does the message need? Choose none if no part on the list fits.",
          options: [
            { option: "CP3-STEAM", description: "Steam wand for Crema Pro 3" },
            { option: "CP3-TRAY", description: "Drip tray for Crema Pro 3" },
            { option: "CM-JUG", description: "Milk jug for Crema Mini" },
            { option: "BR2-LID", description: "Hopper lid for Burrly 2" },
            { option: "MM-SEAL", description: "Lid seal for MilkMate" },
            { option: "none", description: "None of these parts" },
          ],
        },
      ],
    },
  ],
}));

const BAR = 0.9;  // your bar: test it on past messages
const intent = a.intent?.chosen ?? null;
const part = a.sku ?? {};
const sku = (part.probability ?? 0) >= BAR ? part.chosen : null;

if (intent === "spare_part" && sku !== null && sku !== "none") {
  if (found.order_number) {
    console.log(`Order ${found.order_number}: add ${sku} to the cart and reply`);
  } else {
    console.log(`Add ${sku} to the cart and ask for the order number`);
  }
} else if (intent === "spare_part" && sku === "none") {
  console.log("Hand it to a person: the part isn't in the catalog");
} else {
  console.log("Hand it to a person:", intent ?? "intent unclear");
}
```

## Requests

### 1. POST /v1/extract

```bash
curl -s https://sage.levanto.ai/v1/extract \
  -H "Authorization: Bearer $SAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<'JSON'
{
  "content": "Hi, order NB-48213. I dropped my Burrly 2 and the hopper lid cracked. Can I buy just the lid? Please don't make me buy another grinder.",
  "extract": {
    "order_number": "the order number",
    "product": "the product the customer owns",
    "problem": "what went wrong"
  }
}
JSON
```

Response, abridged (a real run):

```json
{
  "values": {
    "order_number": "NB-48213",
    "product": "Burrly 2",
    "problem": "hopper lid cracked"
  },
  "results": {
    "order_number": {
      "value": "NB-48213",
      "text": "NB-48213",
      "start": 10,
      "end": 18,
      "probability": 0.9815,
      "probabilities": [
        {
          "value": "NB-48213",
          "text": "NB-48213",
          "start": 10,
          "end": 18,
          "probability": 0.9815
        },
        {
          "value": null,
          "probability": 0.0185
        }
      ]
    },
    "product": {
      "value": "Burrly 2",
      "text": "Burrly 2",
      "start": 33,
      "end": 41,
      "probability": 0.9988,
      "probabilities": [
        {
          "value": "Burrly 2",
          "text": "Burrly 2",
          "start": 33,
          "end": 41,
          "probability": 0.9988
        },
        {
          "value": null,
          "probability": 0.0012
        }
      ]
    },
    "…": "one entry per field"
  },
  "meta": {
    "model": "levanto-sage-2",
    "usage": {
      "input_tokens": 218,
      "output_tokens": 314
    },
    "latency_ms": 731.6
  }
}
```

### 2. POST /decide/batch

```bash
curl -s https://sage.levanto.ai/decide/batch \
  -H "Authorization: Bearer $SAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<'JSON'
{
  "requests": [
    {
      "content": "Hi, order NB-48213. I dropped my Burrly 2 and the hopper lid cracked. Can I buy just the lid? Please don't make me buy another grinder.",
      "questions": [
        {
          "id": "intent",
          "kind": "choice",
          "instructions": "What does the customer want?",
          "options": [
            {
              "option": "spare_part",
              "description": "A replacement part"
            },
            {
              "option": "order_status",
              "description": "To know where an order is"
            },
            {
              "option": "refund",
              "description": "Their money back"
            },
            {
              "option": "how_to",
              "description": "Help using a product"
            }
          ]
        },
        {
          "id": "sku",
          "kind": "choice",
          "instructions": "Which of these spare parts does the message need? Choose none if no part on the list fits.",
          "options": [
            {
              "option": "CP3-STEAM",
              "description": "Steam wand for Crema Pro 3"
            },
            {
              "option": "CP3-TRAY",
              "description": "Drip tray for Crema Pro 3"
            },
            {
              "option": "CM-JUG",
              "description": "Milk jug for Crema Mini"
            },
            {
              "option": "BR2-LID",
              "description": "Hopper lid for Burrly 2"
            },
            {
              "option": "MM-SEAL",
              "description": "Lid seal for MilkMate"
            },
            {
              "option": "none",
              "description": "None of these parts"
            }
          ]
        }
      ]
    }
  ]
}
JSON
```

Response, abridged (a real run):

```json
{
  "results": [
    {
      "answers": [
        {
          "ok": true,
          "result": {
            "id": "intent",
            "kind": "choice",
            "result": {
              "chosen": "spare_part",
              "probability": 0.9998,
              "probabilities": [
                {
                  "option": "spare_part",
                  "probability": 0.9998
                },
                {
                  "option": "order_status",
                  "probability": 0.0001
                },
                {
                  "option": "refund",
                  "probability": 0
                },
                {
                  "option": "how_to",
                  "probability": 0
                }
              ]
            },
            "meta": {
              "model": "levanto-sage-2"
            }
          },
          "error": null
        },
        {
          "ok": true,
          "result": {
            "id": "sku",
            "kind": "choice",
            "result": {
              "chosen": "BR2-LID",
              "probability": 0.9999,
              "probabilities": [
                {
                  "option": "CP3-STEAM",
                  "probability": 0
                },
                {
                  "option": "CP3-TRAY",
                  "probability": 0
                },
                {
                  "option": "CM-JUG",
                  "probability": 0
                },
                {
                  "option": "BR2-LID",
                  "probability": 0.9999
                },
                {
                  "option": "MM-SEAL",
                  "probability": 0
                },
                {
                  "option": "none",
                  "probability": 0
                }
              ]
            },
            "meta": {
              "model": "levanto-sage-2"
            }
          },
          "error": null
        }
      ]
    }
  ],
  "meta": {
    "model": "levanto-sage-2",
    "request_count": 1,
    "question_count": 2,
    "latency_ms": 144.4,
    "usage": {
      "billed_input_tokens": 263,
      "image_count": 0,
      "image_tokens": 0,
      "input_tokens": 263,
      "output_tokens": 0
    }
  }
}
```

## Docs

- Extract: https://docs.levanto.ai/extract
- Choice: https://docs.levanto.ai/decision-model/choice
- Limits and errors: https://docs.levanto.ai/decision-model/limits
- OpenAPI: https://sage.levanto.ai/openapi.json
