Inspiration Every support team drowns in the same way: a flood of tickets where 90% are routine (password resets, billing questions, "where's my package") and 10% are genuinely urgent (a compromised account, a production outage). Triage — figuring out which is which, fast — is exactly the kind of task that's expensive to do well with people and wasteful to do with a single giant AI model, because most of that volume doesn't need frontier-level reasoning to classify. It needs to be fast and cheap, with the expensive reasoning reserved for the one step that actually benefits from it. That's precisely the tiered-model story Nebius Token Factory and NVIDIA Nemotron 3 (Nano/Super/Ultra) are built for, so we built the product around it instead of bolting it on afterward.

What it does Ticket Triage Copilot takes an incoming support ticket and runs it through a four-stage pipeline:

Classify (Nemotron 3 Nano) — category, urgency, and sentiment, in milliseconds, on every ticket. Route (Nemotron 3 Super) — priority (1–5), which team owns it, and whether it needs to bypass drafting entirely and escalate straight to a human (e.g. a security incident). Retrieve (RAG) — pulls the most relevant articles from a support knowledge base to ground the reply in real policy instead of letting the model invent one. Draft (Nemotron 3 Ultra) — writes a full reply citing those retrieved articles. A human reviews every draft in a dashboard — sees the full pipeline trace, which KB articles it was grounded on, edits if needed — and clicks Approve & Send. It's a copilot, not an autonomous responder.

How we built it Backend: FastAPI + SQLite, with the pipeline as four independently swappable stages (classify.py, route.py, rag.py, draft.py), all calling Nebius Token Factory's OpenAI-compatible API through one shared client. Frontend: React + Vite + TypeScript + Tailwind — a live queue, per-tier latency stats, and a ticket detail view showing the full pipeline trace. Mock mode: when no API key is present, the app falls back to deterministic rule-based logic and a local TF-IDF retrieval index instead of live Nemotron calls, so the whole pipeline and UI are demoable offline and the architecture is verifiable without burning API credits. Deployment: Render (backend, via a render.yaml blueprint) + Vercel (frontend), talking to each other over a VITE_API_BASE env var and open CORS. Challenges we ran into Retrieval quality in mock mode. Our first pass at offline retrieval used raw keyword-overlap counting, which mis-ranked a ticket reporting a hacked account against an unrelated "account deletion" KB article just because it shared more common words. Replacing it with proper TF-IDF (log-dampened term frequency × inverse document frequency, cosine similarity) fixed the ranking without needing any live API calls — a good reminder that "just do keyword matching" and "do it well" are very different amounts of work. Fragile rule-based classification. Our mock classifier's keyword lists caused real misclassifications from substring collisions — "accounting" matching "account," "not urgent" matching "urgent." We rewrote it as word-boundary regex scoring across weighted category buckets instead of a brittle if/elif chain, which is both more correct and easier to extend. A Python 3.14 landmine on Render. Our backend deployed cleanly locally on Python 3.13 but broke on Render with PydanticUserError: Field 'id' requires a type annotation — Render defaulted to Python 3.14, and SQLModel's metaclass isn't yet compatible with how that version evaluates class annotations. Pinning the runtime explicitly (PYTHON_VERSION + .python-version) fixed it, but it's the kind of failure that's invisible until you deploy. Vercel's multi-service auto-detection. Vercel's importer detected both frontend/ and backend/ in the repo and offered to deploy the FastAPI backend as a Vercel Python service (serverless functions). We caught this before deploying: that runtime has no persistent local filesystem, which would silently break SQLite and our in-memory RAG index between requests. We kept the backend on Render, where it runs as a real long-lived process, and pointed Vercel at the frontend only. Accomplishments that we're proud of A genuinely tiered pipeline where each Nemotron size does the job it's actually suited for, not just three calls to the same model with different names. A fully offline-demoable mock mode that isn't a stub — it runs the same code paths, the same RAG grounding logic, and the same UI as the live pipeline. Catching and fixing real correctness bugs (retrieval ranking, classifier keyword collisions) before they made it into the demo, not after. A live, publicly deployed demo, not just a local screen recording. What we learned Tiered model routing isn't just a cost optimization — it changes what you can build. A single-model pipeline would have made drafting the reply the bottleneck for every ticket, even the trivial ones. Grounding matters more than model size for trustworthiness: a smaller model with the right KB excerpts in front of it beats a bigger model guessing at your company's actual policy. Deployment surfaces problems local dev never will — Python version drift and serverless-vs-persistent-process tradeoffs cost more debugging time than the actual pipeline logic did. What's next for Ticket Triage Copilot Real embedding-based retrieval in production (already wired for Nebius's embeddings endpoint, just needs a live key) instead of the mock TF-IDF fallback. Slack/email integration so tickets can flow in from where support teams actually work, instead of a manual submission form. A feedback loop where edits a human makes to a draft get used to improve future drafts for similar tickets. Multi-language support, since Nemotron 3's multilingual capability is already there — just needs the KB and prompts extended.

Built With

Share this project:

Updates

Submission history