Evaluate a ticket
This tutorial routes a support ticket with decide(). The result is a typed value the code can branch on.
decide() asks typed questions about the ticket and gives back typed answers. In this tutorial, a server route returns a queue, an urgency, and a refund flag. It takes about 10 minutes.
1. Install#
npm i @tanstack/ai @tanstack/ai-typesafepnpm add @tanstack/ai @tanstack/ai-typesafeyarn add @tanstack/ai @tanstack/ai-typesafebun add @tanstack/ai @tanstack/ai-typesafe2. Set your key#
Get a key from TypeSafe. Set it in the server environment:
TYPESAFE_API_KEY=your-typesafe-api-keyKeep the key on the server. Do not send it to the browser.
3. Ask the questions on the server#
Create src/lib/evaluate-ticket.ts. It asks three questions about the ticket. POST is a plain Web handler: it takes a Request and returns a Response.
import { boolean, choice, decide, score } from '@tanstack/ai'
import { typesafeDecider } from '@tanstack/ai-typesafe'
type Ticket = {
subject: string
body: string
}
const adapter = typesafeDecider('jev-latest')
export function evaluateTicket(ticket: Ticket) {
return decide({
adapter,
state: ticket,
questions: {
queue: choice({
instructions: 'Which team should handle this ticket?',
options: {
billing: 'Payments, invoices, refunds',
tech: 'Bugs, outages, integrations',
sales: 'Pricing, upgrades, new accounts',
},
}),
urgency: score({
instructions: 'How urgent is this ticket?',
levels: ['low', 'medium', 'high'],
}),
refund: boolean({
instructions: 'Is the customer asking for a refund?',
}),
},
})
}
export type TicketResult = Awaited<ReturnType<typeof evaluateTicket>>
function readTicket(body: unknown): Ticket | null {
if (typeof body !== 'object' || body === null || !('ticket' in body)) {
return null
}
const ticket = body.ticket
if (typeof ticket !== 'object' || ticket === null) return null
if (!('subject' in ticket) || !('body' in ticket)) return null
if (typeof ticket.subject !== 'string' || typeof ticket.body !== 'string') {
return null
}
return { subject: ticket.subject, body: ticket.body }
}
export async function POST(request: Request) {
const ticket = readTicket(await request.json())
if (ticket === null) {
return new Response('Send { ticket: { subject, body } }.', {
status: 400,
})
}
return Response.json(await evaluateTicket(ticket))
}4. Mount the route#
Create src/routes/api.evaluate.ts. Start maps this file name to the /api/evaluate path.
import { createFileRoute } from '@tanstack/react-router'
import { POST } from '@/lib/evaluate-ticket'
export const Route = createFileRoute('/api/evaluate')({
server: {
handlers: {
POST: ({ request }) => POST(request),
},
},
})5. Branch on the answers#
Call the route from the client. TicketResult gives the answers their types, so queue.value is 'billing' | 'tech' | 'sales'.
import type { TicketResult } from '@/lib/evaluate-ticket'
async function postTicket(ticket: { subject: string; body: string }) {
const res = await fetch('/api/evaluate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ticket }),
})
if (!res.ok) throw new Error(`Evaluate failed with ${res.status}`)
const result: TicketResult = await res.json()
return result
}
const result = await postTicket({
subject: 'Charged twice for the same invoice',
body: 'Please refund the extra payment.',
})
if (result.refund.value) {
console.log('refund path', result.queue.value, result.urgency.value)
} else {
console.log('route to', result.queue.value, result.urgency.value)
}Send that ticket. You get a queue, an urgency, and a refund flag, and your code branches on them.
6. Try it#
- Run the app.
- Paste a ticket.
- Pick a provider and click Submit.
The same app is on the Examples tab at /ai/latest/docs/framework/react/examples/evaluate.
The full example is on GitHub: TanStack/ai examples/react/evaluate.
How it works#
decide() sends the ticket as state and asks every question about it. Each helper gives a different answer shape:
- choice: value is one option key. You also get probability and confidence.
- score: value is the nearest level label. score holds the raw number, and you also get probability.
- boolean: value is true when the probability of yes is 0.5 or more. You also get probability.
The result also has a meta field with the model and the usage.
Swap the provider#
The decide() call stays the same. Only the adapter changes:
- OpenRouter: openRouterDecider('~typesafe/jev-latest') with OPENROUTER_API_KEY.
- Vercel AI Gateway: vercelGatewayDecider('typesafe-ai/jev') with AI_GATEWAY_API_KEY.
- Cloudflare: cloudflareDecider('typesafe/jev') with a Worker binding, or with CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN.
For abort, middleware, and the full result shape, open the Evaluate guide.