const { createBedrockClient, getBedrockConfig } = require('./bedrockClient'); const { converse, responseText } = require('./bedrockConverse'); const INTENT_CATEGORIES = { EVENT_INFORMATION: 'Event Information', PERSONAL_INFORMATION: 'Personal Information', FOOD: 'Food', PLACES: 'Places', UNRECOGNIZED: 'unrecognized', }; function tryParse(s) { if (!s) return null; const trimmed = String(s).trim(); try { return JSON.parse(trimmed); } catch { const fenced = trimmed.match(/```json\s*([\s\S]*?)\s*```/i) || trimmed.match(/```\s*([\s\S]*?)\s*```/i); if (fenced?.[1]) { try { return JSON.parse(fenced[1]); } catch {} } const objectMatch = trimmed.match(/\{[\s\S]*\}/); if (objectMatch?.[0]) { try { return JSON.parse(objectMatch[0]); } catch {} } return null; } } function classifyByRules(message) { const text = String(message || '').toLowerCase(); // PERSONAL if ( /\b(my|am i|me)\b/.test(text) && /\b(ticket|badge|registration|registered|qr|check.?in|seat|profile)\b/.test(text) ) { return { category: 'Personal Information', databaseSearchNeeded: true }; } // EVENT if ( /\b(session|sessions|schedule|agenda|speaker|speakers|workshop|workshops|keynote|program)\b/.test( text, ) ) { return { category: 'Event Information', databaseSearchNeeded: true }; } // FOOD if ( /\b(food|eat|restaurant|cafe|vendor|menu|breakfast|lunch|dinner|coffee|tea|vegetarian|vegan)\b/.test( text, ) ) { return { category: 'Food', databaseSearchNeeded: true }; } // PLACES if ( /\b(hall|room|booth|parking|restroom|bathroom|gate|entrance|exit|directions|map|stage)\b/.test( text, ) ) { return { category: 'Places', databaseSearchNeeded: true }; } // GREETINGS if (/\b(hello|hi|hey|good morning|good afternoon)\b/.test(text)) { return { category: 'unrecognized', databaseSearchNeeded: false }; } return null; } function buildIntentClassificationPrompt() { return `You are an intent classifier for an event assistant. Classify the CURRENT USER MESSAGE into exactly ONE category from: - Event Information - Personal Information - Food - Places - unrecognized Categories: - Event Information: schedules, sessions, workshops, speakers, agendas, programs, event details - Personal Information: my registration, my ticket, badge, QR code, check-in, attendee profile - Food: food, meals, restaurants, cafes, vendors, dietary preferences - Places: halls, rooms, parking, restrooms, directions, booths - unrecognized: greetings, compliments, or unrelated requests Rules: - If the message is about the user themselves (ticket/badge/QR/check-in/profile), use Personal Information. - If the message is about what/when/where inside the event (schedule, speakers, sessions, workshops, agenda, program), use Event Information. - If the message asks what to eat or where to eat, use Food. - If the message asks for navigation inside the venue (directions/maps/where is X room/stage/booth), use Places. Also decide whether the backend should perform a knowledge base lookup: - databaseSearchNeeded: true when the answer depends on event knowledge or places/food knowledge. - databaseSearchNeeded: false for greetings or non-actionable messages. Output format (STRICT): Return ONLY a valid minified JSON object with this exact schema: {"category":"Event Information"|"Personal Information"|"Food"|"Places"|"unrecognized","databaseSearchNeeded":true|false} `; } function normalizeMessagesToText(messages) { if (!Array.isArray(messages)) return ''; return messages .slice(-6) .map((m) => { const role = m?.role === 'assistant' ? 'Assistant' : 'User'; const text = String(m?.text || '').trim(); return `${role}: ${text}`.trim(); }) .filter(Boolean) .join('\n'); } async function determineIntentCategory(chatContextMessages = []) { const latestUserMessage = [...chatContextMessages] .reverse() .find(m => m.role === 'user') ?.text || ''; // Rule-based classification first const ruleResult = classifyByRules(latestUserMessage); if (ruleResult) { return ruleResult; } // Fallback to Nova Lite const { region, modelId } = getBedrockConfig(); const client = createBedrockClient(region); const system = buildIntentClassificationPrompt(); const userText = ` CURRENT USER MESSAGE: ${latestUserMessage} Return JSON only. `; const response = await converse(client, modelId, { system: [{ text: system }], messages: [ { role: 'user', content: [{ text: userText }] } ], inferenceConfig: { temperature: 0, topP: 0.01, maxTokens: 50 } }); const raw = responseText(response); const parsed = tryParse(raw); const category = parsed?.category && Object.values(INTENT_CATEGORIES).includes(parsed.category) ? parsed.category : INTENT_CATEGORIES.UNRECOGNIZED; const databaseSearchNeeded = typeof parsed?.databaseSearchNeeded === 'boolean' ? parsed.databaseSearchNeeded : false; return { category, databaseSearchNeeded }; } module.exports = { determineIntentCategory, INTENT_CATEGORIES, };