diff --git a/backend/create_game.py b/backend/create_game.py new file mode 100644 index 0000000..dce7f3b --- /dev/null +++ b/backend/create_game.py @@ -0,0 +1,39 @@ +import boto3 +import json + +# assign DynamoDB table to modify +table_name = "multiplayer-games" +dynamo = boto3.resource('dynamodb') +game_table = dynamo.Table(table_name) + +def lambda_handler(event, context): + ''' event should have the following fields: + - game_id: int, + - topic: string, + - questions: list of dicts + Adds the game to the game table. Returns an empty dictionary if success. + ''' + try: + data = json.loads(event["body"]) + game_id = data["game_id"] + topic = data["topic"] + questions = json.dumps(data["questions"]) + + if game_id == 0: + return { + 'statusCode': 400, + 'body': json.dumps({'error': 'Game ID cannot be 0'}) + } + + item = {"game_id": game_id, "topic": topic, "questions": questions, "players": "{}"} + game_table.put_item(Item=item) + + return { + 'statusCode': 200, + 'body': json.dumps({}) + } + except Exception as e: + return { + 'statusCode': 500, + 'body': json.dumps({'message': str(e)}) + } \ No newline at end of file diff --git a/backend/get_game.py b/backend/get_game.py new file mode 100644 index 0000000..bffd0b8 --- /dev/null +++ b/backend/get_game.py @@ -0,0 +1,48 @@ +import boto3 +import json + +# assign DynamoDB table to modify +table_name = "multiplayer-games" +dynamo = boto3.resource('dynamodb') +game_table = dynamo.Table(table_name) + +def lambda_handler(event, context): + ''' event should have the following fields: + - game_id: int + Returns (topic, questions, num_players). If the game is not found, return ('', [], -1). + + ''' + try: + data = json.loads(event["body"]) + game_id = data["game_id"] + + if game_id == 0: + return { + 'statusCode': 400, + 'body': json.dumps({'error': 'Game ID cannot be 0'}) + } + + response = game_table.get_item(Key={"game_id": game_id}) + if 'Item' not in response: + return { + 'statusCode': 200, + 'body': json.dumps({ + 'topic': '', + 'questions': [], + 'num_players': -1 + }) + } + else: + return { + 'statusCode': 200, + 'body': json.dumps({ + 'topic': response['Item']['topic'], + 'questions': json.loads(response['Item']['questions']), + 'num_players': len(json.loads(response['Item']['players'])) + }) + } + except Exception as e: + return { + 'statusCode': 500, + 'body': json.dumps({'message': str(e)}) + } \ No newline at end of file diff --git a/backend/lambda_functions/add_player.py b/backend/lambda_functions/add_player.py new file mode 100644 index 0000000..c2efd2a --- /dev/null +++ b/backend/lambda_functions/add_player.py @@ -0,0 +1,56 @@ +import boto3 +import json + + +table_name = "multiplayer-games" +dynamo = boto3.resource('dynamodb') +questions_table = dynamo.Table(table_name) + + +def lambda_handler(event, context): + ''' [event] contains the following keys: + - game_id: id of game to add player results to + - score: score of player + - name: player name to display + Adds player and score to game players + ''' + + try: + data = json.loads(event["body"]) + id = data["game_id"] + score = data["score"] + name = data["name"] + + key = {'game_id': id} + response = questions_table.get_item(Key=key) + if 'Item' in response: + players = json.loads(response['Item']['players']) + + # add to players + if name in players: + players[name] = max(players[name], score) + else: + players[name] = score + + + # update table + response = questions_table.update_item( + Key=key, + UpdateExpression="set players=:q", + ExpressionAttributeValues={ + ':q': json.dumps(players)}, + ReturnValues="UPDATED_NEW") + return { + 'statusCode': 200, + 'body': json.dumps({}) + } + else: + return { + 'statusCode': 404, + 'body': json.dumps({'error': "couldn't load players"}) + } + except Exception as e: + return { + 'statusCode': 500, + 'body': json.dumps({'message': str(e)}) + } \ No newline at end of file diff --git a/backend/lambda_functions/get_mcq_passage.py b/backend/lambda_functions/get_mcq_passage.py new file mode 100644 index 0000000..957c19b --- /dev/null +++ b/backend/lambda_functions/get_mcq_passage.py @@ -0,0 +1,81 @@ +import boto3 +import json +import os +import openai + + +# assign DynamoDB table to modify +table_name = "trivai-questions" +dynamo = boto3.resource('dynamodb') +questions_table = dynamo.Table(table_name) + +# constants +OPENAI_API_KEY = os.environ["OPENAI_KEY"] +MAX_QUESTIONS = 20 + + +# entry point for new events, parses event +def lambda_handler(event, context): + ''' [event] contains the following keys: + - passage: passage to generate questions for + - num_questions: number of questions to generate + ''' + + try: + data = json.loads(event["body"]) + passage = data["passage"] + num_questions = data["num_questions"] + + if num_questions > MAX_QUESTIONS: # FIXME: cap instead of error? + return { + 'statusCode': 400, + 'body': json.dumps({"error": "Cannot generate more than %d questions at a time." % MAX_QUESTIONS}) + } + + status, res = mcq_passage(passage, num_questions) + if status == 200: + return { + 'statusCode': 200, + 'body': res + } + except Exception as e: + return { + 'statusCode': 400, + 'body': json.dumps({'error': e}) + } + + +def mcq_passage(passage, num_questions): + mcq_passage_prompt = """ + Create %d questions about the following passage in exactly the following json format, which is surrounded in brackets. Make sure each question has exactly 4 answer choices. Do not output anything other than the questions in the specified format: + [ + { + "question": question, + "options": [option 0, option 1, option 2, option 3], + "answer_id": answer_id + }, + { + "question": question, + "options": [option 0, option 1, option 2, option 3], + "answer_id": answer_id + }, ... + ] + Passage: %s + """ % (num_questions, passage) + return get_chatgpt(mcq_passage_prompt) + + +# get ChatGPT response to given prompt +def get_chatgpt(prompt): # (int, str) + completion = openai.ChatCompletion.create( + api_key=OPENAI_API_KEY, + model="gpt-3.5-turbo", + messages=[{"role": "user", + "content": prompt}] + ) + content = completion["choices"][0]["message"]["content"] # str + + try: + return 200, content + except: + return 400, json.dumps({"error": "OpenAI did not return a json string"}) diff --git a/backend/lambda_functions/get_mcq_topic.py b/backend/lambda_functions/get_mcq_topic.py new file mode 100644 index 0000000..1f21598 --- /dev/null +++ b/backend/lambda_functions/get_mcq_topic.py @@ -0,0 +1,113 @@ +import boto3 +import json +import os +import openai +import random + + +# assign DynamoDB table to modify +table_name = "trivai-questions" +dynamo = boto3.resource('dynamodb') +questions_table = dynamo.Table(table_name) + +# constants +OPENAI_API_KEY = os.environ["OPENAI_KEY"] +MAX_QUESTIONS = 20 + + +# entry point for new events, parses event +def lambda_handler(event, context): + ''' [event] contains the following keys: + - topic: subject to generate questions for + - num_questions: number of questions to generate + ''' + + try: + data = json.loads(event["body"]) + topic = data["topic"] + num_questions = data["num_questions"] + + if num_questions > MAX_QUESTIONS: # FIXME: cap instead of error? + return { + 'statusCode': 400, + 'body': json.dumps({"error": "Cannot generate more than %d questions at a time." % MAX_QUESTIONS}) + } + + res = "" + key = {'topic_id': topic} + response = questions_table.get_item(Key=key) + if 'Item' in response: + # list of dictionaries + questions = json.loads(response['Item']['questions']) + if len(questions) < num_questions: + # Generate more questions and put the updated list of questions into the database + status, more_questions = mcq_topic( + topic, num_questions - len(questions)) + questions = questions + \ + json.loads(more_questions) # list of dicts + response = questions_table.update_item( + Key=key, + UpdateExpression="set questions=:q", + ExpressionAttributeValues={ + ':q': json.dumps(questions)}, + ReturnValues="UPDATED_NEW") + else: + # Randomly choose num_questions questions to return + questions = random.sample(questions, num_questions) + # return questions + return { + 'statusCode': 200, + 'body': json.dumps(questions) + } + else: + status, res = mcq_topic(topic, num_questions) + # store if not error + if status == 200: + item = {"topic_id": topic, "questions": res} + print(item) + questions_table.put_item(Item=item) + # return json.loads(res) + return { + 'statusCode': 200, + 'body': res + } + except Exception as e: + return { + 'statusCode': 400, + 'body': json.dumps({'error': e}) + } + + +def mcq_topic(topic, num_questions): # (int, str) + mcq_topic_prompt = """ + Create %d questions about %s in exactly the following json format, including the outer brackets: + [ + { + "question": question, + "options": [option 0, option 1, option 2, option 3], + "answer_id": answer_id + }, + { + "question": question, + "options": [option 0, option 1, option 2, option 3], + "answer_id": answer_id + }, ... + ] + """ % (num_questions, topic) + return get_chatgpt(mcq_topic_prompt) + + +# get ChatGPT response to given prompt +def get_chatgpt(prompt): # (int, str) + completion = openai.ChatCompletion.create( + api_key=OPENAI_API_KEY, + model="gpt-3.5-turbo", + messages=[{"role": "user", + "content": prompt}] + ) + content = completion["choices"][0]["message"]["content"] # str + + try: + return 200, content + except: + return 400, json.dumps({"error": "OpenAI did not return a json string"}) diff --git a/backend/lambda_functions/get_results.py b/backend/lambda_functions/get_results.py new file mode 100644 index 0000000..d693190 --- /dev/null +++ b/backend/lambda_functions/get_results.py @@ -0,0 +1,37 @@ +import boto3 +import json + + +table_name = "multiplayer-games" +dynamo = boto3.resource('dynamodb') +questions_table = dynamo.Table(table_name) + + +def lambda_handler(event, context): + ''' [event] contains the following keys: + - game_id: id of game to get results for + Returns all finished players for game game_id + ''' + try: + data = json.loads(event["body"]) + id = data["game_id"] + + key = {'game_id': id} + response = questions_table.get_item(Key=key) + if 'Item' in response: + players = json.loads(response['Item']['players']) + players = json.dumps(players) + return { + 'statusCode': 200, + 'body': players + } + else: + return { + 'statusCode': 404, + 'body': json.dumps({'error': "couldn't load players"}) + } + except Exception as e: + return { + 'statusCode': 500, + 'body': json.dumps({'message': str(e)}) + } \ No newline at end of file diff --git a/backend/lambda_functions/openai-layer.zip b/backend/lambda_functions/openai-layer.zip new file mode 100644 index 0000000..78f3ba8 Binary files /dev/null and b/backend/lambda_functions/openai-layer.zip differ