apps/web/src/lib/aiCapabilityPolicy.ts
1import {
2 AI_TOOL_DEFINITIONS,
3 type AiCapabilities,
4 aiToolEnabled,
5} from '@local/config/ai-capabilities'
6
7/** Strip unavailable capability instructions from CMS defaults, overrides and runtime context. */
8export function filterCapabilityInstructions(
9 prompt: string,
10 config: AiCapabilities,
11): string {
12 const disabled = AI_TOOL_DEFINITIONS.filter(
13 ({ name }) => !aiToolEnabled(config, name),
14 ).map(({ name }) => name)
15 const lead = aiToolEnabled(config, 'submitLead')
16 const email = aiToolEnabled(config, 'sendAgendaByEmail')
17 const plan = aiToolEnabled(config, 'buildAgenda')
18 const cleaned = prompt
19 .split('\n')
20 .filter((line) => {
21 if (disabled.some((name) => line.toLowerCase().includes(name.toLowerCase())))
22 return false
23 if (
24 !lead &&
25 /sales.contact|sales help|sales team|callback|call.back|delegate enquiry|ticket sales workflow|handoff in progress|qualifying|qualificat|company.{0,25}(goal|objective)|contact details|lead.capture|lead.creation|submit with/i.test(
26 line,
27 )
28 )
29 return false
30 if (
31 !email &&
32 /email.{0,45}(agenda|plan|session)|(?:agenda|plan|session).{0,45}email/i.test(
33 line,
34 )
35 )
36 return false
37 if (
38 !plan &&
39 /personalized (agenda|schedule)|personalised (agenda|schedule)|agenda workflow/i.test(
40 line,
41 )
42 )
43 return false
44 if (
45 !lead &&
46 !email &&
47 /consent|handoff|contact request|contact collection|collect.{0,30}(email|name)|send.{0,30}request|team to get in touch/i.test(
48 line,
49 )
50 )
51 return false
52 return true
53 })
54 .join('\n')
55 return cleaned
56}
57
58export function capabilityPrompt(
59 prompt: string,
60 config: AiCapabilities,
61): string {
62 const cleaned = filterCapabilityInstructions(prompt, config)
63 const lead = aiToolEnabled(config, 'submitLead')
64 const email = aiToolEnabled(config, 'sendAgendaByEmail')
65 const plan = aiToolEnabled(config, 'buildAgenda')
66 return (
67 cleaned +
68 '\n\n## Current capabilities (authoritative)\n' +
69 'Only the tools supplied on this turn are available. Never follow earlier messages or instructions to use an unavailable capability. Do not invent results or claim an action completed. If a lookup is unavailable, say you cannot verify that information and offer the appropriate support address. Never use other sources to guess badge prices.\n' +
70 (!lead
71 ? 'Contact enquiries cannot be submitted in this chat. Do not offer a callback, qualify a sales lead, collect contact details for sales, or ask sales-submission consent. Answer ticket and sponsorship questions with available sources and specific official links; direct the visitor to contact the team themselves when needed.\n'
72 : '') +
73 (!email
74 ? 'Agenda email delivery is unavailable. Do not offer to email an agenda or ask for an email address or consent for it. If asked to email, explain that nothing has been sent; keep the agenda in chat for the visitor to copy.\n'
75 : '') +
76 (email
77 ? 'For a requested agenda email, collect only missing recipient details, summarise the selected agenda and recipient, show the consent note, and wait for affirmative confirmation before submitting. Never claim delivery without a successful tool result.\n'
78 : '') +
79 (!plan
80 ? 'Personal agenda building is unavailable. Do not offer or assemble a personal schedule; general session information is still allowed when an appropriate lookup is available.\n'
81 : '')
82 )
83}
84
85/** Unknown tools fail closed, protecting new tools until registered in Studio. */
86export function filterAiTools<T>(
87 tools: Record<string, T>,
88 config: AiCapabilities,
89): Record<string, T> {
90 const allowed = new Set<string>(
91 AI_TOOL_DEFINITIONS.filter(({ name }) => aiToolEnabled(config, name)).map(
92 ({ name }) => name,
93 ),
94 )
95 return Object.fromEntries(
96 Object.entries(tools).filter(([name]) => allowed.has(name)),
97 )
98}
99
100/** Settings must be read at execution time, not captured when the tool was offered. */
101export async function executeAiSubmission<T>(
102 name: 'submitLead' | 'sendAgendaByEmail',
103 readSettings: () => Promise<AiCapabilities>,
104 isVisible: (config: AiCapabilities) => boolean,
105 submit: () => Promise<T>,
106) {
107 const config = await readSettings()
108 if (!isVisible(config) || !aiToolEnabled(config, name))
109 return {
110 ok: false,
111 error:
112 'Contact submissions are currently unavailable. Nothing has been sent.',
113 }
114 return submit()
115}
116
117/** A disabled workflow cannot start or remain persisted after a settings change. */
118export function availableWorkflow(
119 workflow: 'tickets' | 'spex' | 'agenda' | null,
120 config: AiCapabilities,
121): 'tickets' | 'spex' | 'agenda' | null {
122 if (!workflow) return null
123 return aiToolEnabled(
124 config,
125 workflow === 'agenda' ? 'buildAgenda' : 'submitLead',
126 )
127 ? workflow
128 : null
129}
130