Classify customer inquiries with AI: Python example

Support tickets, complaints, feature requests — some go to tech, some to product, some to the archive. Here's how an AI model routes them.

3 min · · development · бизнес

Classifying support tickets by hand is drudgery: support issues, complaints, feature requests, spam, and each message has to land in the right queue. An AI model via API does it automatically for a fraction of a cent per inquiry. Below is working Python code, batch processing, a review queue for unclear cases, and a way to measure accuracy on your own data.

Why classify inquiries

Typical categories:

  • Technical problem: error, bug, a feature not working.
  • Product question: how to use it, where to find a setting.
  • Feature request: the user asks for a new capability.
  • Complaint: the user is unhappy.
  • Spam: ads, phishing, nonsense.
  • Other: anything that doesn't fit the above.

Each category goes to its own team: support, product managers, moderators. With hundreds of messages a day, manual sorting doesn't scale.

Quick solution with a cheap model

Start with the cheapest model on Zukko — glm-5.3-flash ($0.04/$0.04 per 1M tokens). The call goes through the OpenAI-compatible API:

bash
import json
from openai import OpenAI

client = OpenAI(
    base_url="https://api.zukko.pro/v1",
    api_key="fetch_your_key",
)

CATEGORIES = [
    "technical_problem",
    "product_question",
    "feature_request",
    "complaint",
    "spam",

Sample response:

bash
{"category": "technical_problem", "confidence": 0.9, "reasoning": "user cannot authenticate"}

Note the two safeguards: a response that doesn't parse as JSON and a category outside the list both get confidence 0 and go to a human in the next step.

Batch processing: dozens of inquiries per request

With a lot of inquiries, you can send them in batches of 20–50: the system prompt is sent once per batch, not once per message. To keep answers from getting mixed up, ask the model to return the inquiry number:

bash
def classify_batch(inquiries: list[str], model: str = "glm-5.3-flash") -> dict[int, dict]:
    """Classify a batch of inquiries, return {number: result}"""
    batch_text = "\n\n".join(f"Inquiry {i}:\n{text}" for i, text in enumerate(inquiries, 1))

    system_prompt_batch = f"""You are an inquiry classifier. Categories: {', '.join(CATEGORIES)}
For EACH inquiry output one line of JSON, no markdown:
{{"id": <inquiry number>, "category": "...", "confidence": 0.9}}
One line per object, no extra text.
"""
    response = client.chat.completions.create(
        model=model,
        max_tokens=60 * len(inquiries) + 100,
        messages=[
            {"role": "system", "content": system_prompt_batch},

The bigger the batch, the higher the chance the model skips a line or shifts numbers, which is why gaps are filled in one at a time.

Unclear cases go to a human

If confidence is low, the inquiry goes to an operator queue:

bash
ROUTING = {
    "technical_problem": "support-tech",
    "product_question": "support-general",
    "feature_request": "product-team",
    "complaint": "customer-success",
    "spam": "moderation",
    "other": "triage",
}

def route_inquiry(text: str, threshold: float = 0.7) -> dict:
    """Route an inquiry automatically or to a human"""
    result = classify_inquiry(text)
    confidence = float(result.get("confidence", 0))
    if confidence < threshold:

The model sets confidence itself; it is not a calibrated probability. Pick the threshold using a labeled sample: find the value at which errors in the automatic flow become acceptable for you.

Measure accuracy on labeled data

Without a test set you can't tell how well the classifier works. Take 100–200 historical inquiries with correct categories:

bash
from collections import defaultdict

def measure_accuracy(test_data: list[dict], model: str = "glm-5.3-flash") -> dict:
    """test_data: [{"text": "...", "true_category": "complaint"}, ...]"""
    correct = 0
    per_category = defaultdict(lambda: {"ok": 0, "total": 0})
    for item in test_data:
        predicted = classify_inquiry(item["text"], model)["category"]
        true = item["true_category"]
        per_category[true]["total"] += 1
        if predicted == true:
            correct += 1
            per_category[true]["ok"] += 1
    return {

The same script answers "which model should I use": run the sample on several models and compare accuracy and cost.

Model choice and cost

Zukko prices per 1M tokens (input/output): glm-5.3-flash — $0.04/$0.04, glm-5.3 and gpt-5.6-luna — $0.10/$0.10, claude-sonnet-5 and gpt-5.6-sol — $0.30/$0.30. A practical setup: the cheap model handles the whole flow, and low-confidence inquiries get a second pass on a pricier model if your sample shows it does noticeably better.

Estimate for 1,000 inquiries per day, ~300 input and ~50 output tokens per inquiry, with 100 unclear ones re-checked:

glm-5.3-flash: 1000 × (300 + 50) × 0.04 / 1M = $0.014 per day claude-sonnet-5: 100 × (300 + 50) × 0.30 / 1M = $0.0105 per day Total: $0.0245 per day ≈ $0.74 per month for ~30k inquiries

Current model list: GET https://api.zukko.pro/v1/models or the Models page in your dashboard.

Set up in the dashboard

Create a key in the Zukko dashboard and leave only the models the classifier uses in the allowed list. A daily dollar limit protects you from an accidental loop in your code. See the Python examples and quick start.

FAQ

What if I have more than six categories?

Add them to the list and the prompt, then re-check accuracy on your sample. If similar categories start getting confused, use a hierarchy: first 3–4 broad groups, then a second request to refine within the group.

Can I skip JSON?

You can ask for a single word — the category name — but JSON is more convenient: it carries confidence and a reason, and an invalid response is easy to catch and send to a human.

Should I cache the system prompt?

In Zukko, cached tokens are billed at the regular input price, so the savings come from batching and a short system prompt rather than from caching. See the limits and cache docs.

Why not a classic model like BERT?

It works, but needs labeled data and training. An LLM via API can be used right away, and you can train a classic classifier later once you have enough labels.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Customer inquiry classification with AI via API · Zukko