We are going to build a support ticket NLU parser that reads unstructured customer messages and returns structured JSON with intent, entities, and urgency. This saves hours of manual triage for support teams and feeds directly into routing rules or CRM fields. I shipped a version of this for a SaaS company last quarter, and it cut first-response categorization time to zero.
What you'll need
- Python 3.10 or newer
- An Oxlo.ai API key from https://portal.oxlo.ai
- The OpenAI SDK:
pip install openai - Pydantic:
pip install pydantic
Step 1: Initialize the Oxlo.ai client
I keep the API key in an environment variable and point the OpenAI SDK at Oxlo.ai. A quick smoke test confirms the endpoint is alive.
from openai import OpenAI
import os
client = OpenAI(
base_url="https://api.oxlo.ai/v1",
api_key=os.environ["OXLO_API_KEY"]
)
# Smoke test
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[{"role": "user", "content": "Reply with OK"}]
)
print(response.choices[0].message.content)
Step 2: Define the system prompt
The system prompt does all the heavy lifting. It tells the model to act as an NLU engine and emit only JSON. I treat this as a config file and version it in Git.
SYSTEM_PROMPT = """You are an NLU engine. Analyze the customer support message and return a single JSON object with no markdown formatting.
Fields:
- intent: one of [billing_question, technical_issue, feature_request, account_access, complaint, other]
- product: the product or module mentioned, or null
- plan: the subscription tier mentioned, or null
- urgency: integer 1 to 5, where 5 means revenue-impacting or security-related
- sentiment: one of [angry, frustrated, neutral, satisfied, excited]
- summary: a 12-word max description of the issue
Rules:
- Return only valid JSON.
- Do not include explanations or markdown code blocks.
- If a field is unknown, use null."""
Step 3: Build the extraction function
Now I wrap the API call. I use llama-3.3-70b because it handles instruction following for structured output reliably. The function passes the raw ticket text and returns a Python dict.
import json
def parse_ticket(text: str) -> dict:
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": text},
],
)
raw = response.choices[0].message.content
return json.loads(raw)
if __name__ == "__main__":
msg = "I was charged twice for the Pro plan this month and I need a refund immediately."
print(parse_ticket(msg))
Step 4: Validate and harden
Production traffic includes edge cases. I add a Pydantic model to enforce types and a fallback that retries once with qwen-3-32b if parsing fails. Because Oxlo.ai uses request-based pricing, the retry costs the same flat rate even on long ticket threads, which keeps costs predictable compared to token-based providers.
from pydantic import BaseModel, Field, ValidationError
from typing import Literal, Optional
class TicketNLU(BaseModel):
intent: Literal["billing_question", "technical_issue", "feature_request", "account_access", "complaint", "other"]
product: Optional[str] = None
plan: Optional[str] = None
urgency: int = Field(..., ge=1, le=5)
sentiment: Literal["angry", "frustrated", "neutral", "satisfied", "excited"]
summary: str = Field(..., max_length=120)
def parse_ticket_safe(text: str) -> TicketNLU:
try:
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": text},
],
)
data = json.loads(response.choices[0].message.content)
return TicketNLU(**data)
except (json.JSONDecodeError, ValidationError):
# Retry with Qwen 3 32B for a stricter rewrite
retry = client.chat.completions.create(
model="qwen-3-32b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": text},
],
)
data = json.loads(retry.choices[0].message.content)
return TicketNLU(**data)
Run it
Here is the full script entrypoint with a small batch of tickets. I run this from the terminal with python ticket_nlu.py.
if __name__ == "__main__":
tickets = [
"I was charged twice for the Pro plan this month and I need a refund immediately.",
"Your API keeps returning 500 errors on the /export endpoint. This is blocking our month-end close.",
"Do you have a roadmap for SSO support in the Enterprise plan?",
]
for t in tickets:
result = parse_ticket_safe(t)
print(result.model_dump_json(indent=2))
print("---")
Example output:
{
"intent": "billing_question",
"product": null,
"plan": "Pro",
"urgency": 4,
"sentiment": "frustrated",
"summary": "Customer was double charged and wants a refund"
}
---
{
"intent": "technical_issue",
"product": "API",
"plan": null,
"urgency": 5,
"sentiment": "frustrated",
"summary": "API 500 errors blocking month-end close"
}
---
{
"intent": "feature_request",
"product": null,
"plan": "Enterprise",
"urgency": 2,
"sentiment": "neutral",
"summary": "Asking about SSO roadmap"
}
Wrap up
Wire this parser into your helpdesk webhook so every incoming ticket gets tagged before a human opens it. If you start processing long conversation threads or need deeper reasoning, swap the model to kimi-k2.6 or deepseek-v3.2 on Oxlo.ai. Both handle extended context and still cost the same flat per-request rate even when the input grows to thousands of tokens. You can compare plans at https://oxlo.ai/pricing.
Top comments (0)