diff --git a/flashcards/apps/cards/__init.py__ b/flashcards/apps/cards/__init.py__ new file mode 100644 index 0000000..e69de29 diff --git a/flashcards/apps/cards/cardgen.py b/flashcards/apps/cards/cardgen.py new file mode 100644 index 0000000..07cec05 --- /dev/null +++ b/flashcards/apps/cards/cardgen.py @@ -0,0 +1,67 @@ +import openai +from django.conf import settings +from edx_rest_api_client import client as rest_client + +from flashcards.apps.cards.prompt import anki_prompt +from flashcards.settings.private import OPENAI_API_KEY # pylint: disable=import-error,no-name-in-module + +openai.api_key = OPENAI_API_KEY + +# provision a user in devstack provision-ida-user.sh flashcards flashcards 3000 + + +def get_client(oauth_base_url=settings.LMS_ROOT_URL): + """ + Returns an authenticated edX REST API client. + """ + client = rest_client.OAuthAPIClient( + oauth_base_url, + settings.BACKEND_SERVICE_EDX_OAUTH2_KEY, + settings.BACKEND_SERVICE_EDX_OAUTH2_SECRET, + ) + client._ensure_authentication() # pylint: disable=protected-access + if not client.auth.token: # pragma: no cover + raise Exception('No Auth Token') # pylint: disable=broad-exception-raised + return client + + +def block_content_for_cards(course_id, block_id, source='lms'): + block_id = block_id.replace(":", "$:") + lms_url = f'{settings.LMS_ROOT_URL}/courses/{course_id}/xblock/aside-usage-v2:{block_id}::extractor_aside/handler/extract_handler' # noqa + # currently sourcing via the CMS doesn't work by auth and if auth is disabled walking + # the handler in the CMS context poisons the cache and obliterates those blocks in the course + # cms_url = f'{settings.CMS_ROOT_URL}/xblock/aside-usage-v2:{block_id}::extractor_aside/handler/extract_handler' + + handler_url = lms_url + + client = get_client() + response = client.get(handler_url) + + if response.status_code < 200 or response.status_code > 300: + response.raise_for_status() + + content = response.json() + joined_content = '\n\n'.join(content['content']) + return joined_content + + +def cards_from_openai(content): + messages = [ + {"role": "system", "content": anki_prompt}, + {"role": "system", "content": content}, + ] + + result = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=messages, + temperature=1.0, + ) + + maybe_csv = result['choices'][0]['message']['content'] + return maybe_csv + + +def cards_from_block_id(course_id, block_id): + content = block_content_for_cards(course_id, block_id) + cards = cards_from_openai(content) + return cards diff --git a/flashcards/apps/cards/prompt.py b/flashcards/apps/cards/prompt.py new file mode 100644 index 0000000..8e27d49 --- /dev/null +++ b/flashcards/apps/cards/prompt.py @@ -0,0 +1,26 @@ +# The prompt to ask ChatGPT to make anki cards. +anki_prompt = """ + +I want you to act as a professional Anki card creator, able to create Anki cards from the text I provide. + +Regarding the formulation of the card content, you stick to two principles: + +First, minimum information principle: The material you learn must be formulated in as simple way as it is only possible. Simplicity does not have to imply losing information and skipping the difficult part. +Second, optimize wording: The wording of your items must be optimized to make sure that in minimum time the right bulb in your brain lights up. This will reduce error rates, increase specificity, reduce response time, and help your concentration. + +Please output these cards you create in a .csv format, with the questions and answers separated by commas. + +The following is a model card-create template for you to study. + +Text: The characteristics of the Dead Sea: Salt lake located on the border between Israel and Jordan. Its shoreline is the lowest point on the Earth's surface, averaging 396 m below sea level. It is 74 km long. It is seven times as salty (30% by volume) as the ocean. Its density keeps swimmers afloat. Only simple organisms can live in its saline waters + +Where is the Dead Sea located?,On the border between Israel and Jordan +What is the lowest point on the Earth's surface?,The Dead Sea shoreline +What is the average level on which the Dead Sea is located?,400 meters (below sea level) +How long is the Dead Sea?,70 km +How much saltier is the Dead Sea as compared with the oceans?,7 times +What is the volume content of salt in the Dead Sea?,30% +Why can the Dead Sea keep swimmers afloat?,Due to high salt content +Why is the Dead Sea called Dead?,Because only simple organisms can live in it +Why only simple organisms can live in the Dead Sea?,Because of high salt content +""" # noqa diff --git a/flashcards/settings/base.py b/flashcards/settings/base.py index cd4bddb..2452018 100644 --- a/flashcards/settings/base.py +++ b/flashcards/settings/base.py @@ -244,3 +244,6 @@ def root(*path_fragments): # OpenAI API key, to be specified in the private settings file OPENAI_API_KEY = '' + +CMS_ROOT_URL = None +LMS_ROOT_URL = None diff --git a/flashcards/settings/devstack.py b/flashcards/settings/devstack.py index d055862..3ba9b62 100644 --- a/flashcards/settings/devstack.py +++ b/flashcards/settings/devstack.py @@ -58,3 +58,8 @@ 'SECRET_KEY': 'lms-secret', }], }) + +CMS_ROOT_URL = 'http://edx.devstack.cms:18010' +LMS_ROOT_URL = 'http://edx.devstack.lms:18000' +BACKEND_SERVICE_EDX_OAUTH2_KEY = 'flashcards-backend-service-key' +BACKEND_SERVICE_EDX_OAUTH2_SECRET = 'flashcards-backend-service-secret' diff --git a/flashcards/settings/local.py b/flashcards/settings/local.py index 34d7aeb..fb1aac9 100644 --- a/flashcards/settings/local.py +++ b/flashcards/settings/local.py @@ -70,3 +70,8 @@ # Lastly, see if the developer has any local overrides. if os.path.isfile(join(dirname(abspath(__file__)), 'private.py')): from .private import * # pylint: disable=import-error,wildcard-import + +CMS_ROOT_URL = 'http://localhost:18010' +LMS_ROOT_URL = 'http://localhost:18000' +BACKEND_SERVICE_EDX_OAUTH2_KEY = 'flashcards-backend-service-key' +BACKEND_SERVICE_EDX_OAUTH2_SECRET = 'flashcards-backend-service-secret'