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

# Customer support bot

> Build a multi-turn support bot on the gateway with tool calling. The model looks up orders when it needs to.

A support bot needs a natural back-and-forth with the user and the ability to look things up in your systems. Here's one built on the [gateway](/build/gateway/drop-in-sdks) with tool calling: it asks for an order ID and email, then calls a `get_order_status` tool to answer.

<div className="opp-term" style={{ borderRadius: "12px", overflow: "hidden", border: "1px solid #232a33", fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: "13.5px", lineHeight: "1.65", boxShadow: "0 8px 30px rgba(0,0,0,0.35)", margin: "1.5em 0" }}>
  <div className="opp-head" style={{ display: "flex", alignItems: "center", gap: "7px", padding: "11px 14px", background: "#11161c", borderBottom: "1px solid #232a33" }}>
    <span style={{ width: "11px", height: "11px", borderRadius: "50%", background: "#ff5f56", display: "inline-block" }} />

    <span style={{ width: "11px", height: "11px", borderRadius: "50%", background: "#ffbd2e", display: "inline-block" }} />

    <span style={{ width: "11px", height: "11px", borderRadius: "50%", background: "#27c93f", display: "inline-block" }} />

    <span style={{ marginLeft: "10px", color: "#6e7681", fontSize: "12px" }}>python app.py</span>
  </div>

  <div className="opp-body" style={{ padding: "16px 18px", background: "#0b0f14", color: "#c9d1d9" }}>
    <p data-role="user" style={{ margin: "0 0 12px" }}><span style={{ color: "#58a6ff", fontWeight: 600 }}>You ›</span> Hi, where is my order?</p>
    <p data-role="bot" style={{ margin: "0 0 18px" }}><span style={{ color: "#14cdcd", fontWeight: 600 }}>Bot ›</span> I can check that. Please share your order ID and the email you used to place the order.</p>
    <p data-role="user" style={{ margin: "0 0 12px" }}><span style={{ color: "#58a6ff", fontWeight: 600 }}>You ›</span> Order 123123, email [santa@clau.se](mailto:santa@clau.se)</p>
    <p data-role="tool" style={{ margin: "0 0 18px" }}><span style={{ color: "#e3b341", fontWeight: 600 }}>Tool ›</span> get\_order\_status(order\_id="123123", email="[santa@clau.se](mailto:santa@clau.se)")<br /><span style={{ color: "#6e7681" }}>→ found · out for delivery · ETA 2024-11-08</span></p>
    <p data-role="bot" style={{ margin: "0 0 18px" }}><span style={{ color: "#14cdcd", fontWeight: 600 }}>Bot ›</span> Your order 123123 is out for delivery, arriving around 2024-11-08 to Snowy Mountain 123, Greenland. Items: Large wooden sled and Reindeer harness (x2).</p>
    <p data-role="user" style={{ margin: "0 0 12px" }}><span style={{ color: "#58a6ff", fontWeight: 600 }}>You ›</span> Great, what did I actually order?</p>
    <p data-role="bot" style={{ margin: 0 }}><span style={{ color: "#14cdcd", fontWeight: 600 }}>Bot ›</span> Your order 123123 contains a Large wooden sled and a Reindeer harness (x2).<span className="opp-cursor" /></p>
  </div>
</div>

The bot asks for the ID and email, looks the order up once it has them, and still knows the contents on the next turn because the history is kept. Here's the whole thing.

## The bot

<CodeGroup>
  ```python app.py theme={null}
  import os, json
  from openai import OpenAI

  client = OpenAI(base_url="https://api.opper.ai/v3/compat", api_key=os.environ["OPPER_API_KEY"])

  # A tiny order "database"
  ORDERS = {
      "123123": {
          "email": "santa@clau.se",
          "status": "out for delivery",
          "eta": "2024-11-08",
          "items": ["Large wooden sled", "Reindeer harness (x2)"],
          "address": "Snowy Mountain 123, Greenland",
      }
  }

  def get_order_status(order_id: str, email: str) -> dict:
      order = ORDERS.get(order_id)
      if not order or order["email"].lower() != email.lower():
          return {"found": False, "reason": "No order matches that ID and email."}
      return {"found": True, **{k: order[k] for k in ("status", "eta", "items", "address")}}

  tools = [{
      "type": "function",
      "function": {
          "name": "get_order_status",
          "description": "Look up an order's status and contents by order ID and the email used to place it.",
          "parameters": {
              "type": "object",
              "properties": {
                  "order_id": {"type": "string"},
                  "email": {"type": "string"},
              },
              "required": ["order_id", "email"],
          },
      },
  }]

  system = (
      "You are a friendly customer support agent for a sleigh supply store. "
      "Use the get_order_status tool to answer questions about orders. "
      "Ask for the order ID and email if you need them. Keep replies short."
  )

  def reply(messages):
      # Loop until the model answers in text instead of calling a tool.
      while True:
          r = client.chat.completions.create(model="openai/gpt-5-mini", messages=messages, tools=tools)
          msg = r.choices[0].message
          messages.append(msg)
          if not msg.tool_calls:
              return msg.content
          for call in msg.tool_calls:
              args = json.loads(call.function.arguments)
              result = get_order_status(**args)
              messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})

  messages = [{"role": "system", "content": system}]
  while True:
      user = input("User: ")
      if user.lower() in {"quit", "exit"}:
          break
      messages.append({"role": "user", "content": user})
      print("Assistant:", reply(messages))
  ```

  ```typescript app.ts theme={null}
  import OpenAI from "openai";
  import readline from "node:readline/promises";

  const client = new OpenAI({ baseURL: "https://api.opper.ai/v3/compat", apiKey: process.env.OPPER_API_KEY });

  // A tiny order "database"
  const ORDERS: Record<string, any> = {
    "123123": {
      email: "santa@clau.se",
      status: "out for delivery",
      eta: "2024-11-08",
      items: ["Large wooden sled", "Reindeer harness (x2)"],
      address: "Snowy Mountain 123, Greenland",
    },
  };

  function getOrderStatus(orderId: string, email: string) {
    const o = ORDERS[orderId];
    if (!o || o.email.toLowerCase() !== email.toLowerCase())
      return { found: false, reason: "No order matches that ID and email." };
    const { status, eta, items, address } = o;
    return { found: true, status, eta, items, address };
  }

  const tools = [{
    type: "function" as const,
    function: {
      name: "get_order_status",
      description: "Look up an order's status and contents by order ID and the email used to place it.",
      parameters: {
        type: "object",
        properties: { order_id: { type: "string" }, email: { type: "string" } },
        required: ["order_id", "email"],
      },
    },
  }];

  const system =
    "You are a friendly customer support agent for a sleigh supply store. " +
    "Use the get_order_status tool to answer questions about orders. " +
    "Ask for the order ID and email if you need them. Keep replies short.";

  async function reply(messages: any[]) {
    // Loop until the model answers in text instead of calling a tool.
    while (true) {
      const r = await client.chat.completions.create({ model: "openai/gpt-5-mini", messages, tools });
      const msg = r.choices[0].message;
      messages.push(msg);
      if (!msg.tool_calls) return msg.content;
      for (const call of msg.tool_calls) {
        const args = JSON.parse(call.function.arguments);
        const result = getOrderStatus(args.order_id, args.email);
        messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });
      }
    }
  }

  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  const messages: any[] = [{ role: "system", content: system }];
  while (true) {
    const user = await rl.question("User: ");
    if (["quit", "exit"].includes(user.toLowerCase())) break;
    messages.push({ role: "user", content: user });
    console.log("Assistant:", await reply(messages));
  }
  ```
</CodeGroup>

Run it:

<CodeGroup>
  ```bash Python theme={null}
  pip install openai
  export OPPER_API_KEY="your-api-key"
  python app.py
  ```

  ```bash TypeScript theme={null}
  npm install openai
  export OPPER_API_KEY="your-api-key"
  npx tsx app.ts
  ```
</CodeGroup>

## How it works

* The `tools` array describes `get_order_status` to the model. It calls the tool only when it needs order data, and asks for the ID and email first when they're missing.
* The `reply` loop runs the model, executes any tool call, feeds the result back, and repeats until the model returns plain text. That's the standard [tool round trip](/build/gateway/tools).
* The whole conversation, including the system prompt and tool results, lives in `messages`, so the bot remembers the order across turns. See [Conversations](/build/gateway/conversations).
* Swap `model` for any of the [300+ models](/capabilities/models) without touching the rest of the code.

<Tip>
  Every call is traced in the platform. Add an [Observe](/control-plane/observe) rule on this bot to score replies and catch regressions.
</Tip>

## What's next

<CardGroup cols={2}>
  <Card title="Tool calling" icon="screwdriver-wrench" href="/build/gateway/tools">
    The full tool-use round trip.
  </Card>

  <Card title="Conversations" icon="comments" href="/build/gateway/conversations">
    Multi-turn chat and message history.
  </Card>

  <Card title="Guard" icon="shield" href="/control-plane/guard">
    Block or redact sensitive content before it reaches the model.
  </Card>

  <Card title="Observe" icon="eye" href="/control-plane/observe">
    Score every reply and watch quality over time.
  </Card>
</CardGroup>
