In this tutorial we are building a zero-shot text clustering script that groups raw customer support tickets into named themes without any embedding pipeline. You feed it a Python list of strings, and an LLM returns structured clusters with human-readable labels. It is ideal for weekly ticket triage, survey analysis, or any ad-hoc sorting task where you do not want to maintain a traditional ML stack.
What you'll need
- Python 3.10 or newer installed locally.
- The OpenAI SDK:
pip install openai - An Oxlo.ai API key from https://portal.oxlo.ai. Oxlo.ai uses request-based pricing, so stuffing a long list of tickets into one prompt does not inflate cost the way token-based billing would. Plan details are at https://oxlo.ai/pricing.
Step 1: Prepare the dataset
I will start with a hard-coded list of ten support tickets. In production you would pull these from a CSV or your help-desk API, but a plain list keeps the example self-contained.
TICKETS = [
"My credit card was charged twice for the same order.",
"The app crashes every time I try to upload a photo.",
"I forgot my password and the reset email never arrives.",
"Shipping was fast, but the box arrived completely crushed.",
"Login fails on Android after the latest update.",
"I see two identical charges on my statement this month.",
"Photo upload freezes the screen on my phone.",
"Package was delivered in perfect condition ahead of schedule.",
"The password reset link expired before I could use it.",
"Android app closes immediately when I open settings.",
]
Step 2: Write the system prompt
The system prompt locks the model into a rigid JSON schema and tells it to produce exactly three clusters. Keeping the instructions in the system message keeps the user message clean.
SYSTEM_PROMPT = """You are a text clustering engine. Your job is to group a numbered list of items into exactly 3 thematic clusters.
Rules:
- Analyze every item.
- Create descriptive cluster names, 2 to 4 words each.
- Return ONLY a JSON object. Do not write markdown fences or explanations.
- The JSON format must be: {"clusters": [{"name": "...", "items": [0, 1, ...]}, ...]}
- Each item index must appear in exactly one cluster.
- Use the item numbers exactly as provided (0-based indexing).
"""
Step 3: Send the clustering request to Oxlo.ai
Initialize the OpenAI client pointing at Oxlo.ai and define a helper that formats the list, sends it to llama-3.3-70b, and returns the raw JSON string. I set the temperature low and enable JSON mode so the output is predictable.
import json
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.oxlo.ai/v1",
api_key=os.environ.get("OXLO_API_KEY", "YOUR_OXLO_API_KEY"),
)
def cluster_items(items, model="llama-3.3-70b"):
numbered = "\n".join(f"{i}. {text}" for i, text in enumerate(items))
user_message = f"Group these {len(items)} items into 3 clusters.\n\n{numbered}"
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
temperature=0.1,
response_format={"type": "json_object"},
)
return response.choices[0].message.content
Step 4: Parse and print the clusters
The helper below loads the JSON, maps each index back to the original ticket text, and prints the groups. If an index is missing or duplicated, the assertion will fail loudly so you know the model drifted from instructions.
def print_clusters(items, raw_json):
data = json.loads(raw_json)
seen_indices = set()
for cluster in data["clusters"]:
print(f"\nCluster: {cluster['name']}")
for idx in cluster["items"]:
assert 0 <= idx < len(items), f"Invalid index {idx}"
assert idx not in seen_indices, f"Duplicate index {idx}"
seen_indices.add(idx)
print(f" [{idx}] {items[idx]}")
missing = set(range(len(items))) - seen_indices
if missing:
print(f"\nWarning: unclustered indices {missing}")
Run it
Tie the pieces together in a main guard, run the script, and inspect the output. The clusters should align to billing, app crashes, and account or shipping issues.
if __name__ == "__main__":
raw = cluster_items(TICKETS)
print_clusters(TICKETS, raw)
Example output:
Cluster: Billing Issues
[0] My credit card was charged twice for the same order.
[5] I see two identical charges on my statement this month.
Cluster: App Bugs
[1] The app crashes every time I try to upload a photo.
[4] Login fails on Android after the latest update.
[6] Photo upload freezes the screen on my phone.
[9] Android app closes immediately when I open settings.
Cluster: Account and Shipping
[2] I forgot my password and the reset email never arrives.
[3] Shipping was fast, but the box arrived completely crushed.
[7] Package was delivered in perfect condition ahead of schedule.
[8] The password reset link expired before I could use it.
Wrap-up and next steps
That is the whole pipeline. A concrete next step is to wrap the cluster_items function in a FastAPI endpoint so your support platform can POST new ticket batches and receive clusters on demand. Another is to swap llama-3.3-70b for qwen-3-32b if you need to cluster non-English items, or move the script into a nightly cron job that reads from a CSV and writes results into a database table.
Top comments (0)