Goal
Every ticket landing in a shared inbox has to go somewhere. This recipe asks Jev one question about the ticket and gets back a single queue name plus a number saying how certain that answer is. Your router assigns the clear ones and parks the rest for a person, which is the shape most support inbox triage work ends up taking.
State shape
State is the content you want judged, and one request carries exactly one state that every question sees. Send an object instead of a wall of text, so each part of the ticket has a name.
{
"subject": "Charged twice for September",
"body": "My card shows two charges of $49 on the 3rd. Please refund one.",
"plan": "team"
}
Leave out the signature block, the previous twelve replies and the CRM export. Accuracy drops as unrelated detail grows, and the budget for state plus the longest question is 32k tokens.
Questions
One question, and it’s a Choice: a question that picks exactly one label from a list you write, and returns a probability for every label you offered. Routing is a pick. A Score would imply the queues sit on a scale from low to high, and a Noul (a yes/no question answered as a probability between 0 and 1) would cost one call per queue.
The labels are billing, technical, order_status and complaint. Each gets a one-line description of what belongs in it, because those descriptions are all Jev knows about how your company sorts mail. Write them the way you would write them for a new hire on their first shift.
A Choice takes up to 255 options, so a deep taxonomy fits in one question. It usually reads better as a cascade, where a broad question picks the branch and a second question picks inside it, which is what the hierarchical classification recipe walks through.
Code
from typesafe_sdk import Choice, TypeSafeClient
QUEUES = {
"billing": "Charges, invoices, refunds, anything about money already paid.",
"technical": "Something in the product errors, breaks, or will not load.",
"order_status": "Asking where an existing order is or when it arrives.",
"complaint": "Unhappy with the service or a person, wants it put right.",
}
ticket = {
"subject": "Charged twice for September",
"body": "My card shows two charges of $49 on the 3rd. Please refund one.",
"plan": "team",
}
with TypeSafeClient() as client:
response = client.system_one(
state=ticket,
questions={
"queue": Choice(
instructions="Which support queue should own this ticket?",
criteria=QUEUES,
)
},
model="jev-1.13.0",
)
answer = response.answers["queue"]
print(answer.choice, round(answer.confidence, 2))
print(answer.probabilities)Decision policy
Jev hands back a label and a number. Which tickets move without a human is a decision your code makes, and the numbers below are starting points taken from TypeSafe’s own worked examples, not tuned values for your inbox.
FLOOR = 0.6 # below this, nothing gets assigned
AUTO_REPLY = 0.9 # only the cheapest action runs unattended
def assign(ticket_id, answer):
if answer.confidence < FLOOR:
return move_to_unsorted(ticket_id)
if answer.choice == "complaint":
return assign_to_queue(ticket_id, "complaint", priority="high")
if answer.choice == "order_status" and answer.confidence >= AUTO_REPLY:
return send_tracking_link(ticket_id)
return assign_to_queue(ticket_id, answer.choice, priority="normal")
assign("t-8812", response.answers["queue"])
Sample output
Illustrative, not a recorded run. Your own numbers will differ.
{
"model": "jev-1.13.0",
"answers": {
"queue": {
"type": "choice",
"choice": "billing",
"probabilities": {
"billing": 0.91,
"technical": 0.03,
"order_status": 0.02,
"complaint": 0.04
},
"confidence": 0.88
}
},
"usage": { "input_tokens": 96, "output_tokens": 12 }
}
Pitfalls
- Negations and scoping words get read at face value. A ticket opening “this isn’t a billing problem” still pushes weight onto
billing. Literal reading heads the nine documented weak spots for Jev 1.13, which TypeSafe publishes under the name jaggedness, a per-version list of what the model gets wrong. confidencesqueezes the whole distribution into one number. When you need to know whether two queues were neck and neck, readprobabilitiesinstead; it comes back on every Choice answer.- Pasting the full thread into state costs accuracy as well as tokens. Documented behaviour is that accuracy falls as unrelated content grows around the part that matters.
- A threshold tuned here will not transfer to a Noul. TypeSafe documents a case where a Noul came back at 0.22 while a yes/no Choice on the same question put 0.01 on yes. Tune each primitive separately.
FAQ
What happens when a ticket really belongs in two queues?
The probabilities split, and confidence drops with them. A ticket about a broken checkout that also mentions a double charge might land at 0.48 billing and 0.44 technical. Your floor catches it and a human picks. If that overlap keeps recurring, the queue descriptions are the thing to fix, not the threshold.
Can I add a fifth queue later without redoing everything?
Yes, but retune afterwards. Confidence comes from the shape of the distribution across the options you offered, so adding a label changes what that shape looks like even for tickets you were already sorting well. Keep a labelled sample of real tickets and replay it after every edit to the list.
Does one request handle more than one question?
It does, and the speculative fan-out recipe is built on that. You can ask for the queue, the language and the refund risk in the same call against the same state, then discard the answers your branch never needed. Each question is judged independently of the others.