208 lines
5.2 KiB
JavaScript
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,
|
|
};
|
|
|