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