Greg Thompson commited on
Commit
67c73c6
2 Parent(s): b825f41 8fd1f61

Merge branch 'vlad' into 'main'

Browse files

Count up quiz

See merge request tangibleai/community/mathtext-fastapi!10

app.py CHANGED
@@ -1,7 +1,11 @@
1
  """FastAPI endpoint
2
  To run locally use 'uvicorn app:app --host localhost --port 7860'
3
  """
4
- import re
 
 
 
 
5
 
6
  from fastapi import FastAPI, Request
7
  from fastapi.responses import JSONResponse
@@ -100,3 +104,168 @@ async def evaluate_user_message_with_nlu_api(request: Request):
100
  message_data = data_dict.get('message_data', '')
101
  nlu_response = evaluate_message_with_nlu(message_data)
102
  return JSONResponse(content=nlu_response)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """FastAPI endpoint
2
  To run locally use 'uvicorn app:app --host localhost --port 7860'
3
  """
4
+ import ast
5
+ import scripts.quiz.generators as generators
6
+ import scripts.quiz.hints as hints
7
+ import scripts.quiz.questions as questions
8
+ import scripts.quiz.utils as utils
9
 
10
  from fastapi import FastAPI, Request
11
  from fastapi.responses import JSONResponse
 
104
  message_data = data_dict.get('message_data', '')
105
  nlu_response = evaluate_message_with_nlu(message_data)
106
  return JSONResponse(content=nlu_response)
107
+
108
+
109
+ @app.post("/question")
110
+ async def ask_math_question(request: Request):
111
+ """Generate a question and return it as response along with question data
112
+
113
+ Input
114
+ request.body: json - amount of correct and incorrect answers in the account
115
+ {
116
+ 'number_correct': 0,
117
+ 'number_incorrect': 0,
118
+ 'level': 'easy'
119
+ }
120
+
121
+ Output
122
+ context: dict - the information for the current state
123
+ {
124
+ 'text': 'What is 1+2?',
125
+ 'question_numbers': [1,2,3], #3 numbers - current number, ordinal number, times
126
+ 'right_answer': 3,
127
+ 'number_correct': 0,
128
+ 'number_incorrect': 0,
129
+ 'hints_used': 0
130
+ }
131
+ """
132
+ data_dict = await request.json()
133
+ message_data = ast.literal_eval(data_dict.get('message_data', '').get('message_body', ''))
134
+ right_answers = message_data['number_correct']
135
+ wrong_answers = message_data['number_incorrect']
136
+ level = message_data['level']
137
+
138
+ return JSONResponse(generators.start_interactive_math(right_answers, wrong_answers, level))
139
+
140
+
141
+ @app.post("/hint")
142
+ async def get_hint(request: Request):
143
+ """Generate a hint and return it as response along with hint data
144
+
145
+ Input
146
+ request.body:
147
+ {
148
+ 'question_numbers': [1,2,3], #3 numbers - current number, ordinal number, times
149
+ 'right_answer': 3,
150
+ 'number_correct': 0,
151
+ 'number_incorrect': 0,
152
+ 'level': 'easy',
153
+ 'hints_used': 0
154
+ }
155
+
156
+ Output
157
+ context: dict - the information for the current state
158
+ {
159
+ 'text': 'What is 1+2?',
160
+ 'question_numbers': [1,2,3], #2 or 3 numbers
161
+ 'right_answer': 3,
162
+ 'number_correct': 0,
163
+ 'number_incorrect': 0,
164
+ 'level': 'easy',
165
+ 'hints_used': 0
166
+ }
167
+ """
168
+ data_dict = await request.json()
169
+ message_data = ast.literal_eval(data_dict.get('message_data', '').get('message_body', ''))
170
+ question_numbers = message_data['question_numbers']
171
+ right_answer = message_data['right_answer']
172
+ number_correct = message_data['number_correct']
173
+ number_incorrect = message_data['number_incorrect']
174
+ level = message_data['level']
175
+ hints_used = message_data['hints_used']
176
+
177
+ return JSONResponse(hints.generate_hint(question_numbers, right_answer, number_correct, number_incorrect, level, hints_used))
178
+
179
+
180
+ @app.post("/generate_question")
181
+ async def generate_question(request: Request):
182
+ """Generate a bare question and return it as response
183
+
184
+ Input
185
+ request.body: json - level
186
+ {
187
+ 'level': 'easy'
188
+ }
189
+
190
+ Output
191
+ context: dict - the information for the current state
192
+ {
193
+ "question": "Let's count up by 2s. What number is next if we start from 10?
194
+ 6 8 10 ..."
195
+ }
196
+ """
197
+ data_dict = await request.json()
198
+ message_data = ast.literal_eval(data_dict.get('message_data', '').get('message_body', ''))
199
+ level = message_data['level']
200
+
201
+ return JSONResponse(questions.generate_question_data(level)['question'])
202
+
203
+
204
+ @app.post("/numbers_by_level")
205
+ async def get_numbers_by_level(request: Request):
206
+ """Generate three numbers and return them as response
207
+
208
+ Input
209
+ request.body: json - level
210
+ {
211
+ 'level': 'easy'
212
+ }
213
+
214
+ Output
215
+ context: dict - three generated numbers for specified level
216
+ {
217
+ "current_number": 10,
218
+ "ordinal_number": 2,
219
+ "times": 1
220
+ }
221
+ """
222
+ data_dict = await request.json()
223
+ message_data = ast.literal_eval(data_dict.get('message_data', '').get('message_body', ''))
224
+ level = message_data['level']
225
+ return JSONResponse(questions.generate_numbers_by_level(level))
226
+
227
+
228
+ @app.post("/number_sequence")
229
+ async def get_number_sequence(request: Request):
230
+ """Generate a number sequence
231
+
232
+ Input
233
+ request.body: json - level
234
+ {
235
+ "current_number": 10,
236
+ "ordinal_number": 2,
237
+ "times": 1
238
+ }
239
+
240
+ Output
241
+ one of following strings with (numbers differ):
242
+ ... 1 2 3
243
+ 1 2 3 ...
244
+ """
245
+ data_dict = await request.json()
246
+ message_data = ast.literal_eval(data_dict.get('message_data', '').get('message_body', ''))
247
+ cur_num = message_data['current_number']
248
+ ord_num = message_data['ordinal_number']
249
+ times = message_data['times']
250
+ return JSONResponse(questions.generate_number_sequence(cur_num, ord_num, times))
251
+
252
+
253
+ @app.post("/level")
254
+ async def get_next_level(request: Request):
255
+ """Depending on current level and desire to level up/down return next level
256
+
257
+ Input
258
+ request.body: json - level
259
+ {
260
+ "current_level": "easy",
261
+ "level_up": True
262
+ }
263
+
264
+ Output
265
+ Literal - "easy", "medium" or "hard"
266
+ """
267
+ data_dict = await request.json()
268
+ message_data = ast.literal_eval(data_dict.get('message_data', '').get('message_body', ''))
269
+ cur_level = message_data['current_level']
270
+ level_up = message_data['level_up']
271
+ return JSONResponse(utils.get_next_level(cur_level, level_up))
mathtext_fastapi/conversation_manager.py CHANGED
@@ -10,9 +10,13 @@ import requests
10
  from dotenv import load_dotenv
11
  from mathtext_fastapi.nlu import evaluate_message_with_nlu
12
  from mathtext_fastapi.math_quiz_fsm import MathQuizFSM
 
13
  from supabase import create_client
14
  from transitions import Machine
15
 
 
 
 
16
  load_dotenv()
17
 
18
  SUPA = create_client(
@@ -106,24 +110,27 @@ def pickle_and_encode_state_machine(state_machine):
106
  return dump_encoded
107
 
108
 
109
- def manage_math_quiz_fsm(user_message, contact_uuid):
110
  fsm_check = SUPA.table('state_machines').select("*").eq(
111
  "contact_uuid",
112
  contact_uuid
113
  ).execute()
114
 
115
  if fsm_check.data == []:
116
- math_quiz_state_machine = MathQuizFSM()
 
 
 
117
  messages = [math_quiz_state_machine.response_text]
118
  dump_encoded = pickle_and_encode_state_machine(math_quiz_state_machine)
119
 
120
  SUPA.table('state_machines').insert({
121
  'contact_uuid': contact_uuid,
122
- 'addition3': dump_encoded
123
  }).execute()
124
  else:
125
  undump_encoded = base64.b64decode(
126
- fsm_check.data[0]['addition3'].encode('utf-8')
127
  )
128
  math_quiz_state_machine = pickle.loads(undump_encoded)
129
 
@@ -132,13 +139,61 @@ def manage_math_quiz_fsm(user_message, contact_uuid):
132
  messages = math_quiz_state_machine.validate_answer()
133
  dump_encoded = pickle_and_encode_state_machine(math_quiz_state_machine)
134
  SUPA.table('state_machines').update({
135
- 'addition3': dump_encoded
136
  }).eq(
137
  "contact_uuid", contact_uuid
138
  ).execute()
139
  return messages
140
 
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  def return_next_conversational_state(context_data, user_message, contact_uuid):
143
  """ Evaluates the conversation's current state to determine the next state
144
 
@@ -159,12 +214,36 @@ def return_next_conversational_state(context_data, user_message, contact_uuid):
159
  elif context_data['state'] == 'addition-question-sequence' or \
160
  user_message == 'add':
161
 
162
- messages = manage_math_quiz_fsm(user_message, contact_uuid)
 
 
 
163
 
164
  if user_message == 'exit':
165
  state_label = 'exit'
166
  else:
167
  state_label = 'addition-question-sequence'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
  input_prompt = messages.pop()
170
 
@@ -173,15 +252,15 @@ def return_next_conversational_state(context_data, user_message, contact_uuid):
173
  'input_prompt': input_prompt,
174
  'state': state_label
175
  }
176
- elif user_message == 'subtract':
177
- message_package = {
178
- 'messages': [
179
- "Time for some subtraction!",
180
- "Type your response as a number. For example, for '1 - 1', you'd write 0."
181
- ],
182
- 'input_prompt': "Here's the first one... What's 3-1?",
183
- 'state': "subtract-question-sequence"
184
- }
185
  elif context_data['state'] == 'exit' or user_message == 'exit':
186
  message_package = {
187
  'messages': [
@@ -198,8 +277,12 @@ def return_next_conversational_state(context_data, user_message, contact_uuid):
198
  'input_prompt': "Please type add or subtract to start a math activity.",
199
  'state': "reprompt-menu-options"
200
  }
 
201
  return message_package
202
 
 
 
 
203
 
204
  def manage_conversation_response(data_json):
205
  """ Calls functions necessary to determine message and context data to send
@@ -227,11 +310,18 @@ def manage_conversation_response(data_json):
227
  # TODO: Need to incorporate nlu_response into wormhole by checking answers against database (spreadsheet?)
228
  nlu_response = evaluate_message_with_nlu(message_data)
229
 
230
- message_package = return_next_conversational_state(
231
- context_data,
232
- user_message,
233
- contact_uuid
234
- )
 
 
 
 
 
 
 
235
 
236
  print("MESSAGE PACKAGE")
237
  print(message_package)
@@ -255,15 +345,32 @@ def manage_conversation_response(data_json):
255
  )
256
 
257
  # Update the context object with the new state of the conversation
258
- context = {
259
- "context":{
260
- "user": whatsapp_id,
261
- "state": message_package['state'],
262
- "bot_message": message_package['input_prompt'],
263
- "user_message": user_message,
264
- "type": 'ask'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  }
266
- }
267
 
268
  return context
269
 
 
10
  from dotenv import load_dotenv
11
  from mathtext_fastapi.nlu import evaluate_message_with_nlu
12
  from mathtext_fastapi.math_quiz_fsm import MathQuizFSM
13
+ from mathtext_fastapi.math_subtraction_fsm import MathSubtractionFSM
14
  from supabase import create_client
15
  from transitions import Machine
16
 
17
+ from scripts.quiz.generators import start_interactive_math
18
+ from scripts.quiz.hints import generate_hint
19
+
20
  load_dotenv()
21
 
22
  SUPA = create_client(
 
110
  return dump_encoded
111
 
112
 
113
+ def manage_math_quiz_fsm(user_message, contact_uuid, type):
114
  fsm_check = SUPA.table('state_machines').select("*").eq(
115
  "contact_uuid",
116
  contact_uuid
117
  ).execute()
118
 
119
  if fsm_check.data == []:
120
+ if type == 'addition':
121
+ math_quiz_state_machine = MathQuizFSM()
122
+ else:
123
+ math_quiz_state_machine = MathSubtractionFSM()
124
  messages = [math_quiz_state_machine.response_text]
125
  dump_encoded = pickle_and_encode_state_machine(math_quiz_state_machine)
126
 
127
  SUPA.table('state_machines').insert({
128
  'contact_uuid': contact_uuid,
129
+ f'{type}': dump_encoded
130
  }).execute()
131
  else:
132
  undump_encoded = base64.b64decode(
133
+ fsm_check.data[0][type].encode('utf-8')
134
  )
135
  math_quiz_state_machine = pickle.loads(undump_encoded)
136
 
 
139
  messages = math_quiz_state_machine.validate_answer()
140
  dump_encoded = pickle_and_encode_state_machine(math_quiz_state_machine)
141
  SUPA.table('state_machines').update({
142
+ f'{type}': dump_encoded
143
  }).eq(
144
  "contact_uuid", contact_uuid
145
  ).execute()
146
  return messages
147
 
148
 
149
+ def use_quiz_module_approach(user_message, context_data):
150
+ print("USER MESSAGE")
151
+ print(user_message)
152
+ print("=======================")
153
+ if user_message == 'add':
154
+ context_result = start_interactive_math()
155
+ message_package = {
156
+ 'messages': [
157
+ "Great, let's do some addition",
158
+ "First, we'll start with single digits.",
159
+ "Type your response as a number. For example, for '1 + 1', you'd write 2."
160
+ ],
161
+ 'input_prompt': context_result['text'],
162
+ 'state': "addition-question-sequence"
163
+ }
164
+
165
+ elif user_message == context_data.get('right_answer'):
166
+ context_result = start_interactive_math(
167
+ context_data['number_correct'],
168
+ context_data['number_incorrect'],
169
+ context_data['level']
170
+ )
171
+ message_package = {
172
+ 'messages': [
173
+ "That's right, great!",
174
+ ],
175
+ 'input_prompt': context_result['text'],
176
+ 'state': "addition-question-sequence"
177
+ }
178
+ else:
179
+ context_result = generate_hint(
180
+ context_data['question_numbers'],
181
+ context_data['right_answer'],
182
+ context_data['number_correct'],
183
+ context_data['number_incorrect'],
184
+ context_data['level'],
185
+ context_data['hints_used']
186
+ )
187
+ message_package = {
188
+ 'messages': [
189
+ context_result['text'],
190
+ ],
191
+ 'input_prompt': context_data['text'],
192
+ 'state': "addition-question-sequence"
193
+ }
194
+ return message_package, context_result
195
+
196
+
197
  def return_next_conversational_state(context_data, user_message, contact_uuid):
198
  """ Evaluates the conversation's current state to determine the next state
199
 
 
214
  elif context_data['state'] == 'addition-question-sequence' or \
215
  user_message == 'add':
216
 
217
+ # Used in FSM
218
+ # messages = manage_math_quiz_fsm(user_message, contact_uuid)
219
+
220
+ message_package, context_result = use_quiz_module_approach(user_message, context_data)
221
 
222
  if user_message == 'exit':
223
  state_label = 'exit'
224
  else:
225
  state_label = 'addition-question-sequence'
226
+ # Used in FSM
227
+ # input_prompt = messages.pop()
228
+ # message_package = {
229
+ # 'messages': messages,
230
+ # 'input_prompt': input_prompt,
231
+ # 'state': state_label
232
+ # }
233
+
234
+ print("MESSAGE PACKAGE")
235
+ print(message_package)
236
+ context_data = context_result
237
+ message_package['state'] = state_label
238
+
239
+ elif context_data['state'] == 'subtraction-question-sequence' or \
240
+ user_message == 'subtract':
241
+ messages = manage_math_quiz_fsm(user_message, contact_uuid, 'subtraction')
242
+
243
+ if user_message == 'exit':
244
+ state_label = 'exit'
245
+ else:
246
+ state_label = 'subtraction-question-sequence'
247
 
248
  input_prompt = messages.pop()
249
 
 
252
  'input_prompt': input_prompt,
253
  'state': state_label
254
  }
255
+
256
+ # message_package = {
257
+ # 'messages': [
258
+ # "Time for some subtraction!",
259
+ # "Type your response as a number. For example, for '1 - 1', you'd write 0."
260
+ # ],
261
+ # 'input_prompt': "Here's the first one... What's 3-1?",
262
+ # 'state': "subtract-question-sequence"
263
+ # }
264
  elif context_data['state'] == 'exit' or user_message == 'exit':
265
  message_package = {
266
  'messages': [
 
277
  'input_prompt': "Please type add or subtract to start a math activity.",
278
  'state': "reprompt-menu-options"
279
  }
280
+ # Used in FSM
281
  return message_package
282
 
283
+ # Used in quiz folder approach
284
+ # return context_result, message_package
285
+
286
 
287
  def manage_conversation_response(data_json):
288
  """ Calls functions necessary to determine message and context data to send
 
310
  # TODO: Need to incorporate nlu_response into wormhole by checking answers against database (spreadsheet?)
311
  nlu_response = evaluate_message_with_nlu(message_data)
312
 
313
+ if context_data['state'] == 'addition':
314
+ context_result, message_package = return_next_conversational_state(
315
+ context_data,
316
+ user_message,
317
+ contact_uuid
318
+ )
319
+ else:
320
+ message_package = return_next_conversational_state(
321
+ context_data,
322
+ user_message,
323
+ contact_uuid
324
+ )
325
 
326
  print("MESSAGE PACKAGE")
327
  print(message_package)
 
345
  )
346
 
347
  # Update the context object with the new state of the conversation
348
+ if context_data['state'] == 'addition':
349
+ context = {
350
+ "context": {
351
+ "user": whatsapp_id,
352
+ "state": message_package['state'],
353
+ "bot_message": message_package['input_prompt'],
354
+ "user_message": user_message,
355
+ "type": 'ask',
356
+ # Necessary for quiz folder approach
357
+ "text": context_result.get('text'),
358
+ "question_numbers": context_result.get('question_numbers'),
359
+ "right_answer": context_result.get('right_answer'),
360
+ "number_correct": context_result.get('number_correct'),
361
+ "hints_used": context_result.get('hints_used'),
362
+ }
363
+ }
364
+ else:
365
+ context = {
366
+ "context": {
367
+ "user": whatsapp_id,
368
+ "state": message_package['state'],
369
+ "bot_message": message_package['input_prompt'],
370
+ "user_message": user_message,
371
+ "type": 'ask',
372
+ }
373
  }
 
374
 
375
  return context
376
 
mathtext_fastapi/math_subtraction_fsm.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from transitions import Machine
3
+
4
+
5
+ class MathSubtractionFSM(object):
6
+ states = [
7
+ 'quiz_start',
8
+ 'quiz_question',
9
+ 'quiz_end'
10
+ ]
11
+
12
+ transitions = [
13
+ ['ask_second_question', 'quiz_start', 'quiz_question'],
14
+ ['ask_next_question', 'quiz_question', 'quiz_question'],
15
+ ['exit', 'quiz_start', 'quiz_end'],
16
+ ['exit', 'quiz_question', 'quiz_end'],
17
+ ]
18
+
19
+ def __init__(
20
+ self,
21
+ initial_state='quiz_start',
22
+ question_nums=[4, 3],
23
+ initial_student_answer=0,
24
+ ):
25
+ # Instantiate the FSM
26
+ self.machine = Machine(
27
+ model=self,
28
+ states=MathSubtractionFSM.states,
29
+ transitions=MathSubtractionFSM.transitions,
30
+ initial=initial_state
31
+ )
32
+
33
+ # Instantiate variables necessary for tracking activity
34
+ self.question_nums = question_nums
35
+ self.correct_answer = self.question_nums[0] - self.question_nums[1]
36
+ self.student_answer = initial_student_answer
37
+ self.is_correct_answer = False
38
+ self.response_text = f"What is {self.question_nums[0]} - {self.question_nums[1]}?"
39
+
40
+ # Define functions to run on transitions
41
+ self.machine.on_enter_quiz_question('generate_math_problem')
42
+ self.machine.on_exit_quiz_question('validate_answer')
43
+
44
+ def validate_answer(self):
45
+ if self.student_answer == 'exit':
46
+ self.machine.set_state('quiz_end')
47
+ return ["Come back any time!"]
48
+ elif self.correct_answer == self.student_answer:
49
+ self.machine.set_state('quiz_question')
50
+ self.generate_math_problem()
51
+ return ['Great job!', self.response_text]
52
+ else:
53
+ return ["That's not quite right. Try again.", self.response_text]
54
+
55
+ def generate_math_problem(self):
56
+ self.question_nums = random.sample(range(1, 100), 2)
57
+ self.response_text = f"What is {self.question_nums[0]} - {self.question_nums[1]}"
58
+ self.correct_answer = self.question_nums[0] - self.question_nums[1]
scripts/make_request.py CHANGED
@@ -20,13 +20,25 @@ def add_message_text_to_sample_object(message_text):
20
 
21
  """
22
  message_data = '{' + f'"author_id": "+57787919091", "author_type": "OWNER", "contact_uuid": "j43hk26-2hjl-43jk-hnk2-k4ljl46j0ds09", "message_body": "{message_text}", "message_direction": "inbound", "message_id": "4kl209sd0-a7b8-2hj3-8563-3hu4a89b32", "message_inserted_at": "2023-01-10T02:37:28.477940Z", "message_updated_at": "2023-01-10T02:37:28.487319Z"' + '}'
 
 
23
  context_data = '{' + '"user":"", "state":"start-conversation", "bot_message":"", "user_message":"{message_text}"' + '}'
24
 
 
 
25
  json_string = '{' + f'"context_data": {context_data}, "message_data": {message_data}' + '}'
26
  b_string = json_string.encode("utf-8")
27
 
28
  return b_string
29
 
 
 
 
 
 
 
 
 
30
 
31
  def run_simulated_request(endpoint, sample_answer, context=None):
32
  print(f"Case: {sample_answer}")
@@ -58,7 +70,35 @@ def run_simulated_request(endpoint, sample_answer, context=None):
58
  # run_simulated_request('nlu', 'IDK 5?')
59
  # run_simulated_request('manager', '')
60
  run_simulated_request('manager', 'add')
61
- # run_simulated_request('manager', 'subtract')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  # run_simulated_request('manager', 'exit')
63
 
64
 
 
20
 
21
  """
22
  message_data = '{' + f'"author_id": "+57787919091", "author_type": "OWNER", "contact_uuid": "j43hk26-2hjl-43jk-hnk2-k4ljl46j0ds09", "message_body": "{message_text}", "message_direction": "inbound", "message_id": "4kl209sd0-a7b8-2hj3-8563-3hu4a89b32", "message_inserted_at": "2023-01-10T02:37:28.477940Z", "message_updated_at": "2023-01-10T02:37:28.487319Z"' + '}'
23
+ # context_data = '{' + '"user":"", "state":"addition-question-sequence", "bot_message":"", "user_message":"{message_text}"' + '}'
24
+
25
  context_data = '{' + '"user":"", "state":"start-conversation", "bot_message":"", "user_message":"{message_text}"' + '}'
26
 
27
+ # context_data = '{' + '"user":"", "state":"addition-question-sequence", "bot_message":"", "user_message":"{message_text}","text": "What is 2+3?","question_numbers": [4,3],"right_answer": 7,"number_correct": 2, "number_incorrect": 0, "hints_used": 0, "level": "easy"' + '}'
28
+
29
  json_string = '{' + f'"context_data": {context_data}, "message_data": {message_data}' + '}'
30
  b_string = json_string.encode("utf-8")
31
 
32
  return b_string
33
 
34
+ # """
35
+ # "text": "What is 2+3?",
36
+ # "question_numbers": [2,3],
37
+ # "right_answer": 5,
38
+ # "number_correct": 2,
39
+ # "hints_used": 0,
40
+ # """
41
+
42
 
43
  def run_simulated_request(endpoint, sample_answer, context=None):
44
  print(f"Case: {sample_answer}")
 
70
  # run_simulated_request('nlu', 'IDK 5?')
71
  # run_simulated_request('manager', '')
72
  run_simulated_request('manager', 'add')
73
+ run_simulated_request('manager', 'subtract')
74
+ run_simulated_request("question", {
75
+ 'number_correct': 0,
76
+ 'number_incorrect': 0,
77
+ 'level': 'easy'
78
+ })
79
+ run_simulated_request("hint", {
80
+ 'question_numbers': [1, 2, 3],
81
+ 'right_answer': 3,
82
+ 'number_correct': 0,
83
+ 'number_incorrect': 0,
84
+ 'level': 'easy',
85
+ 'hints_used': 0
86
+ })
87
+ run_simulated_request("generate_question", {
88
+ 'level': 'medium'
89
+ })
90
+ run_simulated_request("numbers_by_level", {
91
+ 'level': 'medium'
92
+ })
93
+ run_simulated_request("number_sequence", {
94
+ "current_number": 10,
95
+ "ordinal_number": 2,
96
+ "times": 1
97
+ })
98
+ run_simulated_request("level", {
99
+ "current_level": "hard",
100
+ "level_up": False
101
+ })
102
  # run_simulated_request('manager', 'exit')
103
 
104
 
scripts/quiz/generators.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .questions import generate_question_data
2
+ from .utils import get_next_level
3
+
4
+
5
+ def start_interactive_math(right_answers=0, wrong_answers=0, level="easy"):
6
+ if wrong_answers > 2:
7
+ wrong_answers = 0
8
+ right_answers = 0
9
+ level = get_next_level(level, False)
10
+ elif right_answers > 2:
11
+ right_answers = 0
12
+ wrong_answers = 0
13
+ level = get_next_level(level)
14
+
15
+ question_data = generate_question_data(level)
16
+ question = question_data['question']
17
+ right_answer = question_data['answer']
18
+ cur_num = question_data['current_number']
19
+ ord_num = question_data['ordinal_number']
20
+ times = question_data['times']
21
+
22
+ numbers_group = [cur_num, ord_num, times]
23
+ output = {
24
+ "text": question,
25
+ "question_numbers": numbers_group,
26
+ "right_answer": right_answer,
27
+ 'number_correct': right_answers,
28
+ 'number_incorrect': wrong_answers,
29
+ 'level': level,
30
+ "hints_used": 0
31
+ }
32
+ return output
33
+
scripts/quiz/hints.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+
3
+
4
+ def generate_hint(question_nums, right_answer, right_answers, wrong_answers, level, hints_used):
5
+ ord_num = question_nums[1] # ordinal number
6
+ equation = right_answer - 2 * ord_num - 1
7
+ min_num = equation if equation > 0 else 0
8
+ seq_before = " ".join(
9
+ [str(num) for num in range(right_answer - ord_num, min_num, -ord_num)][::-1]
10
+ ) # sequence before right answer
11
+ seq_after = " ".join(
12
+ [str(num) for num in range(right_answer + ord_num, right_answer + 2 * ord_num + 1, ord_num)]
13
+ ) # sequence after right answer
14
+ hints = [
15
+ f"What number will fill the gap in a sequence {seq_before} ... {seq_after}?",
16
+ f"What number is {ord_num} in the account after {right_answer - ord_num}?",
17
+ f"What number is {ord_num} in the account before {right_answer + ord_num}?",
18
+ f"What number is greater than {right_answer - 1} and less than {right_answer + 1}?"
19
+ ]
20
+ rand_hint = random.choice(hints)
21
+ hints_used += 1
22
+
23
+ output = {
24
+ "text": rand_hint,
25
+ "question_numbers": question_nums,
26
+ "right_answer": right_answer,
27
+ 'number_correct': right_answers,
28
+ 'number_incorrect': wrong_answers,
29
+ 'level': level,
30
+ "hints_used": hints_used
31
+ }
32
+ return output
scripts/quiz/questions.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from typing import Literal
3
+
4
+
5
+ def generate_question_data(level: Literal["easy", "medium", "hard"] = "easy"):
6
+ """generate question, its numbers and proper answer"""
7
+
8
+ nums = generate_numbers_by_level(level)
9
+ cur_num = nums['current_number'] # current number
10
+ ord_num = nums['ordinal_number'] # ordinal number
11
+ seq_up_by_one = generate_number_sequence(cur_num, ord_num=1, times=1) # sequence with ord_num = 1, times = 1
12
+
13
+ count_up_by_one_questions = [
14
+ {
15
+ "question": f"Let's practice counting. After {cur_num}, what number is next?\n{seq_up_by_one}",
16
+ "current_number": cur_num,
17
+ "ordinal_number": 1,
18
+ "times": 1,
19
+ "answer": cur_num + 1
20
+ }
21
+ ]
22
+ seq_up_by_ord = generate_number_sequence(cur_num, ord_num, times=1) # sequence with times = 1
23
+ count_up_by_ord_questions = [
24
+ {
25
+ "question": f"What number comes {ord_num} number after {cur_num}?\n{seq_up_by_ord}",
26
+ "current_number": cur_num,
27
+ "ordinal_number": ord_num,
28
+ "times": 1,
29
+ "answer": cur_num + ord_num
30
+ },
31
+ {
32
+ "question": f"If we count up {ord_num} from {cur_num}, what number is next?\n{seq_up_by_ord}",
33
+ "current_number": cur_num,
34
+ "ordinal_number": ord_num,
35
+ "times": 1,
36
+ "answer": cur_num + ord_num
37
+ }
38
+ ]
39
+ times = 1 if level == "easy" else nums['times']
40
+ times_ord_seq = generate_number_sequence(cur_num, ord_num, times)
41
+ times_ord_questions = [
42
+ {
43
+ "question": f"We're counting up by {times}s. What number is {ord_num} after {cur_num}?\n{times_ord_seq}",
44
+ "current_number": cur_num,
45
+ "ordinal_number": ord_num,
46
+ "times": times,
47
+ "answer": cur_num + ord_num * times
48
+ }
49
+ ]
50
+ times_only_seq = generate_number_sequence(cur_num, 1, times) # sequence with ordinal number = 1
51
+ times_only_questions = [
52
+ {
53
+ "question": f"Let's count up by {times}s. What number is next if we start from {cur_num}?\n{times_only_seq}",
54
+ "current_number": cur_num,
55
+ "ordinal_number": 1,
56
+ "times": times,
57
+ "answer": cur_num + times
58
+ }
59
+ ]
60
+ questions = [*count_up_by_one_questions, *count_up_by_ord_questions, *times_only_questions, *times_ord_questions]
61
+ random_choice = random.choice(questions)
62
+ return random_choice
63
+
64
+
65
+ def generate_numbers_by_level(level: Literal["easy", "medium", "hard"] = "easy"):
66
+ """generate current number, ordinal number and times parameter
67
+
68
+ returns
69
+ dict with params:
70
+ :param current_number: current number
71
+ :param ordinal numebr: the number we count up by
72
+ :param times: the number of times we count up by ordinal number"""
73
+
74
+ if level == "easy":
75
+ cur_num = random.randint(1, 8)
76
+ ord_num = random.randint(1, 2)
77
+ times = 1
78
+ elif level == "medium":
79
+ cur_num = random.randint(1, 94)
80
+ ord_num = random.randint(1, 3)
81
+ times = random.randint(1, 2)
82
+ elif level == "hard":
83
+ cur_num = random.randint(1, 488)
84
+ ord_num = random.randint(1, 4)
85
+ times = random.randint(1, 2)
86
+
87
+ return {
88
+ "current_number": cur_num,
89
+ "ordinal_number": ord_num,
90
+ "times": times
91
+ }
92
+
93
+
94
+ def generate_number_sequence(cur_num, ord_num, times=1):
95
+ """generate one of 2 sequences. For example we want 55 to be a right answer, then sequences can be:
96
+ 52 53 54 ...
97
+ ... 56 57 58
98
+
99
+ parameters
100
+ :cur_num: current number
101
+ :ord_num: ordinal number
102
+ :times: times"""
103
+ max_num = cur_num + times * ord_num
104
+
105
+ seq_before = [str(num) for num in range(max_num - times, 0, -times)][:3][::-1]
106
+ seq_after = [str(num) for num in range(max_num + times, max_num + 4 * times, times)]
107
+ seq_before.append("...")
108
+ seq_after.insert(0, "...")
109
+
110
+ seqs = []
111
+ if len(seq_before) == 4:
112
+ seqs.append(seq_before)
113
+ if len(seq_after) == 4:
114
+ seqs.append(seq_after)
115
+ rand_seq = " ".join(random.choice(seqs))
116
+ return rand_seq
scripts/quiz/utils.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Literal
2
+
3
+ def get_next_level(cur_level, levep_up: Literal[True, False] = True):
4
+ if levep_up:
5
+ if cur_level == "easy":
6
+ return "medium"
7
+ else:
8
+ return "hard"
9
+ else:
10
+ if cur_level == "medium":
11
+ return "easy"
12
+ else:
13
+ return "medium"