Topic modeling with an LLM replaces weeks of clustering work with a few API calls. In this tutorial I will build a complete pipeline that reads raw support tickets, extracts a clean topic taxonomy, and labels every ticket. It is useful for product teams and researchers who need thematic analysis without training a custom model.
What you'll need
- Python 3.10 or newer
pip install openai pandas- An Oxlo.ai API key from https://portal.oxlo.ai
- A dozen or more text snippets to analyze
Step 1: Configure the Oxlo.ai client
Oxlo.ai is fully OpenAI SDK compatible, so the switch is only a base URL and an API key. I use llama-3.3-70b as the general-purpose workhorse for this pipeline.
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[{"role": "user", "content": "Reply with exactly: Connection OK"}],
)
print(response.choices[0].message.content)
Step 2: Prepare the documents
I will use a hardcoded list of support snippets. In production you would read from a CSV or your data warehouse.
documents = [
"PDF export button spins forever and never downloads.",
"Please add dark mode to the dashboard for night usage.",
"CSV export works, but PDF fails on datasets over 50 rows.",
"Dark mode would reduce eye strain during long sessions.",
"Team invite link returns a 403 error when clicked.",
"Billing page displays the wrong VAT number and won't update.",
"Invite links expire too fast, should be valid for 24 hours.",
"PDF exports are blank if the report contains large charts.",
"Mobile app needs dark mode too.",
"Cannot save the VAT number in billing settings.",
]
Step 3: Define the extraction prompt
The system prompt is the most important part of the pipeline. It forces JSON output and constrains the model to only the themes present in the text.
SYSTEM_PROMPT = """You are a precise topic modeling assistant.
Your job is to read a list of user feedback texts and extract the main themes.
Rules:
- Identify up to 5 distinct topics.
- Each topic must have a short label (2 to 4 words) and a one-sentence description.
- Base topics only on the provided texts. Do not hallucinate.
- Output strictly as JSON in this format:
{
"topics": [
{"id": "topic_1", "label": "Label", "description": "Description."}
]
}
"""
Step 4: Generate the topic taxonomy
I send the full document list in one request. Because Oxlo.ai uses flat per-request pricing, stuffing the prompt with every ticket does not inflate cost the way token-based billing would.
from openai import OpenAI
import json
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
numbered_docs = "\n".join([f"{i}. {d}" for i, d in enumerate(documents)])
user_message = f"Analyze the following feedback texts and return the JSON taxonomy:\n\n{numbered_docs}"
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
)
taxonomy = json.loads(response.choices[0].message.content)
print(json.dumps(taxonomy, indent=2))
Step 5: Classify every document
Now I ask the model to assign each document to one of the topics from Step 4. I keep the taxonomy and the documents in the same prompt so the model has full context.
CLASSIFY_PROMPT = """You are a document classification assistant.
Given a topic taxonomy and a list of user feedback texts, assign each text to exactly one topic from the taxonomy.
Rules:
- Use only the topic IDs provided.
- If a text fits multiple topics, choose the best match.
- Output strictly as JSON in this format:
{
"assignments": [
{"doc_index": 0, "topic_id": "topic_1", "reason": "One sentence explaining why."}
]
}
"""
taxonomy_text = json.dumps(taxonomy, indent=2)
classify_user_msg = f"Taxonomy:\n{taxonomy_text}\n\nDocuments:\n{numbered_docs}\n\nReturn the JSON assignments."
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": CLASSIFY_PROMPT},
{"role": "user", "content": classify_user_msg},
],
)
assignments = json.loads(response.choices[0].message.content)
print(json.dumps(assignments, indent=2))
Step 6: Export the results
I join the assignments back to the original texts and write a CSV.
import pandas as pd
topic_lookup = {t["id"]: t["label"] for t in taxonomy["topics"]}
rows = []
for a in assignments["assignments"]:
idx = a["doc_index"]
rows.append({
"document": documents[idx],
"topic_id": a["topic_id"],
"topic_label": topic_lookup.get(a["topic_id"], "Unknown"),
"reason": a["reason"],
})
df = pd.DataFrame(rows)
df.to_csv("topic_modeling_results.csv", index=False)
print(df.to_string(index=False))
Run it
Save the script as topic_model.py, export your key, and run it.
export OXLO_API_KEY="sk-..."
python topic_model.py
The first call produces a taxonomy like this:
{
"topics": [
{
"id": "topic_1",
"label": "PDF Export Failure",
"description": "Users report that PDF exports hang, fail, or produce blank files."
},
{
"id": "topic_2",
"label": "Dark Mode Request",
"description": "Users want a dark theme for the dashboard and mobile app."
},
{
"id": "topic_3",
"label": "Team Invite Errors",
"description": "Invitation links return 403 errors or expire too quickly."
},
{
"id": "topic_4",
"label": "Billing VAT Issues",
"description": "The VAT number field displays wrong values or will not save."
}
]
}
The second call produces assignments, and the final CSV looks like this:
document topic_id topic_label reason
PDF export button spins forever and never... topic_1 PDF Export Failure Directly describes a PDF export hang.
Please add dark mode to the dashboard... topic_2 Dark Mode Request Explicitly requests dark mode.
CSV export works, but PDF fails on datasets... topic_1 PDF Export Failure Reports PDF failure on large datasets.
Dark mode would reduce eye strain... topic_2 Dark Mode Request Requests dark mode for eye strain.
Team invite link returns a 403 error... topic_3 Team Invite Errors Describes a 403 error on invite link.
Billing page displays the wrong VAT number... topic_4 Billing VAT Issues Reports wrong VAT and update failure.
Invite links expire too fast... topic_3 Team Invite Errors Complains about short invite link expiry.
PDF exports are blank if the report... topic_1 PDF Export Failure PDF blank with large charts.
Mobile app needs dark mode too. topic_2 Dark Mode Request Requests dark mode on mobile.
Cannot save the VAT number in billing settings. topic_4 Billing VAT Issues Cannot save VAT in billing settings.
Wrap-up and next steps
If your documents are highly technical or ambiguous, swap llama-3.3-70b for deepseek-v3.2 or kimi-k2.6 to get stronger reasoning on edge cases. Both are available on Oxlo.ai with the same client and request-based pricing.
A concrete next step is to wrap this pipeline in a FastAPI endpoint so other services can POST text batches and receive labeled JSON back. Another is to schedule the script daily against your support ticket export and append results to a data warehouse table for trending.
Top comments (0)