STRIKE / BLOG

LLM classification API: route support tickets with choice questions (Python & JS)

Route support tickets with a typed classification API: one choice question for the team, one yes/no for urgency, and a probability for every option. Full Python and JavaScript examples.

LLM classification API: route support tickets with choice questions (Python & JS)

Ticket routing is a classic classification problem. A message comes in, and you need to decide which team gets it, how urgent it is and whether a human should look first. Training a dedicated classifier means collecting labelled data and retraining every time your categories change. Prompting a general chat model means parsing text and guessing how confident it was.

A typed classification API sits in between. You describe your categories in plain language, and you get back the chosen label plus a probability for every label. There's no training step, and no text to parse.

This guide builds a small ticket router with Strike in Python and JavaScript.

Preview: Strike's API is currently in mock mode and returns placeholder answers. The code below is the real integration. Only the answer values will change when the model goes live.

The plan

For each ticket we ask three questions in a single request:

| Key | Type | Purpose | |---|---|---| | team | choice | billing, technical, account or other | | is_urgent | noul | yes/no: does the customer need help now? | | frustration | score | calm, frustrated, very angry |

Then we apply our own routing policy to the probabilities.

The request

{
  "model": "strike-1",
  "state": {
    "subject": "Can't log in after password reset",
    "body": "I reset my password twice and still get 'invalid credentials'. Our team demo is in an hour."
  },
  "questions": {
    "team": {
      "type": "choice",
      "instructions": "Which team should handle this ticket?",
      "criteria": {
        "billing": "Payments, refunds, invoices, plan changes",
        "technical": "Bugs, errors, outages, integrations",
        "account": "Login, passwords, user management, permissions",
        "other": null
      }
    },
    "is_urgent": {
      "type": "noul",
      "instructions": "Does the customer need a response within the next few hours?",
      "criteria": {
        "true": "Explicit deadline or service blocked",
        "false": "No time pressure expressed"
      }
    },
    "frustration": {
      "type": "score",
      "instructions": "How frustrated is the customer?",
      "criteria": ["Calm", "Frustrated", "Very angry"]
    }
  }
}

A few tips on writing criteria:

  • Describe each option in terms of the content, not your org chart. "Login, passwords" beats "Team B".
  • Include an other option. It gives the model somewhere to put tickets that don't fit, rather than forcing a bad match.
  • Keep options mutually exclusive where you can. Overlapping categories spread probability between them.

Python

import os
import requests

STRIKE_URL = "https://api.strike-ai.eu/v1/systemone"
HEADERS = {"Authorization": f"Bearer {os.environ['STRIKE_API_KEY']}"}

QUESTIONS = {
    "team": {
        "type": "choice",
        "instructions": "Which team should handle this ticket?",
        "criteria": {
            "billing": "Payments, refunds, invoices, plan changes",
            "technical": "Bugs, errors, outages, integrations",
            "account": "Login, passwords, user management, permissions",
            "other": None,
        },
    },
    "is_urgent": {
        "type": "noul",
        "instructions": "Does the customer need a response within the next few hours?",
    },
    "frustration": {
        "type": "score",
        "instructions": "How frustrated is the customer?",
        "criteria": ["Calm", "Frustrated", "Very angry"],
    },
}

# Your policy, not the model's. Tune these on your own data.
MIN_TEAM_PROBABILITY = 0.8
URGENT_THRESHOLD = 0.7


def classify(ticket: dict) -> dict:
    resp = requests.post(
        STRIKE_URL,
        headers=HEADERS,
        json={"model": "strike-1", "state": ticket, "questions": QUESTIONS},
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()["answers"]


def route(ticket: dict) -> dict:
    a = classify(ticket)
    team = a["team"]["choice"]
    team_p = a["team"]["probabilities"][team]

    return {
        "queue": team if team_p >= MIN_TEAM_PROBABILITY and team != "other" else "triage",
        "priority": "high" if a["is_urgent"]["noul"] >= URGENT_THRESHOLD else "normal",
        "frustration": a["frustration"]["score"],  # 0 = calm ... 2 = very angry
        "probabilities": a["team"]["probabilities"],  # keep for auditing
    }


if __name__ == "__main__":
    print(route({
        "subject": "Can't log in after password reset",
        "body": "I reset my password twice and still get 'invalid credentials'.",
    }))

JavaScript / TypeScript

const STRIKE_URL = "https://api.strike-ai.eu/v1/systemone";

const questions = {
  team: {
    type: "choice",
    instructions: "Which team should handle this ticket?",
    criteria: {
      billing: "Payments, refunds, invoices, plan changes",
      technical: "Bugs, errors, outages, integrations",
      account: "Login, passwords, user management, permissions",
      other: null,
    },
  },
  is_urgent: {
    type: "noul",
    instructions: "Does the customer need a response within the next few hours?",
  },
  frustration: {
    type: "score",
    instructions: "How frustrated is the customer?",
    criteria: ["Calm", "Frustrated", "Very angry"],
  },
};

const MIN_TEAM_PROBABILITY = 0.8; // tune on your own data
const URGENT_THRESHOLD = 0.7;

export async function route(ticket: { subject: string; body: string }) {
  const res = await fetch(STRIKE_URL, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.STRIKE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ model: "strike-1", state: ticket, questions }),
  });
  if (!res.ok) throw new Error(`Strike error ${res.status}: ${await res.text()}`);
  const { answers } = await res.json();

  const team: string = answers.team.choice;
  const teamP: number = answers.team.probabilities[team];

  return {
    queue: teamP >= MIN_TEAM_PROBABILITY && team !== "other" ? team : "triage",
    priority: answers.is_urgent.noul >= URGENT_THRESHOLD ? "high" : "normal",
    frustration: answers.frustration.score,
    probabilities: answers.team.probabilities,
  };
}

What comes back

{
  "model": "strike-1",
  "answers": {
    "team": {
      "type": "choice",
      "choice": "account",
      "probabilities": { "account": 0.85, "technical": 0.1, "billing": 0.03, "other": 0.02 },
      "confidence": 0.75
    },
    "is_urgent": { "type": "noul", "noul": 0.9 },
    "frustration": {
      "type": "score",
      "score": 1.1,
      "legend": { "0": "Calm", "1": "Frustrated", "2": "Very angry" },
      "probabilities": { "0": 0.1, "1": 0.7, "2": 0.2 },
      "confidence": 0.5
    }
  }
}

(Values are illustrative.)

Choosing thresholds

The thresholds above are placeholders. To set real ones:

  1. Run a few hundred past tickets, with the team that actually resolved them, through the router.
  2. For each candidate threshold, measure how many tickets are auto-routed and how many of those went to the right team.
  3. Pick the point where the error rate is acceptable for your support org, and send the rest to triage.

Re-check the thresholds when you change your categories or criteria, or when you switch model versions.

Handling errors

  • 401: check the Authorization: Bearer header.
  • 422: the request failed validation. The response body names the field.
  • 429: you hit a rate limit. Retry with exponential backoff.

Next steps

← All posts · Home