Skip to content

Commit 6008670

Browse files
isaac-vicentiniadminSapphicFire
authored
Add !quiz command (#360)
* SlackerBot Event Handler: - Added logic to handle Slack interactive message responses for the quiz feature. - Implemented content-type based logic to differentiate between Default messages, Challenge requests and Quiz interactions. - Captured and processed the user selected quiz answer, comparing it against the correct answer. - Updated the bot to send a response message back to the user indicating whether the answer was correct or not. * Create a new SN Quiz.js - Implemented functionality to capture the quiz topic from the Slack command using regex. - Integrated OpenAI API to generate multiple-choice quiz questions dynamically based on the quiz topic provided. - Processed and formatted the AI response into structured JSON, containing the question, options, correct answer, and explanation. - Enhanced Slack integration using Block Kit to present the quiz question with interactive buttons for each option. * Scripted Rest API: - Updated the interaction script to validate user selections and provide feedback based on the correctness of the response. - Added dynamic button styling (primary/danger) to visually indicate correct and incorrect answers. - Improved feedback messages with detailed explanations for correct answers. - Integrated the update_chat method to ensure real-time message updates in Slack with button styling and quiz results. Slacker Script Include: - Enhanced maintainability by centralizing the Slack message update logic in a dedicated Script Include function. * Update - Create a new SN Quiz.js - Added error handling to manage unexpected responses from the OpenAI API. - Improved user interaction by formatting the question and answers using Slack Block Kit. - Ensured that correct answers are validated and passed along with the user selection for feedback. * Update Create a new SN Quiz.js Initial comments inserted * The API - Event Handler has been updated with some adjustments and improvements: - Fix: The Script Include Slacker() is no longer instantiated twice. - The block only accepts interaction if the user interacting with the block is the same user who sent the command. - All other blocks automatically turn red after the user selects the correct option. - Now the bot briefly explains why the option is correct. - A note has been added so the user knows that only the person who sent the command can answer the question. * Update Create a new SN Quiz.js The script has been updated with some adjustments and improvements: - The model used in the OpenAI integration has been changed to gpt-4o. - The ID of the user who asked the question is being retrieved and sent for interaction validation. - A greeting was added to the quiz so that the person who asked the question knows it is directed at them. - A note has been added to inform the user that only the person who sent the command can answer the quiz. * A new function has been created to validate whether the bot can interact in the channel. The function checks if the original message was sent by a user and if the interaction is of the type “block_actions” - Necessary for setting up the quiz. The function checks if the interaction contains a bot_id, and if it does, two things are validated: 1. If the type of interaction is “message_changed” - Necessary for the bot to update the styles of the blocks (colors). 2. If the thread was started by a user - Necessary for the bot to respond to the correct question. This way, it does not get stuck in a loop responding to itself. If the interaction does not have a bot_id, then the message can be sent. --------- Co-authored-by: admin <admin@example.com> Co-authored-by: Astrid Sapphire <59789839+SapphicFire@users.noreply.github.com>
1 parent 64407b5 commit 6008670

4 files changed

Lines changed: 268 additions & 25 deletions

File tree

Parsers/Create a new SN Quiz.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
activation_example:!quiz itsm
3+
regex:!quiz\s+(.+)
4+
flags:gmi
5+
*/
6+
7+
(function(current) {
8+
var slacker = new x_snc_slackerbot.Slacker();
9+
10+
// Capture the text and extract the quiz topic using regex
11+
var text = current.text;
12+
var match = text.match(/!quiz\s+(.+)/i);
13+
14+
if (!match) {
15+
slacker.send_chat(current, "Please provide a valid ServiceNow quiz topic, e.g., !quiz ITSM.", false);
16+
return;
17+
}
18+
19+
var quizTopic = match[1].trim();
20+
var originalUserId = current.user.user_id;
21+
22+
try {
23+
// Openai Integration - Generate Quiz
24+
var apiKey = gs.getProperty("openai.key");
25+
26+
var restMessage = new sn_ws.RESTMessageV2();
27+
restMessage.setEndpoint('https://api.openai.com/v1/chat/completions');
28+
restMessage.setHttpMethod('POST');
29+
restMessage.setRequestHeader('Authorization', 'Bearer ' + apiKey);
30+
restMessage.setRequestHeader('Content-Type', 'application/json');
31+
32+
var requestBody = {
33+
"model": "gpt-4o",
34+
"messages": [{
35+
"role": "system",
36+
"content": "You are an assistant that creates structured multiple-choice quiz questions about ServiceNow topics. Return the question and answers in JSON format."
37+
},
38+
{
39+
"role": "user",
40+
"content": "Please generate one multiple-choice quiz question on the topic of " + quizTopic + ". The answers should not include any prefixes like 'A)', '1.', etc. Just provide the plain answer text. Return the result in the following JSON format:\n{\n \"question\": \"<question text>\",\n \"answers\": [\"option 1\", \"option 2\", \"option 3\", \"option 4\"],\n \"correct\": \"<correct answer>\",\n \"explanation\": \"<why this option is correct>\"\n}"
41+
}
42+
],
43+
"max_tokens": 200
44+
};
45+
46+
47+
restMessage.setRequestBody(JSON.stringify(requestBody));
48+
var response = restMessage.execute();
49+
50+
if (response.getStatusCode() !== 200) {
51+
throw new Error('Received non-200 response from OpenAI: ' + response.getStatusCode());
52+
}
53+
54+
var responseBody = response.getBody();
55+
var jsonResponse = JSON.parse(responseBody);
56+
57+
if (!jsonResponse.choices || !jsonResponse.choices[0].message || !jsonResponse.choices[0].message.content) {
58+
throw new Error('Unexpected response format from OpenAI');
59+
}
60+
61+
var quizData = jsonResponse.choices[0].message.content;
62+
var quizQuestion = JSON.parse(quizData);
63+
64+
// Find the correct answer
65+
var correctOptionIndex = "";
66+
quizQuestion.answers.forEach(function(answer, index) {
67+
if (answer === quizQuestion.correct) {
68+
correctOptionIndex = "option_" + (index + 1);
69+
}
70+
});
71+
72+
// Prepare Slack Block Kit for the quiz question and answers
73+
var optionLetters = ['A', 'B', 'C', 'D'];
74+
var blocks = [{
75+
"type": "section",
76+
"text": {
77+
"type": "mrkdwn",
78+
"text": "Hello, " + current.user.name + "! Test your ServiceNow skills and have fun! \n\n" +
79+
"*Question:* " + quizQuestion.question + "\n\n" +
80+
optionLetters[0] + ") " + quizQuestion.answers[0] + "\n" +
81+
optionLetters[1] + ") " + quizQuestion.answers[1] + "\n" +
82+
optionLetters[2] + ") " + quizQuestion.answers[2] + "\n" +
83+
optionLetters[3] + ") " + quizQuestion.answers[3] + "\n\n" +
84+
"_*Note:* Only the person who initiated the quiz can answer._"
85+
}
86+
},
87+
{
88+
"type": "actions",
89+
"elements": quizQuestion.answers.map(function(answer, index) {
90+
return {
91+
"type": "button",
92+
"text": {
93+
"type": "plain_text",
94+
"text": optionLetters[index]
95+
},
96+
97+
"value": "option_" + (index + 1) + "|" + correctOptionIndex + "|" + originalUserId + "|" + quizQuestion.explanation,
98+
"action_id": "quiz_answer_" + (index + 1)
99+
};
100+
})
101+
}
102+
];
103+
104+
// Send Slack message - Quiz
105+
slacker.send_chat(current, {
106+
"blocks": blocks
107+
}, false);
108+
109+
} catch (error) {
110+
gs.error('[SLACKER - QUIZ] Error generating quiz: ' + error.message);
111+
slacker.send_chat(current, "Sorry, there was an issue generating the quiz. Please try again later.", false);
112+
}
113+
114+
})(current);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
OXfnAqsApch05cMfRB7DnJnPrhgcNBWxHmqCEtplh9J-0tLkq_ahRkN6ATYblVZEi2dpPa-gS8aTEn4YpeMwCQGYt9UEZjvZbU7yGt79Fm8sKCUTxnt5kPioAP4h13NbBnbSGJAhf796X8mkDhRpthIKIVKAhAkv5qYjRbohRG5HJ8mB4l3pvDIPxsIFEYph5eHyX9ej49Meb1ngqjTdRF7_e8vzqM8gjx2892GdF7gynrmIOpZDjcmJ9miakQWjIFpRGlS1UkhaiA6EeaiDukz6V0iP81OlXlbXghRxamvDwaiXjfemlDHY_R0xmSxb7uJ9eBCvgXmJkFQriOM1Uqu7SvcnfE3jvRcAw22U-snz-VTZxOBhvdsdf2ULyz_5OqsxAiGF3f39xg42lc3jqi8wGubgMvFUvdKCnpoMywoC6tGYX7eXLaAqIdQkAmuK3nXuWYbsl5Q-dM_iRs0sXQ9DK0uEUSBLz8wBjlQXXihsT8Z96NpSz6xg6YPA6ZENNuyUSak2dsOZsoCsQo7Mo-ZbBbbDBScyyU3b42OZ3O0HUx6qpIQxl8CNw0mjJyk30yzThMxOvV5-hgdfcFLWDV3VbNmv65HElwxU4Hc4J6uKHuLyBUzcnx5pXIgM4o0fktAygrdHTP0rToqxGCDsuhLlQnqfNtM0NfrrgFDQAws
1+
1Xbk9WxmheTaW0AgaPLvwbV3zWsmHQtuzDv3BzXUTOXkkeTOT3dWqOrS7c8vRhhL57_VWPRyJeeP5HfoSHF21CYx49uZ6ejBWpfI8oo79_rSECBBj-o5imozu6nwjoIoqchvEwga8PlPmyZBkslRcpawEqlirCLUNQh0gPJdYMPsSOCAJbjOIWXl8qdsyR_c5fhsNb0VxlciIVbYn6KBZnC_-QzBu1Z7E0lSqeISpirHNr-Y_Et56ZIL-fIAl5Oh-L-WARn_-Ei8lgvy-zs1mIvLyy9rOsKirmI5jzRYCAfO5lvltsjGDfJnCRAr4aRHE5w5-lSmLSgX6mR4EBTc8FkryIbfOHZXsp3HSSJGLVpNYn7n77O2V-F9cNrOidCpmWlXVANq9GpgCmJoeO3TrCmJVwmyuF_3qok2zuxsKR2dCSHVjr2THKK_CmyuQIeaMtUGIxSbw5ngkRVcz3pCIfMvCnfjNLHwPf2EOQgNC8bHkYBOylKsnvnOG9cPiquKE_aS_9lkCR0H_JLQsOGDndDBNOVRqcONO8p1JsFrkP5ihhqAmNjAz8jrRi-cUfyynNVH4FticFbBrNm7pw5aYOq1nwok0EN7SCwNX-LfoUh8uk4HNrPzQvY1Pu9IXZnFpAaTtcK-fg_sAOMVs55itBxzQOVZQjbSC2sORre2P5U

b02cf9e61b861d10d806dc23b24bcb3f/update/sys_script_include_b2c246ae1b861d10d806dc23b24bcbac.xml

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ Slacker.prototype = {
2626
}
2727
}
2828
29-
if (slackoff_user.getValue('name_updated') < gs.daysAgoStart(0)) {
30-
this.get_user_info(payload_user, userSysID); //Update user info once per day, in case of name change.
31-
}
32-
29+
if (slackoff_user.getValue('name_updated') < gs.daysAgoStart(0)) {
30+
this.get_user_info(payload_user, userSysID); //Update user info once per day, in case of name change.
31+
}
32+
3333
return userSysID;
3434
},
3535
@@ -73,6 +73,32 @@ Slacker.prototype = {
7373
return rm.execute();
7474
},
7575
76+
/**
77+
* Updates a message on Slack
78+
* @example
79+
* var payload = {
80+
* "channel": "D0XXXT13XXX", // Channel ID
81+
* "ts": 1728146070.000000, // Message timestamp
82+
* "blocks": blocks, // Optional if the message is block
83+
* "text": message // If the message is text
84+
* };
85+
* new x_snc_slackerbot.Slacker().update_chat(payload);
86+
* @param {object} payload Object containing the payload to be sent to the Slack API
87+
* @returns {RESTResponseV2} RESTResponseV2 object containing response payload, and headers
88+
*/
89+
update_chat: function(payload) {
90+
var rmUpdateMessage = new sn_ws.RESTMessageV2();
91+
rmUpdateMessage.setEndpoint('https://slack.com/api/chat.update');
92+
rmUpdateMessage.setHttpMethod('POST');
93+
rmUpdateMessage.setRequestHeader('Authorization', 'Bearer ' + gs.getProperty('x_snc_slackerbot.SlackerBot.token'));
94+
rmUpdateMessage.setRequestHeader('Content-Type', 'application/json');
95+
rmUpdateMessage.setRequestBody(JSON.stringify(payload));
96+
97+
var response = rmUpdateMessage.execute();
98+
var responseBody = response.getBody();
99+
return responseBody;
100+
},
101+
76102
send_delete: function(channel, ts) {
77103
var rm = new sn_ws.RESTMessageV2();
78104
rm.setHttpMethod('POST');
@@ -109,7 +135,7 @@ Slacker.prototype = {
109135
var grupdate = new GlideRecord('x_snc_slackerbot_user');
110136
grupdate.get(record_id);
111137
grupdate.setValue('name', response_body.user.real_name);
112-
grupdate.setValue('name_updated', new GlideDateTime());
138+
grupdate.setValue('name_updated', new GlideDateTime());
113139
grupdate.update();
114140
return true;
115141
} else {
@@ -160,13 +186,13 @@ Slacker.prototype = {
160186
<sys_created_by>earl.duque</sys_created_by>
161187
<sys_created_on>2022-09-23 06:06:00</sys_created_on>
162188
<sys_id>b2c246ae1b861d10d806dc23b24bcbac</sys_id>
163-
<sys_mod_count>3</sys_mod_count>
189+
<sys_mod_count>7</sys_mod_count>
164190
<sys_name>Slacker</sys_name>
165191
<sys_package display_value="SlackerBot" source="x_snc_slackerbot">b02cf9e61b861d10d806dc23b24bcb3f</sys_package>
166192
<sys_policy>read</sys_policy>
167193
<sys_scope display_value="SlackerBot">b02cf9e61b861d10d806dc23b24bcb3f</sys_scope>
168194
<sys_update_name>sys_script_include_b2c246ae1b861d10d806dc23b24bcbac</sys_update_name>
169-
<sys_updated_by>SapphicFire</sys_updated_by>
170-
<sys_updated_on>2023-11-04 10:48:44</sys_updated_on>
195+
<sys_updated_by>admin</sys_updated_by>
196+
<sys_updated_on>2024-10-05 17:42:35</sys_updated_on>
171197
</sys_script_include>
172198
</record_update>

b02cf9e61b861d10d806dc23b24bcb3f/update/sys_ws_operation_bbcb42a61bc61d10d806dc23b24bcbd2.xml

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,126 @@
11
<?xml version="1.0" encoding="UTF-8"?><record_update table="sys_ws_operation">
22
<sys_ws_operation action="INSERT_OR_UPDATE">
33
<active>true</active>
4-
<consumes>application/json,application/xml,text/xml</consumes>
5-
<consumes_customized>false</consumes_customized>
4+
<consumes>application/json,application/xml,text/xml,application/x-www-form-urlencoded</consumes>
5+
<consumes_customized>true</consumes_customized>
66
<default_operation_uri/>
77
<enforce_acl>cf9d01d3e73003009d6247e603f6a990</enforce_acl>
88
<http_method>POST</http_method>
99
<name>Event</name>
10-
<operation_script><![CDATA[(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
11-
12-
if (request.body.data.challenge){
13-
response.setStatus(200);
14-
response.setContentType('text/plain');
15-
response.getStreamWriter().writeString(request.body.data.challenge);
16-
} else {
17-
if (request.body.data.event.bot_id) return;
18-
//gs.info('slackoff: ' + (request.body.dataString));
19-
var payload = new GlideRecord('x_snc_slackerbot_payload');
20-
payload.initialize();
21-
payload.setValue('payload', JSON.stringify(request.body.data, null, 2));
22-
payload.insert();
23-
}
10+
<operation_script><![CDATA[(function process( /*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
11+
var contentType = request.getHeader('Content-Type');
12+
var slackerProvider = new x_snc_slackerbot.Slacker();
13+
14+
if (contentType === 'application/json') {
15+
if (request.body.data.challenge) {
16+
response.setStatus(200);
17+
response.setContentType('text/plain');
18+
response.getStreamWriter().writeString(request.body.data.challenge);
19+
} else {
20+
var botCanAnswerJson = checkIfBotCanAnswer(request.body.data.event);
21+
if (!botCanAnswerJson) return;
22+
23+
//gs.info('slackoff: ' + (request.body.dataString));
24+
var payload = new GlideRecord('x_snc_slackerbot_payload');
25+
payload.initialize();
26+
payload.setValue('payload', JSON.stringify(request.body.data, null, 2));
27+
payload.insert();
28+
}
29+
30+
} else if (contentType === 'application/x-www-form-urlencoded') {
31+
var bodyData = request.queryParams;
32+
var formData = bodyData.payload;
33+
var interactionPayload = JSON.parse(formData);
34+
var botCanAnswerEncoded = checkIfBotCanAnswer(interactionPayload);
35+
if (!botCanAnswerEncoded) return;
36+
37+
sendQuizInteraction(bodyData, formData, interactionPayload);
38+
}
39+
40+
// ###### Slacker QUIZ ###### //
41+
function sendQuizInteraction(bodyData, formData) {
42+
// Get selected option, correct option, the user ID that initiated the quiz, and explanation
43+
var action = interactionPayload.actions[0];
44+
var selectedOption = action.value;
45+
var parts = selectedOption.split("|");
46+
var userSelection = parts[0];
47+
var correctOption = parts[1];
48+
var originalUserId = parts[2];
49+
var explanation = parts[3];
50+
51+
// Verify if the interacting user is the same as the original user
52+
if (interactionPayload.user.id !== originalUserId) return;
53+
54+
// Apply styles to each button based on whether it was selected and whether it's correct
55+
var blocks = interactionPayload.message.blocks;
56+
var correctIndex = parseInt(correctOption.split('_')[1]) - 1;
57+
58+
blocks[1].elements.forEach(function(button, index) {
59+
if (userSelection === correctOption) {
60+
button.style = (index === correctIndex) ? "primary" : "danger";
61+
} else {
62+
if (button.value === selectedOption) {
63+
button.style = "danger";
64+
}
65+
}
66+
});
67+
68+
var updateMessagePayload = {
69+
"channel": interactionPayload.channel.id,
70+
"ts": interactionPayload.message.ts,
71+
"blocks": blocks,
72+
"text": "UpdatingQuiz"
73+
};
74+
75+
var responseMessageUpdated = slackerProvider.update_chat(updateMessagePayload);
76+
77+
// Validate User Answer and send appropriate feedback
78+
var current = {
79+
"text": interactionPayload.message.blocks[0].text.text,
80+
"ts": interactionPayload.message.ts,
81+
"thread_ts": interactionPayload.container.message_ts,
82+
"channel": interactionPayload.channel.id,
83+
"user": {
84+
"user_id": interactionPayload.user.id,
85+
"name": interactionPayload.user.name
86+
}
87+
};
88+
89+
var questionText = interactionPayload.message.blocks[0].text.text;
90+
var textLines = questionText.split("\n");
91+
var correctAnswerText = textLines[correctIndex + 4].trim();
92+
var correctAnswer = correctAnswerText.replace(/^[A-D]\)\s*/, '');
93+
94+
if (userSelection === correctOption) {
95+
var correctMessage = "Well done! The correct answer is:\n*" + correctAnswer + "*\n\n" + explanation;
96+
slackerProvider.send_chat(current, correctMessage, false);
97+
} else {
98+
var incorrectMessage = "Oops, not quite! Try Again.";
99+
slackerProvider.send_chat(current, incorrectMessage, false);
100+
}
101+
}
102+
103+
function checkIfBotCanAnswer(requestBodyEvent) {
104+
var botCanAnswer = false;
105+
106+
// Allows response if the type is block_action and the message was sent by a user
107+
if(requestBodyEvent.user && requestBodyEvent.user.username && requestBodyEvent.type === "block_actions") return true;
108+
109+
if (requestBodyEvent.bot_id) {
110+
// Allows response if the message subtype is message update
111+
if (requestBodyEvent.subtype && requestBodyEvent.subtype === "message_changed") {
112+
botCanAnswer = true;
113+
}
114+
// Allows response if the thread was started by a user
115+
else if (requestBodyEvent.parent_user_id && requestBodyEvent.parent_user_id !== requestBodyEvent.user) {
116+
botCanAnswer = true;
117+
}
118+
} else {
119+
botCanAnswer = true;
120+
}
121+
122+
return botCanAnswer;
123+
}
24124
25125
})(request, response);]]></operation_script>
26126
<operation_uri>/api/x_snc_slackerbot/slackerbot_event_handler</operation_uri>
@@ -36,11 +136,14 @@
36136
<sys_created_by>earl.duque</sys_created_by>
37137
<sys_created_on>2022-09-23 06:36:30</sys_created_on>
38138
<sys_id>bbcb42a61bc61d10d806dc23b24bcbd2</sys_id>
139+
<sys_mod_count>105</sys_mod_count>
39140
<sys_name>Event</sys_name>
40141
<sys_package display_value="SlackerBot" source="x_snc_slackerbot">b02cf9e61b861d10d806dc23b24bcb3f</sys_package>
41142
<sys_policy/>
42143
<sys_scope display_value="SlackerBot">b02cf9e61b861d10d806dc23b24bcb3f</sys_scope>
43144
<sys_update_name>sys_ws_operation_bbcb42a61bc61d10d806dc23b24bcbd2</sys_update_name>
145+
<sys_updated_by>admin</sys_updated_by>
146+
<sys_updated_on>2024-10-11 00:48:14</sys_updated_on>
44147
<web_service_definition display_value="SlackerBot Event Handler">1fabce661bc61d10d806dc23b24bcbd6</web_service_definition>
45148
<web_service_version/>
46149
</sys_ws_operation>

0 commit comments

Comments
 (0)