Skip to main content

Classify Customer Intent in Support Tickets with GLiClass

Automatically classify customer support tickets by intent using GLiClass, an open-source zero-shot classification framework that runs entirely on your local machine -- no API keys, no external services, no usage limits.

Overview

This cookbook shows how to build a complete intent classification pipeline for customer support tickets using GLiClass. You will:

  • Classify tickets into intent categories (billing, technical support, complaints, etc.) with zero training data
  • Handle multi-label scenarios where a single ticket spans multiple intents
  • Assess urgency and sentiment to calculate priority levels
  • Route tickets to the right teams and flag escalations
  • Process batches of tickets and export results to JSON

GLiClass uses a lightweight transformer model that runs locally on CPU or GPU, making it suitable for environments where data privacy matters or where you want predictable costs at any volume.

Installation

pip install gliclass torch transformers

Quick Start

Classify a support ticket in under 15 lines:

from gliclass import GLiClassModel, ZeroShotClassificationPipeline
from transformers import AutoTokenizer

model = GLiClassModel.from_pretrained("knowledgator/gliclass-base-v3.0")
tokenizer = AutoTokenizer.from_pretrained("knowledgator/gliclass-base-v3.0")
pipeline = ZeroShotClassificationPipeline(model, tokenizer, classification_type='multi-label', device='cpu')

ticket = "I've been charged twice for my subscription this month. Can you refund the duplicate?"
labels = ["billing inquiry", "technical issue", "account access problem", "feature request", "complaint"]

results = pipeline(ticket, labels, threshold=0.5)[0]

for r in results:
print(f"{r['label']}: {r['score']:.2f}")
# billing inquiry: 0.92

Define Intent Labels

The quality of zero-shot classification depends heavily on how you phrase your labels. Use clear, descriptive text that distinguishes each category from the others.

Basic Intent Categories

BASIC_INTENTS = [
"billing or payment inquiry",
"technical support issue",
"account management request",
"feature request or suggestion",
"complaint or negative feedback",
"general question or information request",
]

Detailed Intent Categories

DETAILED_INTENTS = [
# Billing
"payment failed or declined",
"invoice or receipt request",
"subscription upgrade or downgrade",
"refund request",
"pricing question",
# Technical
"bug report or error",
"product not working",
"integration or API issue",
"performance problem",
"data loss or corruption",
# Account
"password reset",
"account access issue",
"profile or settings update",
"user permissions change",
# Feedback
"feature request",
"product improvement suggestion",
"complaint about service",
"praise or positive feedback",
# Sales
"product information inquiry",
"demo or trial request",
"enterprise pricing question",
]

Industry-Specific Intent Sets

# E-commerce
ECOMMERCE_INTENTS = [
"order status inquiry",
"shipping or delivery issue",
"return or exchange request",
"product defect report",
"payment or checkout problem",
"product availability question",
"discount or coupon inquiry",
]

# SaaS
SAAS_INTENTS = [
"onboarding assistance",
"feature usage question",
"API or integration support",
"billing or subscription inquiry",
"bug or technical issue",
"feature request",
"account access problem",
"data export request",
]

# Financial Services
FINANCE_INTENTS = [
"transaction inquiry",
"fraud or security concern",
"account balance question",
"loan or credit inquiry",
"card replacement request",
"statement request",
"fee dispute",
]

Basic Intent Classification

A simple function that returns the top-scoring intent:

from gliclass import GLiClassModel, ZeroShotClassificationPipeline
from transformers import AutoTokenizer

model = GLiClassModel.from_pretrained("knowledgator/gliclass-base-v3.0")
tokenizer = AutoTokenizer.from_pretrained("knowledgator/gliclass-base-v3.0")
pipeline = ZeroShotClassificationPipeline(model, tokenizer, classification_type='multi-label', device='cpu')


def classify_intent(text: str, labels: list[str], threshold: float = 0.5) -> list[dict]:
"""Return all intents above the confidence threshold, sorted by score."""
results = pipeline(text, labels, threshold=threshold)[0]
return sorted(results, key=lambda r: r["score"], reverse=True)


def get_primary_intent(results: list[dict]) -> tuple[str, float]:
"""Return the single highest-scoring intent."""
if not results:
return ("unknown", 0.0)
return (results[0]["label"], results[0]["score"])


# Example
ticket = """
Hi, I've been charged twice for my subscription this month.
My credit card shows two transactions of $49.99 on the same day.
Can you please refund the duplicate charge?
"""

results = classify_intent(ticket, BASIC_INTENTS)
intent, confidence = get_primary_intent(results)
print(f"Primary intent: {intent} (confidence: {confidence:.2f})")
# Primary intent: billing or payment inquiry (confidence: 0.92)

Multi-Label Classification

Many support tickets express more than one intent. By using a lower threshold and returning multiple results, you can capture all of them:

def classify_multi_intent(
text: str,
labels: list[str],
threshold: float = 0.4,
max_intents: int = 3,
) -> list[dict]:
"""
Return up to max_intents intents that score above threshold.
"""
results = pipeline(text, labels, threshold=threshold)[0]
sorted_results = sorted(results, key=lambda r: r["score"], reverse=True)
return sorted_results[:max_intents]


# Example: a ticket with multiple intents
complex_ticket = """
Your app keeps crashing whenever I try to view my invoice.
I've been trying to download it for my expense report but can't.
Also, is there a way to export invoices automatically each month?
"""

intents = classify_multi_intent(complex_ticket, DETAILED_INTENTS)
print("Detected intents:")
for r in intents:
print(f" - {r['label']}: {r['score']:.2f}")

# Detected intents:
# - invoice or receipt request: 0.85
# - bug report or error: 0.78
# - feature request: 0.65

Urgency and Priority Classification

Beyond intent, you can classify urgency and sentiment to calculate an overall priority level:

URGENCY_LABELS = [
"critical - service completely down or major financial impact",
"high - significant functionality blocked",
"medium - inconvenient but workaround exists",
"low - general question or minor issue",
]

SENTIMENT_LABELS = [
"angry or frustrated customer",
"neutral tone",
"positive or patient customer",
]


def classify_urgency(text: str, threshold: float = 0.3) -> list[dict]:
"""Classify the urgency level of a ticket."""
return pipeline(text, URGENCY_LABELS, threshold=threshold)[0]


def classify_sentiment(text: str, threshold: float = 0.3) -> list[dict]:
"""Classify the customer sentiment in a ticket."""
return pipeline(text, SENTIMENT_LABELS, threshold=threshold)[0]


def get_top_label(results: list[dict]) -> str:
"""Return the label with the highest score, or 'unknown'."""
if not results:
return "unknown"
return max(results, key=lambda r: r["score"])["label"]


def calculate_priority(intent: str, urgency_results: list[dict], sentiment_results: list[dict]) -> str:
"""
Compute a P1-P4 priority from intent, urgency, and sentiment signals.
"""
priority_map = {"critical": 4, "high": 3, "medium": 2, "low": 1}

# Base score from urgency
top_urgency = get_top_label(urgency_results).lower()
base_score = 2
for keyword, score in priority_map.items():
if keyword in top_urgency:
base_score = score
break

# Boost for frustrated customers
top_sentiment = get_top_label(sentiment_results).lower()
if "angry" in top_sentiment or "frustrated" in top_sentiment:
base_score = min(base_score + 1, 4)

# Boost for inherently critical intents
critical_keywords = ["data loss", "security", "fraud", "payment failed"]
if any(kw in intent.lower() for kw in critical_keywords):
base_score = min(base_score + 1, 4)

labels = {4: "P1-Critical", 3: "P2-High", 2: "P3-Medium", 1: "P4-Low"}
return labels.get(base_score, "P3-Medium")


# Example
urgent_ticket = """
URGENT! Your system charged my card 5 times and now my account is overdrawn!
I've been trying to reach someone for hours. This is absolutely unacceptable.
I need this fixed IMMEDIATELY or I'm disputing all charges with my bank!
"""

intent_results = classify_intent(urgent_ticket, BASIC_INTENTS)
urgency_results = classify_urgency(urgent_ticket)
sentiment_results = classify_sentiment(urgent_ticket)

primary_intent = get_primary_intent(intent_results)[0]
priority = calculate_priority(primary_intent, urgency_results, sentiment_results)

print(f"Intent: {primary_intent}")
print(f"Priority: {priority}")
# Intent: billing or payment inquiry
# Priority: P1-Critical

Full Pipeline

The IntentClassificationPipeline class below loads the model once and provides methods for single-ticket classification, batch processing, routing, escalation, and JSON export.

from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
import json

from gliclass import GLiClassModel, ZeroShotClassificationPipeline
from transformers import AutoTokenizer


# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------

@dataclass
class TicketClassification:
"""Complete classification result for a single support ticket."""
ticket_id: str
text: str
primary_intent: str
intent_confidence: float
secondary_intents: list[dict]
priority: str
sentiment: str
assigned_team: str
requires_escalation: bool
classified_at: str

def to_dict(self) -> dict:
return {
"ticket_id": self.ticket_id,
"primary_intent": self.primary_intent,
"intent_confidence": round(self.intent_confidence, 4),
"secondary_intents": self.secondary_intents,
"priority": self.priority,
"sentiment": self.sentiment,
"assigned_team": self.assigned_team,
"requires_escalation": self.requires_escalation,
"classified_at": self.classified_at,
}


# ---------------------------------------------------------------------------
# Default configuration
# ---------------------------------------------------------------------------

DEFAULT_ROUTING_RULES: dict[str, str] = {
"billing or payment inquiry": "billing-team",
"technical support issue": "tech-support",
"account management request": "account-team",
"feature request or suggestion": "product-team",
"complaint or negative feedback": "escalations",
"general question or information request": "general-support",
}

BASIC_INTENTS = [
"billing or payment inquiry",
"technical support issue",
"account management request",
"feature request or suggestion",
"complaint or negative feedback",
"general question or information request",
]

URGENCY_LABELS = [
"critical - service completely down or major financial impact",
"high - significant functionality blocked",
"medium - inconvenient but workaround exists",
"low - general question or minor issue",
]

SENTIMENT_LABELS = [
"angry or frustrated customer",
"neutral tone",
"positive or patient customer",
]


# ---------------------------------------------------------------------------
# Pipeline
# ---------------------------------------------------------------------------

class IntentClassificationPipeline:
"""
End-to-end intent classification pipeline for customer support tickets.

Loads the GLiClass model once, then exposes methods for classifying
individual tickets or batches, routing them to teams, and exporting
results.
"""

def __init__(
self,
model_name: str = "knowledgator/gliclass-base-v3.0",
device: str = "cpu",
intent_labels: list[str] | None = None,
routing_rules: dict[str, str] | None = None,
intent_threshold: float = 0.5,
):
_model = GLiClassModel.from_pretrained(model_name)
_tokenizer = AutoTokenizer.from_pretrained(model_name)
self.pipeline = ZeroShotClassificationPipeline(
_model, _tokenizer, classification_type="multi-label", device=device,
)
self.intent_labels = intent_labels or BASIC_INTENTS
self.routing_rules = routing_rules or DEFAULT_ROUTING_RULES
self.intent_threshold = intent_threshold
self.classifications: list[TicketClassification] = []

# -- helpers ----------------------------------------------------------

def _top_label(self, results: list[dict]) -> str:
if not results:
return "unknown"
return max(results, key=lambda r: r["score"])["label"]

def _calculate_priority(
self, intent: str, urgency_results: list[dict], sentiment_results: list[dict],
) -> str:
priority_map = {"critical": 4, "high": 3, "medium": 2, "low": 1}
top_urgency = self._top_label(urgency_results).lower()
base_score = 2
for keyword, value in priority_map.items():
if keyword in top_urgency:
base_score = value
break

if "angry" in self._top_label(sentiment_results).lower():
base_score = min(base_score + 1, 4)

critical_keywords = ["data loss", "security", "fraud", "payment failed"]
if any(kw in intent.lower() for kw in critical_keywords):
base_score = min(base_score + 1, 4)

labels = {4: "P1-Critical", 3: "P2-High", 2: "P3-Medium", 1: "P4-Low"}
return labels.get(base_score, "P3-Medium")

def _check_escalation(
self, intent: str, priority: str, sentiment: str, confidence: float,
) -> bool:
if priority == "P1-Critical":
return True
if "angry" in sentiment.lower() and priority in ("P1-Critical", "P2-High"):
return True
if "complaint" in intent.lower():
return True
if confidence < 0.4:
return True
return False

# -- public API -------------------------------------------------------

def classify_ticket(self, ticket_id: str, text: str) -> TicketClassification:
"""Run full classification on a single ticket."""

# Intent
intent_results = self.pipeline(text, self.intent_labels, threshold=0.3)[0]
intent_results = sorted(intent_results, key=lambda r: r["score"], reverse=True)
primary_intent = intent_results[0]["label"] if intent_results else "unknown"
intent_confidence = intent_results[0]["score"] if intent_results else 0.0
secondary_intents = intent_results[1:3]

# Urgency and sentiment
urgency_results = self.pipeline(text, URGENCY_LABELS, threshold=0.3)[0]
sentiment_results = self.pipeline(text, SENTIMENT_LABELS, threshold=0.3)[0]
sentiment = self._top_label(sentiment_results)

# Priority, routing, escalation
priority = self._calculate_priority(primary_intent, urgency_results, sentiment_results)
assigned_team = self.routing_rules.get(primary_intent, "general-support")
requires_escalation = self._check_escalation(primary_intent, priority, sentiment, intent_confidence)

classification = TicketClassification(
ticket_id=ticket_id,
text=text,
primary_intent=primary_intent,
intent_confidence=intent_confidence,
secondary_intents=secondary_intents,
priority=priority,
sentiment=sentiment,
assigned_team=assigned_team,
requires_escalation=requires_escalation,
classified_at=datetime.now(timezone.utc).isoformat(),
)

self.classifications.append(classification)
return classification

def classify_batch(
self, tickets: list[tuple[str, str]],
) -> list[TicketClassification]:
"""Classify a list of (ticket_id, text) tuples."""
return [self.classify_ticket(tid, txt) for tid, txt in tickets]

def get_routing_summary(self) -> dict[str, dict]:
"""Count tickets and escalations per team."""
summary: dict[str, dict] = {}
for c in self.classifications:
entry = summary.setdefault(c.assigned_team, {"count": 0, "escalations": 0})
entry["count"] += 1
if c.requires_escalation:
entry["escalations"] += 1
return summary

def export_classifications(self, filepath: str) -> None:
"""Write all classification results to a JSON file."""
data = [c.to_dict() for c in self.classifications]
with open(filepath, "w") as f:
json.dump(data, f, indent=2)


# ---------------------------------------------------------------------------
# Usage example
# ---------------------------------------------------------------------------

pipe = IntentClassificationPipeline(device="cpu")

incoming_tickets = [
("TKT-1001", "I can't log into my account. Password reset isn't working."),
("TKT-1002", "Love your product! Would be great if you added calendar sync."),
("TKT-1003", "Your service has been down for 2 hours. We're losing money!"),
("TKT-1004", "Quick question -- do you offer student discounts?"),
]

results = pipe.classify_batch(incoming_tickets)

for r in results:
escalation_flag = " ** ESCALATION REQUIRED **" if r.requires_escalation else ""
print(f"{r.ticket_id}: {r.primary_intent} ({r.intent_confidence:.2f}) "
f"| {r.priority} | -> {r.assigned_team}{escalation_flag}")

# Routing summary
print("\nRouting Summary:")
for team, stats in pipe.get_routing_summary().items():
print(f" {team}: {stats['count']} tickets ({stats['escalations']} escalations)")

# Export to JSON
pipe.export_classifications("ticket_classifications.json")

Best Practices

Tune Thresholds for Your Use Case

Different threshold values trade off precision against recall:

StrategyThresholdWhen to use
High precision0.7Avoid misrouting; accept that some tickets go to a human triage queue
Balanced0.5Good default for most workflows
High recall0.3Catch edge-case intents; accept more false positives for review

Use Descriptive Labels

Labels should be specific enough that the model can distinguish between them:

# Good -- specific and descriptive
good_labels = [
"customer requesting refund for product",
"customer reporting technical bug or error",
"customer asking how to use a feature",
]

# Bad -- vague or overlapping
bad_labels = [
"refund", # too short, ambiguous
"problem", # too vague
"question", # too broad
]

Handle Low-Confidence Classifications

When no label scores above a reasonable threshold, route the ticket for human review instead of guessing:

def classify_with_fallback(text: str, labels: list[str], threshold: float = 0.5) -> dict:
results = pipeline(text, labels, threshold=0.3)[0]

if not results:
return {"intent": "needs_human_review", "confidence": 0.0, "needs_review": True}

top = max(results, key=lambda r: r["score"])

if top["score"] < threshold:
return {"intent": top["label"], "confidence": top["score"], "needs_review": True}

return {"intent": top["label"], "confidence": top["score"], "needs_review": False}

Limitations and Considerations

  1. Language support -- GLiClass works best with English text. Test thoroughly before deploying with other languages.

  2. Context window -- Classification is based only on the text you pass in. If prior conversation history matters, concatenate it into the input.

  3. Domain adaptation -- Generic labels are a starting point. Refine wording based on the kinds of tickets your team actually receives.

  4. Edge cases -- Very short messages, heavy sarcasm, or text full of typos can reduce accuracy.

  5. Hardware -- The base model runs comfortably on CPU for moderate volumes. For high-throughput batch processing, use a GPU by setting device='cuda'.

  6. Human oversight -- Use automated classification to assist agents, not to replace human judgment on complex or sensitive cases.

Next Steps