We are building a customer support intent triage agent that reads unstructured ticket text and returns structured JSON with intent, sentiment, urgency, and extracted entities. This helps small support teams route tickets automatically instead of reading every message manually. I run this on Oxlo.ai because their per-request pricing stays predictable even when customers paste long logs or thread histories into a single ticket.
What you'll need
- Python 3.10 or newer
- The OpenAI SDK:
pip install openai - An Oxlo.ai API key from https://portal.oxlo.ai
- Optional:
pip install pydanticfor the validation step
Step 1: Set up the Oxlo.ai client
I keep the API key in an environment variable and point the OpenAI SDK at Oxlo.ai's endpoint. This is a drop-in replacement, so the rest of the code looks exactly like the standard OpenAI pattern.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.oxlo.ai/v1",
api_key=os.environ.get("OXLO_API_KEY")
)
Step 2: Define the NLU schema and system prompt
The system prompt is the contract. It tells the model how to analyze the text and restricts the output to a specific JSON schema so I can parse it reliably.
SYSTEM_PROMPT = """You are an NLU triage engine. Analyze the customer support message below and return ONLY a JSON object with no markdown formatting.
Fields:
- intent: one of [billing, technical_issue, account_access, feature_request, cancellation, other]
- sentiment: one of [angry, frustrated, neutral, satisfied, excited]
- urgency: one of [low, medium, high, critical]
- entities: an object containing any order IDs, email addresses, dollar amounts, or product names mentioned
- summary: a one-sentence summary of the problem
Rules:
- Do not include explanations outside the JSON.
- If a field is uncertain, use the closest match.
- Keep the summary under 20 words."""
Step 3: Build the triage function
I wrap the chat completion call in a small function. I use Llama 3.3 70B because it follows structured instructions well and handles the mixed tone of support tickets accurately. Since Oxlo.ai charges per request, I can send the entire thread context without watching token meters.
import json
def triage_ticket(ticket_text: str) -> dict:
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": ticket_text},
],
temperature=0.1,
max_tokens=512,
)
raw = response.choices[0].message.content.strip()
# Remove accidental markdown code fences if the model emits them
if raw.startswith("
```json"):
raw = raw.split("\n", 1)[1].rsplit("```
", 1)[0].strip()
elif raw.startswith("
```"):
raw = raw.split("\n", 1)[1].rsplit("```
", 1)[0].strip()
return json.loads(raw)
Step 4: Add validation and error handling
In production, I refuse to let a malformed LLM response crash the pipeline. I added a lightweight Pydantic model and a retry fallback. If parsing fails, the function returns a safe default so the ticket still lands in a human queue.
from pydantic import BaseModel, Field, ValidationError
from typing import Literal, Optional
class TriageResult(BaseModel):
intent: Literal["billing", "technical_issue", "account_access", "feature_request", "cancellation", "other"]
sentiment: Literal["angry", "frustrated", "neutral", "satisfied", "excited"]
urgency: Literal["low", "medium", "high", "critical"]
entities: dict = Field(default_factory=dict)
summary: str
def triage_ticket_safe(ticket_text: str, retries: int = 2) -> dict:
for attempt in range(retries + 1):
try:
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": ticket_text},
],
temperature=0.1 + (attempt * 0.1),
max_tokens=512,
)
raw = response.choices[0].message.content.strip()
if raw.startswith("
```"):
raw = raw.split("\n", 1)[1].rsplit("```
", 1)[0].strip()
result = TriageResult(**json.loads(raw))
return result.model_dump()
except (json.JSONDecodeError, ValidationError) as e:
if attempt == retries:
return {
"intent": "other",
"sentiment": "neutral",
"urgency": "medium",
"entities": {},
"summary": "Failed to parse triage result.",
"parse_error": str(e)
}
continue
Run it
I test the agent with a realistic ticket that mixes billing frustration with a technical detail. The function returns structured data that my router can act on immediately.
if __name__ == "__main__":
ticket = (
"Hi, I was charged $49.99 twice on March 12 for order OX-8842. "
"I tried to cancel the second charge in your dashboard but it keeps saying 'payment method expired'. "
"This is the third time I've emailed. Please fix this today or I will dispute the charge."
)
result = triage_ticket_safe(ticket)
print(json.dumps(result, indent=2))
Example output:
{
"intent": "billing",
"sentiment": "frustrated",
"urgency": "high",
"entities": {
"amount": "$49.99",
"order_id": "OX-8842",
"date": "March 12"
},
"summary": "Customer double-billed and unable to cancel charge via dashboard."
}
Wrap-up
This triage agent turns unstructured support noise into structured router input. The next step is to wire it into a webhook so billing tickets auto-create a Stripe refund task and critical urgency messages ping Slack. If you are processing high volumes, Oxlo.ai's request-based pricing removes the penalty for long ticket threads, which keeps costs flat regardless of how much context you feed the model. See https://oxlo.ai/pricing for plan details.
Top comments (0)