EventBuzz_Backend/src/utils/conciergeIntent.js

208 lines
5.2 KiB
JavaScript

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,
};