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:
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:
{"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:
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:
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:
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.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Prompt Engineering Examples: 12 Ready-Made Templates for WorkTwelve prompt templates for everyday tasks: what each one says, why it is built that way, and what a single API call costs.