APPLIED AI · OCTOBER 21, 2026
Every ticket.
A clearer next step.
A support triage app that exposes its uncertainty, keeps routing rules in code, and lets a person correct the recommendation.
The decision behind the label
A ticket classifier can identify a payment problem and still make the wrong operational recommendation. The useful question is which team should look at it, how quickly, and when a person needs to resolve uncertainty.
I built this console with Codex to make those decisions inspectable. A visitor pastes a redacted ticket or selects a synthetic example, then sees a category, recommended team, frustration score, urgency probability, and suggested queue. They can accept or correct the category and urgency, revisit recent analyses, and export the original results alongside their reviews.
There are five categories: payments, shipping, technical, returns, and other. The app recommends routing only. It does not send messages, issue refunds, or act on a customer account.
One request, three narrow questions
The server uses TypeSafe's official JavaScript SDK to send one request containing three typed questions. Choice selects a category and returns its option probabilities and confidence. Score estimates frustration against three descriptive levels. Noul returns the probability that urgent attention is needed.
The frustration scale runs from 0 for calm, through 1 for frustrated but civil, to 2 for very angry. Its score is a probability-weighted mean, so a value between levels is valid. Confidence describes the returned distribution; it is not a guarantee of correctness. Noul has no separate confidence field, and the interface does not invent one.
The ticket enters the request as untrusted content. Fixed question instructions tell the model to evaluate the issue rather than obey instructions embedded in the message. Input and response validation, timeouts, and visible upstream errors support that boundary without claiming that prompting eliminates every failure mode.
Keep the queue policy in code
The model provides signals; ordinary TypeScript chooses the queue. The initial policy sends uncertain categories and middle-range urgency probabilities to human review. High urgency or frustration gets priority. Remaining tickets go to routine support.
if (categoryConfidence < 0.60 || categoryMargin < 0.20) {
return 'human review';
}
if (urgentProbability >= 0.35 && urgentProbability <= 0.65) {
return 'human review';
}
if (urgentProbability >= 0.75 || frustration >= 1.50) {
return 'priority';
}
return 'routine';These values are starting assumptions that need evaluation against error costs and representative tickets. The app preserves its original recommendations when a reviewer changes the category or urgency, so the exported corrections can support later analysis.
The first live evaluation found a routing gap
I ran the entire eight-ticket synthetic smoke set against jev-1.13.0, with one paid request per ticket and automatic retries disabled. All eight category labels and binary urgency decisions matched the authored labels. No requests failed.
However, the share sent to human review was 0 of 8. The deliberately ambiguous ticket was classified as other and routed to routine, even though the scenario called for a person to investigate. A correct category did not make the routing recommendation appropriate.
That is the most useful finding from this first pass. The eight examples are a small, manually authored smoke set, not evidence of general accuracy or business impact. I preserved the result instead of tuning the thresholds against those same examples and reporting an improved score. The next step is a larger held-out evaluation that measures routing mistakes and the trade-off between review coverage and automation.
The saved evaluation artifact records the model, dataset checksum, request count, outcomes, and limitation. Exact billed cost was not captured.
A demo that stays honest about its mode
Demo mode works without credentials and uses clearly labeled, fixed sample results. Its ambiguous example illustrates human review, but those authored numbers do not predict what the live model will return. A failed live request produces an error; it never silently substitutes a sample result.
History stays in browser localStorage on the current device and browser, with an explicit clear action. It is not synchronized between devices. The server does not persist or log ticket bodies, and visitors are asked to use synthetic or redacted messages.
Protect the paid path
The API key is a server-only environment variable. Live requests require an access code, and Vercel's native Firewall enforces a shared limit of ten attempts per minute in each serving region. The endpoint fails closed if the firewall check is unavailable or the rule is missing. Regional rate limits are not an exact global spending cap.
The current deployment is a protected Vercel Preview, so it requires Vercel access as well as the app's live access code. It is not an anonymously accessible public demo. The repository includes the setup needed to run its free demo locally.
What is verified, and what is next
Seven automated tests cover response parsing, queue rules, input validation, SDK request serialization, and protection of the live endpoint. Lint, the production build, desktop and mobile browser flows, and hosted checks for successful inference, rejected access codes, and rate limiting passed.
This is a working hosted-model application. It does not claim to complete the separate BANKING77 training and MLOps research brief preserved in the repository. Before using it operationally, I would expand the labeled evaluation, measure missed review cases, and choose routing thresholds using explicit costs.
Technical references: TypeSafe quick start, confidence semantics, and Vercel's rate limiting SDK.