apps/web/src/lib/ticketAnswer.server.ts
1import { createGateway } from '@ai-sdk/gateway'
2import { experimental_evaluate as evaluate } from 'ai'
3import { type TicketAnswerKind, ticketAnswerKind } from './ticketAnswer'
4import { ticketQuestions } from './ticketQuestions'
5
6type Turn = { role: 'user' | 'assistant'; text: string }
7
8/** Bounded semantic interpretation only: never extracts money or authorizes a handoff. */
9export async function resolveTicketAnswerKind(
10 message: string,
11 history: Turn[],
12 signal?: AbortSignal,
13): Promise<TicketAnswerKind> {
14 const baseline = ticketAnswerKind(message, history)
15 const key = process.env.AI_GATEWAY_API_KEY || process.env.AI_GATEWAY
16 if (process.env.AI_TICKET_JEV_MODE !== 'active' || !key) return baseline
17 const previous =
18 history.at(-1)?.role === 'user' && history.at(-1)?.text === message
19 ? history.slice(0, -1)
20 : history
21 const messages = previous.slice(-6)
22 if (
23 !/\b(?:tickets?|badges?|passes|pass|admission|diamond|explorer)\b/i.test(
24 `${message} ${messages.map((t) => t.text).join(' ')}`,
25 )
26 )
27 return baseline
28 try {
29 const r = await evaluate({
30 model: createGateway({ apiKey: key }).evaluationModel('typesafe-ai/jev'),
31 state: {
32 message,
33 messages,
34 catalogue: [
35 'General Admission / General Attendee',
36 'Explorer (company visits)',
37 'Diamond (Day 1 leadership sessions)',
38 ],
39 },
40 questions: ticketQuestions,
41 maxRetries: 0,
42 abortSignal: signal
43 ? AbortSignal.any([signal, AbortSignal.timeout(1500)])
44 : AbortSignal.timeout(1500),
45 providerOptions: { gateway: { zeroDataRetention: true } },
46 })
47 if (r.answers.ticket.probability < 0.8) return 'other'
48 const answer = r.answers.kind
49 return (answer.probabilities?.[answer.choice] ?? 0) >= 0.8
50 ? answer.choice
51 : baseline
52 } catch {
53 return baseline
54 }
55}
56