diff --git a/.github/workflows/upgrade-python-requirements.yml b/.github/workflows/upgrade-python-requirements.yml deleted file mode 100644 index d2aef30..0000000 --- a/.github/workflows/upgrade-python-requirements.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Upgrade Python Requirements - -on: - schedule: - - cron: "0 0 * * 1" - workflow_dispatch: - inputs: - branch: - description: Target branch against which to create requirements PR - required: true - default: master - -jobs: - call-upgrade-python-requirements-workflow: - uses: openedx/.github/.github/workflows/upgrade-python-requirements.yml@master - # Do not run on forks - if: github.repository_owner == 'open-craft' - with: - branch: ${{ github.event.inputs.branch || 'master' }} - # optional parameters below; fill in if you'd like github or email notifications - # user_reviewers: "" - # team_reviewers: "" - # email_address: "" - # send_success_notification: false - secrets: - requirements_bot_github_token: ${{ secrets.REQUIREMENTS_BOT_GITHUB_TOKEN }} - requirements_bot_github_email: ${{ secrets.REQUIREMENTS_BOT_GITHUB_EMAIL }} - edx_smtp_username: ${{ secrets.EDX_SMTP_USERNAME }} - edx_smtp_password: ${{ secrets.EDX_SMTP_PASSWORD }} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c992e2f --- /dev/null +++ b/Makefile @@ -0,0 +1,98 @@ +.PHONY: clean help compile_translations dummy_translations extract_translations detect_changed_source_translations \ + build_dummy_translations validate_translations check_translations_up_to_date \ + requirements selfcheck test test.python test.unit test.quality upgrade + +.DEFAULT_GOAL := help + +WORKING_DIR := ai_eval +JS_TARGET := $(WORKING_DIR)/public/js/translations +EXTRACT_DIR := $(WORKING_DIR)/conf/locale/en/LC_MESSAGES +EXTRACTED_DJANGO_PARTIAL := $(EXTRACT_DIR)/django-partial.po +EXTRACTED_DJANGOJS_PARTIAL := $(EXTRACT_DIR)/djangojs-partial.po +EXTRACTED_DJANGO := $(EXTRACT_DIR)/django.po + +help: ## display this help message + @echo "Please use \`make ' where is one of" + @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' + +clean: ## remove generated byte code, coverage reports, and build artifacts + find . -name '__pycache__' -exec rm -rf {} + + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + rm -fr build/ + rm -fr dist/ + rm -fr *.egg-info + +## Localization targets + +extract_translations: ## extract strings to be translated, outputting .po files + cd $(WORKING_DIR) && i18n_tool extract + mv $(EXTRACTED_DJANGO_PARTIAL) $(EXTRACTED_DJANGO) + # Safely concatenate djangojs if it exists. The file will exist in this repo, but we're trying to follow a pattern + # between all repositories that use i18n_tool + if test -f $(EXTRACTED_DJANGOJS_PARTIAL); then \ + msgcat $(EXTRACTED_DJANGO) $(EXTRACTED_DJANGOJS_PARTIAL) -o $(EXTRACTED_DJANGO) && \ + rm $(EXTRACTED_DJANGOJS_PARTIAL); \ + fi + sed -i'' -e 's/nplurals=INTEGER/nplurals=2/' $(EXTRACTED_DJANGO) + sed -i'' -e 's/plural=EXPRESSION/plural=\(n != 1\)/' $(EXTRACTED_DJANGO) + +compile_translations: ## compile translation files, outputting .mo files for each supported language + cd $(WORKING_DIR) && i18n_tool generate -v + python manage.py compilejsi18n --namespace DragAndDropI18N --output $(JS_TARGET) + +detect_changed_source_translations: + cd $(WORKING_DIR) && i18n_tool changed + +dummy_translations: ## generate dummy translation (.po) files + cd $(WORKING_DIR) && i18n_tool dummy + +build_dummy_translations: dummy_translations compile_translations ## generate and compile dummy translation files + +validate_translations: build_dummy_translations detect_changed_source_translations ## validate translations + +check_translations_up_to_date: extract_translations compile_translations dummy_translations detect_changed_source_translations ## extract, compile, and check if translation files are up-to-date + +piptools: ## install pinned version of pip-compile and pip-sync + pip install -r requirements/pip.txt + pip install -r requirements/pip-tools.txt + +requirements: piptools ## install test requirements locally + pip-sync requirements/ci.txt + +requirements_python: piptools ## install all requirements locally + pip-sync requirements/dev.txt requirements/private.* + +test.quality: selfcheck ## run quality checkers on the codebase + tox -e quality + +test.python: ## run python unit tests in the local virtualenv + pytest --cov ai_eval $(TEST) + +test.unit: ## run all unit tests + tox $(TEST) + +test: test.unit test.quality ## Run all tests + tox -e translations + +# Define PIP_COMPILE_OPTS=-v to get more information during make upgrade. +PIP_COMPILE = pip-compile --upgrade $(PIP_COMPILE_OPTS) + +upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade +upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in + pip install -qr requirements/pip-tools.txt + # Make sure to compile files after any other files they include! + $(PIP_COMPILE) --allow-unsafe -o requirements/pip.txt requirements/pip.in + $(PIP_COMPILE) -o requirements/pip-tools.txt requirements/pip-tools.in + pip install -qr requirements/pip.txt + pip install -qr requirements/pip-tools.txt + $(PIP_COMPILE) -o requirements/base.txt requirements/base.in + $(PIP_COMPILE) -o requirements/test.txt requirements/test.in + $(PIP_COMPILE) -o requirements/quality.txt requirements/quality.in + $(PIP_COMPILE) -o requirements/ci.txt requirements/ci.in + $(PIP_COMPILE) -o requirements/dev.txt requirements/dev.in + sed -i '/^[dD]jango==/d' requirements/test.txt + +selfcheck: ## check that the Makefile is well-formed + @echo "The Makefile is well-formed." diff --git a/README.md b/README.md new file mode 100644 index 0000000..3c897f0 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +## Introduction + +This repository hosts two Open edX XBlocks: + +1. **Short Answer with AI Evaluation**: This XBlock allows students to submit short answers, which are then evaluated with the help of a large language model (LLM). +2. **Coding with AI Evaluation**: This XBlock allows students to submit code in a text editor. The code is executed via a third-party API (currently using [Judge0](https://judge0.com/)), and both the code and its output are sent to an LLM for feedback. + +## Screeshots + +| ![Short Answer with AI evaluation Xblock](docs/shortanswer-xblock.png) | ![Coding with AI evaluation Xblock](docs/coding-xblock.png) | +|-----------------------------------------------------------------------|----------------------------------------------------------------| +| ![Coding with AI evaluation Xblock HTML](docs/coding-xblock-ai-feedback.png) | ![Coding with AI evaluation Xblock AI feedback](docs/coding-xblock-html.png) | + + + +## Setup + +### Using Tutor + +1. Add the following line to the `OPENEDX_EXTRA_PIP_REQUIREMENTS` in your Tutor `config.yml` file: + ```yaml + OPENEDX_EXTRA_PIP_REQUIREMENTS: + - git+https://github.com/open-craft/xblock-ai-evaluation + ``` + You can append `@vX.Y.Z` to the URL to specify your desired version. + +2. Launch Tutor. + +3. In the Open edX platform, navigate to `Settings > Advanced Settings` and add `shortanswer_ai_eval` and `coding_ai_eval` to the `Advanced Module List`. + +4. Add either XBlock using the `Advanced` button in the `Add New Component` section of Studio. + +5. Configure the added Xblock and make sure to add correct API keys. You can format your question and prompts using [Markdown](https://marked.js.org/demo/). + +## Dependencies +- [Judge0 API](https://judge0.com/) +- [Monaco editor](https://github.com/microsoft/monaco-editor) +- [LiteLLM](https://github.com/BerriAI/litellm) \ No newline at end of file diff --git a/ai_eval/__init__.py b/ai_eval/__init__.py new file mode 100644 index 0000000..63412c2 --- /dev/null +++ b/ai_eval/__init__.py @@ -0,0 +1,6 @@ +""" +Xblock to have short text and code entries with AI-driven evaluation. +""" + +from .shortanswer import ShortAnswerAIEvalXBlock +from .coding_ai_eval import CodingAIEvalXBlock diff --git a/ai_eval/base.py b/ai_eval/base.py new file mode 100644 index 0000000..3e67e7f --- /dev/null +++ b/ai_eval/base.py @@ -0,0 +1,146 @@ +"""Base Xblock with AI evaluation.""" + +import pkg_resources + +from django.utils.translation import gettext_noop as _ +from xblock.core import XBlock +from xblock.fields import String, Scope, Dict +from xblock.validation import ValidationMessage + + +from .llm import SupportedModels + +try: + from xblock.utils.studio_editable import StudioEditableXBlockMixin +except ModuleNotFoundError: # For compatibility with Palm and earlier + from xblockutils.studio_editable import StudioEditableXBlockMixin + +try: + from xblock.utils.resources import ResourceLoader +except ( + ModuleNotFoundError +): # For backward compatibility with releases older than Quince. + from xblockutils.resources import ResourceLoader + + +class AIEvalXBlock(StudioEditableXBlockMixin, XBlock): + """ + Base class for Xblocks with AI evaluation + """ + + USER_KEY = "USER" + LLM_KEY = "LLM" + + loader = ResourceLoader(__name__) + + icon_class = "problem" + model_api_key = String( + display_name=_("Chosen model API Key"), + help=_("Enter your the API Key of your chosen model."), + default="", + scope=Scope.settings, + ) + model_api_url = String( + display_name=_("Set your API URL"), + help=_( + "Fill this only for LLama. This required with models that don't have an official provider." + " Example URL: https://model-provider-example/llama3_70b" + ), + default=None, + scope=Scope.settings, + ) + model = String( + display_name=_("AI model"), + help=_("Select the AI language model to use."), + values=[ + {"display_name": model, "value": model} for model in SupportedModels.list() + ], + Scope=Scope.settings, + default=SupportedModels.GPT4O.value, + ) + + evaluation_prompt = String( + display_name=_("Evaluation prompt"), + help=_( + "Enter the evaluation prompt given to the model." + " The question will be inserted right after it." + " The student's answer would then follow the question. Markdown format can be used." + ), + default="You are a teacher. Evaluate the student's answer for the following question:", + multiline_editor=True, + scope=Scope.settings, + ) + question = String( + display_name=_("Question"), + help=_( + "Enter the question you would like the students to answer." + " Markdown format can be used." + ), + default="", + multiline_editor=True, + scope=Scope.settings, + ) + + messages = Dict( + help=_("Dictionary with chat messages"), + scope=Scope.user_state, + default={USER_KEY: [], LLM_KEY: []}, + ) + editable_fields = ( + "display_name", + "evaluation_prompt", + "question", + "model", + "model_api_key", + "model_api_url", + ) + + def resource_string(self, path): + """Handy helper for getting resources from our kit.""" + data = pkg_resources.resource_string(__name__, path) + return data.decode("utf8") + + def validate_field_data(self, validation, data): + """ + Validate fields. + """ + + if not data.model or data.model not in SupportedModels.list(): + validation.add( + ValidationMessage( + ValidationMessage.ERROR, + _( # pylint: disable=translation-of-non-string + f"Model field is mandatory and must be one of {', '.join(SupportedModels.list())}" + ), + ) + ) + + if not data.model_api_key: + validation.add( + ValidationMessage( + ValidationMessage.ERROR, _("Model API key is mandatory") + ) + ) + + if data.model == SupportedModels.LLAMA and not data.model_api_url: + validation.add( + ValidationMessage( + ValidationMessage.ERROR, + _("API URL field is mandatory when using ollama/llama2."), + ) + ) + + if data.model != SupportedModels.LLAMA and data.model_api_url: + validation.add( + ValidationMessage( + ValidationMessage.ERROR, + _("API URL field can be set only when using ollama/llama2."), + ) + ) + + if not data.question: + validation.add( + ValidationMessage( + ValidationMessage.ERROR, _("Question field is mandatory") + ) + ) diff --git a/ai_eval/coding_ai_eval.py b/ai_eval/coding_ai_eval.py new file mode 100644 index 0000000..92f2658 --- /dev/null +++ b/ai_eval/coding_ai_eval.py @@ -0,0 +1,259 @@ +"""Coding Xblock with AI evaluation.""" + +import logging +import traceback +import pkg_resources + + +from django.utils.translation import gettext_noop as _ +from web_fragments.fragment import Fragment +from xblock.core import XBlock +from xblock.exceptions import JsonHandlerError +from xblock.fields import Dict, Scope, String +from xblock.validation import ValidationMessage + +from .llm import get_llm_response +from .base import AIEvalXBlock +from .utils import ( + submit_code, + get_submission_result, + SUPPORTED_LANGUAGE_MAP, + LanguageLabels, +) + +logger = logging.getLogger(__name__) + +USER_RESPONSE = "USER_RESPONSE" +AI_EVALUATION = "AI_EVALUATION" +CODE_EXEC_RESULT = "CODE_EXEC_RESULT" + + +class CodingAIEvalXBlock(AIEvalXBlock): + """ + TO-DO: document what your XBlock does. + """ + + has_author_view = True + + display_name = String( + display_name=_("Display Name"), + help=_("Name of the component in the studio"), + default="Coding with AI Evaluation", + scope=Scope.settings, + ) + + judge0_api_key = String( + display_name=_("Judge0 API Key"), + help=_( + "Enter your the Judge0 API key used to execute code on Judge0." + " Get your key at https://rapidapi.com/judge0-official/api/judge0-ce." + ), + default="", + scope=Scope.settings, + ) + + language = String( + display_name=_("Programming Language"), + help=_("The programming language used for this Xblock."), + values=[ + {"display_name": language, "value": language} + for language in SUPPORTED_LANGUAGE_MAP + ], + default=LanguageLabels.Python, + Scope=Scope.settings, + ) + messages = Dict( + help=_("Dictionary with messages"), + scope=Scope.user_state, + default={USER_RESPONSE: "", AI_EVALUATION: "", CODE_EXEC_RESULT: {}}, + ) + + editable_fields = AIEvalXBlock.editable_fields + ("judge0_api_key", "language") + + def resource_string(self, path): + """Handy helper for getting resources from our kit.""" + data = pkg_resources.resource_string(__name__, path) + return data.decode("utf8") + + def student_view(self, context=None): + """ + The primary view of the CodingAIEvalXBlock, shown to students + when viewing courses. + """ + html = self.loader.render_django_template( + "/templates/coding_ai_eval.html", + { + "self": self, + }, + ) + + frag = Fragment(html) + frag.add_css(self.resource_string("static/css/coding_ai_eval.css")) + frag.add_javascript(self.resource_string("static/js/src/utils.js")) + + frag.add_javascript(self.resource_string("static/js/src/coding_ai_eval.js")) + + monaco_html = self.loader.render_django_template( + "/templates/monaco.html", + { + "monaco_language": SUPPORTED_LANGUAGE_MAP[self.language].monaco_id, + }, + ) + marked_html = self.resource_string("static/html/marked-iframe.html") + js_data = { + "monaco_html": monaco_html, + "question": self.question, + "code": self.messages[USER_RESPONSE], + "ai_evaluation": self.messages[AI_EVALUATION], + "code_exec_result": self.messages[CODE_EXEC_RESULT], + "marked_html": marked_html, + "language": self.language, + } + frag.initialize_js("CodingAIEvalXBlock", js_data) + return frag + + def author_view(self, context=None): + """ + Create preview to be show to course authors in Studio. + """ + if not self.validate(): + fragment = Fragment() + fragment.add_content( + _( + "To ensure this component works correctly, please fix the validation issues." + ) + ) + return fragment + + return self.student_view(context=context) + + def validate_field_data(self, validation, data): + """ + Validate fields + """ + + super().validate_field_data(validation, data) + + if data.language != LanguageLabels.HTML_CSS and not data.judge0_api_key: + validation.add( + ValidationMessage( + ValidationMessage.ERROR, _("Judge0 API key is mandatory") + ) + ) + + @XBlock.json_handler + def get_response(self, data, suffix=""): # pylint: disable=unused-argument + """Get LLM feedback.""" + + answer = f""" + student code : + + {data['code']} + """ + + # stdout and stderr only for executable languages (non HTML) + if self.language != LanguageLabels.HTML_CSS: + answer += f""" + stdout: + + {data['stdout']} + + stderr: + + {data['stderr']} + """ + + messages = [ + { + "role": "system", + "content": f""" + {self.evaluation_prompt} + + {self.question}. + + The programmimg language is {self.language} + + Evaluation must be in Makrdown format. + """, + }, + { + "content": f""" Here is the student's answer: + {answer} + """, + "role": "user", + }, + ] + + try: + response = get_llm_response( + self.model, + self.model_api_key, + messages, + self.model_api_url, + ) + + except Exception as e: + traceback.print_exc() + logger.error( + f"Failed while making LLM request using model {self.model}. Eaised error type: {type(e)}, Error: {e}" + ) + raise JsonHandlerError(500, "A probem occured. Please retry.") from e + + if response: + self.messages[USER_RESPONSE] = data["code"] + self.messages[AI_EVALUATION] = response + self.messages[CODE_EXEC_RESULT] = { + "stdout": data["stdout"], + "stderr": data["stderr"], + } + return {"response": response} + + raise JsonHandlerError(500, "No AI Evaluation available. Please retry.") + + @XBlock.json_handler + def submit_code_handler(self, data, suffix=""): # pylint: disable=unused-argument + """ + Submit code to Judge0. + """ + submission_id = submit_code( + self.judge0_api_key, data["user_code"], self.language + ) + return {"submission_id": submission_id} + + @XBlock.json_handler + def reset_handler(self, data, suffix=""): # pylint: disable=unused-argument + """ + Reset the Xblock. + """ + self.messages = {USER_RESPONSE: "", AI_EVALUATION: "", CODE_EXEC_RESULT: {}} + return {"message": "reset successful."} + + @XBlock.json_handler + def get_submission_result_handler( + self, data, suffix="" + ): # pylint: disable=unused-argument + """ + Get code submission result. + """ + submission_id = data["submission_id"] + return get_submission_result(self.judge0_api_key, submission_id) + + @staticmethod + def workbench_scenarios(): + """A canned scenario for display in the workbench.""" + return [ + ( + "CodingAIEvalXBlock", + """ + """, + ), + ( + "Multiple CodingAIEvalXBlock", + """ + + + + + """, + ), + ] diff --git a/ai_eval/llm.py b/ai_eval/llm.py new file mode 100644 index 0000000..0c956e7 --- /dev/null +++ b/ai_eval/llm.py @@ -0,0 +1,65 @@ +""" +Integration with LLMs. +""" + +from enum import Enum +from litellm import completion + + +class SupportedModels(Enum): + """ + LLM Models supported by the CodingAIEvalXBlock and ShortAnswerAIEvalXBlock + """ + + GPT4O = "gpt-4o" + GEMINI_PRO = "gemini/gemini-pro" + CLAUDE_SONNET = "claude-3-5-sonnet-20240620" + LLAMA = "ollama/llama2" + + @staticmethod + def list(): + return [str(m.value) for m in SupportedModels] + + +def get_llm_response( + model: SupportedModels, api_key: str, messages: list, api_base: str +) -> str: + """ + Get LLm response. + + Args: + model (SupportedModels): The model to use for generating the response. This should be an instance of + the SupportedModels enum, specifying which LLM model to call. + api_key (str): The API key required for authenticating with the LLM service. This key should be kept + confidential and used to authorize requests to the service. + messages (list): A list of message objects to be sent to the LLM. Each message should be a dictionary + with the following format: + + { + "content": str, # The content of the message. This is the text that you want to send to the LLM. + "role": str # The role of the message sender. This must be one of the following values: + # "user" - Represents a user message. + # "system" - Represents a system message, typically used for instructions or context. + # "assistant" - Represents a response or message from the LLM itself. + } + + Example: + [ + {"content": "Hello, how are you?", "role": "user"}, + {"content": "I'm here to help you.", "role": "assistant"} + ] + api_base (str): The base URL of the LLM API endpoint. This is the root URL used to construct the full + API request URL. This is required only when using Llama which doesn't have an official provider. + + Returns: + str: The response text from the LLM. This is typically the generated output based on the provided + messages. + """ + kwargs = {} + if api_base: + kwargs["api_base"] = api_base + return ( + completion(model=model, api_key=api_key, messages=messages, **kwargs) + .choices[0] + .message.content + ) diff --git a/ai_eval/shortanswer.py b/ai_eval/shortanswer.py new file mode 100644 index 0000000..e9514da --- /dev/null +++ b/ai_eval/shortanswer.py @@ -0,0 +1,152 @@ +"""Short answers Xblock with AI evaluation.""" + +import logging +import traceback + + +from django.utils.translation import gettext_noop as _ +from web_fragments.fragment import Fragment +from xblock.core import XBlock +from xblock.exceptions import JsonHandlerError +from xblock.fields import Integer, String, Scope +from xblock.validation import ValidationMessage + +from .llm import get_llm_response +from .base import AIEvalXBlock + + +logger = logging.getLogger(__name__) + + +class ShortAnswerAIEvalXBlock(AIEvalXBlock): + """ + Short Answer Xblock. + """ + + display_name = String( + display_name=_("Display Name"), + help=_("Name of the component in the studio"), + default="Short answer with AI Evaluation", + scope=Scope.settings, + ) + + max_responses = Integer( + display_name=_("Max Responses"), + help=_("The maximum number of response messages the student can submit"), + scope=Scope.settings, + default=3, + ) + + editable_fields = AIEvalXBlock.editable_fields + ("max_responses",) + + def validate_field_data(self, validation, data): + """ + Validate fields + """ + + super().validate_field_data(validation, data) + + if not data.max_responses or data.max_responses <= 0 or data.max_responses > 9: + validation.add( + ValidationMessage( + ValidationMessage.ERROR, + _("max responses must be an integer between 1 and 9"), + ) + ) + + def student_view(self, context=None): + """ + The primary view of the ShortAnswerAIEvalXBlock, shown to students + when viewing courses. + """ + + frag = Fragment() + frag.add_content( + self.loader.render_django_template( + "/templates/shortanswer.html", + { + "self": self, + }, + ) + ) + + frag.add_css(self.resource_string("static/css/shortanswer.css")) + frag.add_javascript(self.resource_string("static/js/src/utils.js")) + frag.add_javascript(self.resource_string("static/js/src/shortanswer.js")) + + marked_html = self.resource_string("static/html/marked-iframe.html") + + js_data = { + "question": self.question, + "messages": self.messages, + "max_responses": self.max_responses, + "marked_html": marked_html, + } + frag.initialize_js("ShortAnswerAIEvalXBlock", js_data) + return frag + + @XBlock.json_handler + def get_response(self, data, suffix=""): # pylint: disable=unused-argument + """Get LLM feedback""" + user_submission = str(data["user_input"]) + system_msg = { + "role": "system", + "content": f""" + {self.evaluation_prompt} + + {self.question}. + + Evaluation must be in Makrdown format. + """, + } + messages = [system_msg] + # add previous messages + # the first AI role is 'system' which defines the LLM's personnality and behavior. + # subsequent roles are 'assistant' and 'user' + for i in range(len(self.messages[self.USER_KEY])): + messages.append( + {"content": self.messages[self.USER_KEY][i], "role": "user"} + ) + messages.append( + {"content": self.messages[self.LLM_KEY][i], "role": "assistant"} + ) + + messages.append({"role": "user", "content": user_submission}) + + try: + response = get_llm_response( + self.model, self.model_api_key, messages, self.model_api_url + ) + + except Exception as e: + traceback.print_exc() + logger.error( + f"Failed while making LLM request using model {self.model}. Eaised error type: {type(e)}, Error: {e}" + ) + raise JsonHandlerError(500, "A probem occured. Please retry.") from e + + if response: + self.messages[self.USER_KEY].append(user_submission) + self.messages[self.LLM_KEY].append(response) + return {"response": response} + + raise JsonHandlerError(500, "A probem occured. The LLM sent an empty response.") + + @staticmethod + def workbench_scenarios(): + """A canned scenario for display in the workbench.""" + return [ + ( + "ShortAnswerAIEvalXBlock", + """ + """, + ), + ( + "Multiple ShortAnswerAIEvalXBlock", + """ + + + + """, + ), + ] diff --git a/ai_eval/static/README.txt b/ai_eval/static/README.txt new file mode 100644 index 0000000..0472ef6 --- /dev/null +++ b/ai_eval/static/README.txt @@ -0,0 +1,19 @@ +This static directory is for files that should be included in your kit as plain +static files. + +You can ask the runtime for a URL that will retrieve these files with: + + url = self.runtime.local_resource_url(self, "static/js/lib.js") + +The default implementation is very strict though, and will not serve files from +the static directory. It will serve files from a directory named "public". +Create a directory alongside this one named "public", and put files there. +Then you can get a url with code like this: + + url = self.runtime.local_resource_url(self, "public/js/lib.js") + +The sample code includes a function you can use to read the content of files +in the static directory, like this: + + frag.add_javascript(self.resource_string("static/js/my_block.js")) + diff --git a/ai_eval/static/css/coding_ai_eval.css b/ai_eval/static/css/coding_ai_eval.css new file mode 100644 index 0000000..1dc3548 --- /dev/null +++ b/ai_eval/static/css/coding_ai_eval.css @@ -0,0 +1,126 @@ +.used-prog-language { + flex: 5; + margin: 0.5rem 1rem; + text-align: end; +} + +.used-prog-language span { + background-color: #00262b; + color: white; + display: inline-block; + padding: .25em .4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25rem; +} + +.eval-ai-container { + display: flex; + max-height: 600px; + box-shadow: 0px 0px 10px 0px #0000001A; + border: 1px solid #CED4DA; + border-radius: 4px; +} + +.eval-ai-container .eval-ai-code-editor { + flex: 5; +} + +.eval-ai-container #monaco { + min-height: 500px; + width: 100%; + border-bottom: 1.5px solid #CED4DA; +} + +.eval-ai-container .result { + flex: 3; + background-color: #f5f5f5; + overflow-y: hidden; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.eval-ai-container .html-render { + height: 100vh; +} + +.eval-ai-container .eval-ai-buttons { + display: flex; + justify-content: flex-end; +} + +.eval-ai-container .submit-loader { + margin-left: 5px; +} + +.eval-ai-container .eval-ai-buttons .eval-ai-button { + margin: 10px; + text-align: center; + height: fit-content; + padding: 8px 12px; + border-radius: 4px; + font-size: 0.875em; + line-height: 1.25em; +} + +.eval-ai-container .eval-ai-buttons .disabled-btn { + opacity: 0.8; + cursor: not-allowed !important; +} + +.eval-ai-container .stderr { + margin-top: 10px; + color: red; + white-space: pre-wrap; +} + +.eval-ai-container .stdout { + white-space: pre-wrap; +} + +/* tabs */ +.tab { + display: flex; + border-bottom: 1px solid #edeae9; +} + +.tab .result-tab-btn { + float: left; + border: none; + outline: none; + cursor: pointer; + padding: 8px 12px; + transition: 0.3s; + font-size: 0.875em; + line-height: 1.25em; +} + +.tab .result-tab-btn:hover { + background-color: #ddd; +} + +.tab .result-tab-btn.active { + border-bottom: 2px solid #546d70; +} + +.tabcontent { + padding: 6px 12px; + border-top: none; + animation: fadeEffect 1s; + overflow-y: scroll; + height: calc(100% - 48px); +} + +@keyframes fadeEffect { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} \ No newline at end of file diff --git a/ai_eval/static/css/shortanswer.css b/ai_eval/static/css/shortanswer.css new file mode 100644 index 0000000..48ab5b8 --- /dev/null +++ b/ai_eval/static/css/shortanswer.css @@ -0,0 +1,127 @@ +/* CSS for ShortAnswerAIEvalXBlock */ + +.shortanswer_block .count { + font-weight: bold; +} + +.shortanswer_block p { + cursor: pointer; +} + +.chat-history { + min-height: 250px; + max-height: 400px; + overflow-y: auto; + margin-bottom: 10px; + padding: 0px 2em; +} + +.chat-message-container { + display: flex; +} +.chat-message-container .chat-message { + flex: 0 1 auto; + max-width: 80%; + padding: 10px 10px 5px 10px; + margin-bottom: 10px; + border-radius: 5px; +} + +.user-answer { + text-align: right; + margin-left: auto; + background-color: #f0f3f5; + color: #44586c; +} + +.ai-eval, +.message-spinner { + text-align: left; + color: white; + margin-right: auto; + background-color: #476480; +} +.ai-eval * { + color: white !important; +} + +.submit-row { + display: flex; + margin-bottom: 1em; + font-size: 16px; + padding: 10px; +} + +.submit-row .user-input { + flex: 8; + height: auto; + margin-right: 10px; + border: 1px solid gray; + border-radius: 5px; + overflow: auto; + max-height: 200px; + resize: none; +} + +.submit-row #submit-button { + flex: 1; + height: fit-content; + text-align: center; + padding: 10px 0; + margin-left: auto; + background-color: #00262b; + color: white; + border-radius: 5px; + border: none; + cursor: pointer; +} +.submit-row .disabled-btn { + opacity: 0.8; + cursor: not-allowed !important; +} +/* spinner animation */ +.chat-message-container .message-spinner > div { + width: 4px; + height: 4px; + margin-right: 2px; + background-color: white; + + border-radius: 100%; + display: inline-block; + -webkit-animation: chat-block-sk-bouncedelay 1.4s infinite ease-in-out both; + animation: chat-block-sk-bouncedelay 1.4s infinite ease-in-out both; +} + +.chat-message-container.message-spinner .bounce1 { + -webkit-animation-delay: -0.32s; + animation-delay: -0.32s; +} + +.chat-message-container .message-spinner .bounce2 { + -webkit-animation-delay: -0.16s; + animation-delay: -0.16s; +} + +@-webkit-keyframes chat-block-sk-bouncedelay { + 0%, + 80%, + 100% { + -webkit-transform: scale(0); + } + 40% { + -webkit-transform: scale(1); + } +} + +@keyframes chat-block-sk-bouncedelay { + 0%, + 80%, + 100% { + -webkit-transform: scale(0); + transform: scale(0); + } + 40% { + -webkit-transform: scale(1); + transform: scale(1); + } +} diff --git a/ai_eval/static/html/marked-iframe.html b/ai_eval/static/html/marked-iframe.html new file mode 100644 index 0000000..d21a301 --- /dev/null +++ b/ai_eval/static/html/marked-iframe.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ai_eval/static/js/src/coding_ai_eval.js b/ai_eval/static/js/src/coding_ai_eval.js new file mode 100644 index 0000000..e5ef4d5 --- /dev/null +++ b/ai_eval/static/js/src/coding_ai_eval.js @@ -0,0 +1,235 @@ +/* Javascript for CodingAIEvalXBlock. */ +function CodingAIEvalXBlock(runtime, element, data) { + const runCodeHandlerURL = runtime.handlerUrl(element, "submit_code_handler"); + const resetHandlerURL = runtime.handlerUrl(element, "reset_handler"); + + + const submissionResultURL = runtime.handlerUrl( + element, + "get_submission_result_handler", + ); + loadMarkedInIframe(data.marked_html); + const llmResponseHandlerURL = runtime.handlerUrl(element, "get_response"); + const HTML_CSS = "HTML/CSS"; + const HTML_PLACEHOLER = + "\n\n\n\n\n\n

This is a heading

\n

This is a paragraph.

\n\n"; + + const iframe = $("#monaco", element)[0]; + const submitButton = $("#submit-button", element); + const resetButton = $("#reset-button", element); + const AIFeeback = $("#ai-feedback", element); + const stdout = $(".stdout", element); + const stderr = $(".stderr", element); + const htmlRenderIframe = $(".html-render", element); + + const MAX_JUDGE0_RETRY_ITER = 5; + const WAIT_TIME_MS = 1000; + + $(function () { + // The newer runtime uses the 'data-usage' attribute, while the LMS uses 'data-usage-id' + // A Jquery object can sometimes be returned e.g. after a studio field edit, we handle it with ?.[0] + const xblockUsageId = + element.getAttribute?.("data-usage") || + element.getAttribute?.("data-usage-id") || + element?.[0].getAttribute("data-usage-id"); + + if (!xblockUsageId) { + throw new Error( + "XBlock is missing a usage ID attribute on its root HTML node.", + ); + } + + // __USAGE_ID_PLACEHOLDER__ is the event data sent from the monaco iframe after loading + // we rely on the usage_id to limit the event to the Xblock scope + iframe.srcdoc = data.monaco_html.replace( + "__USAGE_ID_PLACEHOLDER__", + xblockUsageId, + ); + runFuncAfterLoading(init); + function submitCode() { + const code = iframe.contentWindow.editor.getValue(); + return $.ajax({ + url: runCodeHandlerURL, + method: "POST", + data: JSON.stringify({ user_code: code }), + }); + } + function delay(ms, data) { + const deferred = $.Deferred(); + setTimeout(function () { + deferred.resolve(data); + }, ms); + return deferred.promise(); + } + function getSubmissionResult(data) { + let retries = 0; + const deferred = $.Deferred(); + + function attempt() { + return $.ajax({ + url: submissionResultURL, + method: "POST", + data: JSON.stringify({ submission_id: data.submission_id }), + }) + .then(function (result) { + console.log("result", result, retries) + if (result.status.id === 1 || result.status.id === 2) { + // https://ce.judge0.com/#statuses-and-languages-status-get + // Retry if status is 1 (In Queue) or 2 (Processing) + if (retries < MAX_JUDGE0_RETRY_ITER) { + retries++; + console.log("Judge0 fetch result retry attempt:", retries); + setTimeout(function () { + attempt(); + }, WAIT_TIME_MS); + } else { + deferred.reject(new Error("Judge0 submission result fetch failed after " + MAX_JUDGE0_RETRY_ITER + " attempts.")); + } + } else { + const output = [result.compile_output, result.stdout].join("\n").trim(); + stdout.text(output); + stderr.text(result.stderr); + deferred.resolve(result); + } + + + }) + .fail(function (error) { + console.log("Error: ", error); + deferred.reject(new Error("An error occured while trying to fetch Judge0 submission result.")); + }); + } + attempt(); + return deferred.promise(); + } + + function getLLMFeedback(data) { + return $.ajax({ + url: llmResponseHandlerURL, + method: "POST", + data: JSON.stringify({ + code: iframe.contentWindow.editor.getValue(), + stdout: data.stdout, + stderr: data.stderr, + }), + success: function (data) { + console.log(data); + AIFeeback.html(MarkdownToHTML(data.response)); + $("#ai-feedback-tab", element).click(); + }, + }); + } + + resetButton.click(() => { + $.ajax({ + url: resetHandlerURL, + method: "POST", + data: JSON.stringify({}), + success: function (data) { + iframe.contentWindow.editor.setValue(""); + AIFeeback.html(""); + if (data.language !== HTML_CSS) { + stdout.text(""); + stderr.text(""); + } + }, + error: function(xhr, status, error) { + console.error('Error:', error); + alert("A problem occured during reset."); + } + }); + + }); + + submitButton.click(() => { + const code = iframe.contentWindow.editor.getValue(); + if (!code?.length) { + return; + } + disableSubmitButton(); + var deferred = null; + if (data.language === HTML_CSS) { + // no need to submit HTML code, we directly get LLM feedback + deferred = getLLMFeedback({ stdout: "", stderr: "" }); + } else { + deferred = submitCode() + .then(function (data) { + return delay(WAIT_TIME_MS * 2, data); + }) + .then(getSubmissionResult) + .then(getLLMFeedback); + } + + deferred + .done(function (data) { + enableSubmitButton(); + }) + .fail(function (error) { + console.log("Error: ", error); + enableSubmitButton(); + alert("A problem occured while submitting the code."); + }); + }); + + function init() { + $("#question-text", element).html(MarkdownToHTML(data.question)); + // Triggered when the Monaco editor loads. + // Since a Unit can have multiple instances of this Xblock, + // we use the XblockUsageId to differentiate between them. + window.addEventListener("message", function (event) { + if (event.data === xblockUsageId) { + if (data.code?.length) { + iframe.contentWindow.editor.setValue(data.code); + } + + AIFeeback.html(MarkdownToHTML(data.ai_evaluation || "")); + if (data.language === HTML_CSS) { + // render HTML/CSS into iframe + if (data.code?.length) { + renderUserHTML(data.code); + } else { + iframe.contentWindow.editor.setValue(HTML_PLACEHOLER); + renderUserHTML(HTML_PLACEHOLER); + } + addMonacoHTMLRenderEventListener(); + } else { + // load existing results for executable languages + stdout.text(data.code_exec_result?.stdout || ""); + stderr.text(data.code_exec_result?.stderr || ""); + } + } + }); + } + function addMonacoHTMLRenderEventListener() { + iframe.contentWindow.editor.onDidChangeModelContent((event) => { + renderUserHTML(iframe.contentWindow.editor.getValue()); + }); + } + function renderUserHTML(userHTML) { + htmlRenderIframe.attr("srcdoc", stripScriptTags(userHTML)); + } + }); + + function disableSubmitButton() { + submitButton.append(''); + submitButton.prop("disabled", true); + submitButton.addClass("disabled-btn"); + } + + function enableSubmitButton() { + $(".submit-loader", element).remove(); + submitButton.prop("disabled", false); + submitButton.removeClass("disabled-btn"); + } + + // basic tabs + $(".tablinks", element).click((event) => { + $(".tabcontent", element).hide(); + $(".tablinks", element).removeClass("active"); + $(event.target).addClass("active"); + const contentID = "#" + $(event.target).data("id"); + $(contentID, element).show(); + }); + // default tab + $("#defaultOpen", element).click(); +} diff --git a/ai_eval/static/js/src/shortanswer.js b/ai_eval/static/js/src/shortanswer.js new file mode 100644 index 0000000..4bbd0ab --- /dev/null +++ b/ai_eval/static/js/src/shortanswer.js @@ -0,0 +1,105 @@ +/* Javascript for ShortAnswerAIEvalXBlock. */ +function ShortAnswerAIEvalXBlock(runtime, element, data) { + const handlerUrl = runtime.handlerUrl(element, "get_response"); + + loadMarkedInIframe(data.marked_html); + + $(function () { + const spinner = $(".message-spinner", element); + const spinnnerContainer = $("#chat-spinner-container", element); + const submitButton = $("#submit-button", element); + const userInput = $(".user-input", element); + const userInputElem = userInput[0]; + let initDone = false; + + runFuncAfterLoading(init); + + function getResponse() { + if (!userInput.val().length) return; + + disableInput(); + spinner.show(); + insertUserMessage(userInput.val()); + $.ajax({ + url: handlerUrl, + method: "POST", + data: JSON.stringify({ user_input: userInput.val() }), + success: function (response) { + spinner.hide(); + insertAIMessage(response.response); + userInput.val(""); + if ($(".user-answer", element).length >= data.max_responses) { + disableInput(); + } else { + enableInput(); + } + }, + error: function (jqXHR, textStatus, errorThrown) { + spinner.hide(); + alert(errorThrown); + + deleteLastMessage(); + enableInput(); + }, + }); + } + + submitButton.click(getResponse); + + function disableInput() { + userInput.prop("disabled", true); + userInput.removeAttr("placeholder"); + submitButton.prop("disabled", true); + submitButton.addClass("disabled-btn"); + } + + function enableInput() { + userInput.prop("disabled", false); + submitButton.prop("disabled", false); + submitButton.removeClass("disabled-btn"); + } + + function adjustTextareaHeight(element) { + element.style.height = ""; + element.style.height = element.scrollHeight + "px"; + } + userInputElem.addEventListener("input", (event) => { + adjustTextareaHeight(userInputElem); + }); + + function init() { + if (initDone) return; + initDone = true; + $("#question-text", element).html(MarkdownToHTML(data.question)); + for (let i = 0; i < data.messages.USER.length; i++) { + insertUserMessage(data.messages.USER[i]); + insertAIMessage(data.messages.LLM[i]); + } + if ( + data.messages.USER.length && + data.messages.USER.length >= data.max_responses + ) { + disableInput(); + } + } + + function insertUserMessage(msg) { + if (msg?.length) { + $(`
+
${MarkdownToHTML(msg)}
+
`).insertBefore(spinnnerContainer); + } + } + + function insertAIMessage(msg) { + if (msg?.length) { + $(`
+
${MarkdownToHTML(msg)}
+
`).insertBefore(spinnnerContainer); + } + } + function deleteLastMessage() { + spinnnerContainer.prev().remove(); + } + }); +} diff --git a/ai_eval/static/js/src/utils.js b/ai_eval/static/js/src/utils.js new file mode 100644 index 0000000..063ef98 --- /dev/null +++ b/ai_eval/static/js/src/utils.js @@ -0,0 +1,41 @@ +// we load Marked in a Iframe to avoid RequireJS conflicts with the Xblock runtime +function loadMarkedInIframe(html) { + if (!window.marked && !document.getElementById("marked-iframe")) { + const iframe = document.createElement("iframe"); + iframe.style.display = "none"; + iframe.setAttribute("id", "marked-iframe"); + iframe.srcdoc = html; + document.body.appendChild(iframe); + } +} +function MarkdownToHTML(text) { + window.marked = + window.marked ?? + document.getElementById("marked-iframe").contentWindow.marked; + if (typeof marked !== "undefined") text = marked.parse(text); + return text; +} + +function runFuncAfterLoading(func) { + const markedIframe = document.getElementById("marked-iframe"); + // loading is finished when Marked is available + if (markedIframe.contentWindow.marked) { + func(); + } else { + markedIframe.addEventListener("load", (event) => { + func(); + }); + } +} + +function stripScriptTags(html) { + const div = document.createElement("div"); + div.innerHTML = html; + + const scripts = div.getElementsByTagName("script"); + while (scripts.length > 0) { + scripts[0].parentNode.removeChild(scripts[0]); + } + + return div.innerHTML; +} diff --git a/ai_eval/templates/coding_ai_eval.html b/ai_eval/templates/coding_ai_eval.html new file mode 100644 index 0000000..4472024 --- /dev/null +++ b/ai_eval/templates/coding_ai_eval.html @@ -0,0 +1,31 @@ +
+
+
+
+ {{ self.language }} +
+ +
+ + +
+
+ +
+
+ Output + AI feedback +
+ +
+ {% if self.language == "HTML/CSS" %} + + {% else %} +
+
+ {% endif %} +
+ +
+
+
diff --git a/ai_eval/templates/monaco.html b/ai_eval/templates/monaco.html new file mode 100644 index 0000000..56de7e5 --- /dev/null +++ b/ai_eval/templates/monaco.html @@ -0,0 +1,22 @@ + + + + + Monaco Editor + + +
+ + + diff --git a/ai_eval/templates/shortanswer.html b/ai_eval/templates/shortanswer.html new file mode 100644 index 0000000..cc73600 --- /dev/null +++ b/ai_eval/templates/shortanswer.html @@ -0,0 +1,26 @@ +
+
+
+ Loading .. +
+
+ +
+
+
+ +
+
+
+ + Submit +
+
+
\ No newline at end of file diff --git a/ai_eval/tests/__init__.py b/ai_eval/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ai_eval/tests/test_ai_eval.py b/ai_eval/tests/test_ai_eval.py new file mode 100644 index 0000000..0623558 --- /dev/null +++ b/ai_eval/tests/test_ai_eval.py @@ -0,0 +1,55 @@ +""" +Testing module. +""" + +import unittest +from xblock.field_data import DictFieldData +from xblock.test.toy_runtime import ToyRuntime +from ai_eval import CodingAIEvalXBlock, ShortAnswerAIEvalXBlock + + +class TestCodingAIEvalXBlock(unittest.TestCase): + """Tests for CodingAIEvalXBlock""" + + def test_basics_student_view(self): + """Test the basic view loads.""" + data = { + "language": "Python", + "question": "ca va?", + "code": "", + "ai_evaluation": "", + "code_exec_result": {}, + "marked_html": '\n\n\n\n \n\n', + "monaco_html": '\n\n\n \n' + ' Monaco Editor\n\n\n
\n \n\n\n', + } + block = CodingAIEvalXBlock(ToyRuntime(), DictFieldData(data), None) + frag = block.student_view() + self.assertEqual(data, frag.json_init_args) + self.assertIn('
', frag.content) + + +class TestShortAnswerAIEvalXBlock(unittest.TestCase): + """Tests for ShortAnswerAIEvalXBlock""" + + def test_basics_student_view(self): + """Test the basic view loads.""" + data = { + "question": "ca va?", + "messages": {"USER": [], "LLM": []}, + "max_responses": 3, + "marked_html": '\n\n\n\n \n\n', + } + block = ShortAnswerAIEvalXBlock(ToyRuntime(), DictFieldData(data), None) + frag = block.student_view() + self.assertEqual(data, frag.json_init_args) + self.assertIn('
', frag.content) diff --git a/ai_eval/translations/README.txt b/ai_eval/translations/README.txt new file mode 100644 index 0000000..0493bcc --- /dev/null +++ b/ai_eval/translations/README.txt @@ -0,0 +1,4 @@ +Use this translations directory to provide internationalized strings for your XBlock project. + +For more information on how to enable translations, visit the Open edX XBlock tutorial on Internationalization: +http://edx.readthedocs.org/projects/xblock-tutorial/en/latest/edx_platform/edx_lms.html diff --git a/ai_eval/utils.py b/ai_eval/utils.py new file mode 100644 index 0000000..cf866bf --- /dev/null +++ b/ai_eval/utils.py @@ -0,0 +1,84 @@ +""" +Utilities +""" + +from dataclasses import dataclass +import requests + + +@dataclass +class ProgrammimgLanguage: + """A programming language.""" + + monaco_id: str + judge0_id: int + + +class LanguageLabels: + """Language labels as seen by users.""" + + Python = "Python" + JavaScript = "JavaScript" + Java = "Java" + CPP = "C++" + HTML_CSS = "HTML/CSS" + + +# supported programming languages and their IDs in judge0 and monaco +# https://ce.judge0.com/#statuses-and-languages-active-and-archived-languages +SUPPORTED_LANGUAGE_MAP = { + LanguageLabels.Python: ProgrammimgLanguage( + monaco_id="python", judge0_id=92 + ), # Python (3.11.2) + LanguageLabels.JavaScript: ProgrammimgLanguage( + monaco_id="javascript", judge0_id=93 + ), # JavaScript (Node.js 18.15.0) + LanguageLabels.Java: ProgrammimgLanguage( + monaco_id="java", judge0_id=91 + ), # Java (JDK 17.0.6) + LanguageLabels.CPP: ProgrammimgLanguage( + monaco_id="cpp", judge0_id=54 + ), # C++ (GCC 9.2.0) + # Monaco's HTML support includes CSS support within the 'style' tag. + LanguageLabels.HTML_CSS: ProgrammimgLanguage( + monaco_id="html", judge0_id=-1 + ), # no exec +} + + +JUDGE0_BASE_CE_URL = "https://judge0-ce.p.rapidapi.com" + + +def submit_code(api_key: str, code: str, language: str) -> str: + """ + Submit code to the judge0 API. + """ + url = f"{JUDGE0_BASE_CE_URL}/submissions?base64_encoded=false&wait=false" + headers = {"content-type": "application/json", "x-rapidapi-key": api_key} + + data = { + "source_code": code, + "language_id": SUPPORTED_LANGUAGE_MAP[language].judge0_id, + } + + response = requests.post(url, headers=headers, json=data, timeout=10) + response.raise_for_status() + result = response.json() + sub_id = result["token"] + + return sub_id + + +def get_submission_result(api_key: str, submission_id: str): + """ + Get result from Judge0 submission. + """ + + url = f"{JUDGE0_BASE_CE_URL}/submissions/{submission_id}?base64_encoded=false&fields=*" + headers = {"content-type": "application/json", "x-rapidapi-key": api_key} + + response = requests.get(url, headers=headers, timeout=10) + response.raise_for_status() + result = response.json() + + return result diff --git a/docs/coding-xblock-ai-feedback.png b/docs/coding-xblock-ai-feedback.png new file mode 100644 index 0000000..d676db2 Binary files /dev/null and b/docs/coding-xblock-ai-feedback.png differ diff --git a/docs/coding-xblock-html.png b/docs/coding-xblock-html.png new file mode 100644 index 0000000..df43202 Binary files /dev/null and b/docs/coding-xblock-html.png differ diff --git a/docs/coding-xblock.png b/docs/coding-xblock.png new file mode 100644 index 0000000..cf8a0a3 Binary files /dev/null and b/docs/coding-xblock.png differ diff --git a/docs/shortanswer-xblock.png b/docs/shortanswer-xblock.png new file mode 100644 index 0000000..b55f91f Binary files /dev/null and b/docs/shortanswer-xblock.png differ diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..2c17a7b --- /dev/null +++ b/pylintrc @@ -0,0 +1,389 @@ +# *************************** +# ** DO NOT EDIT THIS FILE ** +# *************************** +# +# This file was generated by edx-lint: https://github.com/openedx/edx-lint +# +# If you want to change this file, you have two choices, depending on whether +# you want to make a local change that applies only to this repo, or whether +# you want to make a central change that applies to all repos using edx-lint. +# +# Note: If your pylintrc file is simply out-of-date relative to the latest +# pylintrc in edx-lint, ensure you have the latest edx-lint installed +# and then follow the steps for a "LOCAL CHANGE". +# +# LOCAL CHANGE: +# +# 1. Edit the local pylintrc_tweaks file to add changes just to this +# repo's file. +# +# 2. Run: +# +# $ edx_lint write pylintrc +# +# 3. This will modify the local file. Submit a pull request to get it +# checked in so that others will benefit. +# +# +# CENTRAL CHANGE: +# +# 1. Edit the pylintrc file in the edx-lint repo at +# https://github.com/openedx/edx-lint/blob/master/edx_lint/files/pylintrc +# +# 2. install the updated version of edx-lint (in edx-lint): +# +# $ pip install . +# +# 3. Run (in edx-lint): +# +# $ edx_lint write pylintrc +# +# 4. Make a new version of edx_lint, submit and review a pull request with the +# pylintrc update, and after merging, update the edx-lint version and +# publish the new version. +# +# 5. In your local repo, install the newer version of edx-lint. +# +# 6. Run: +# +# $ edx_lint write pylintrc +# +# 7. This will modify the local file. Submit a pull request to get it +# checked in so that others will benefit. +# +# +# +# +# +# STAY AWAY FROM THIS FILE! +# +# +# +# +# +# SERIOUSLY. +# +# ------------------------------ +# Generated by edx-lint version: 5.3.6 +# ------------------------------ +[MASTER] +ignore = migrations +persistent = yes +load-plugins = edx_lint.pylint,pylint_django,pylint_celery + +[MESSAGES CONTROL] +enable = + blacklisted-name, + line-too-long, + + abstract-class-instantiated, + abstract-method, + access-member-before-definition, + anomalous-backslash-in-string, + anomalous-unicode-escape-in-string, + arguments-differ, + assert-on-tuple, + assigning-non-slot, + assignment-from-no-return, + assignment-from-none, + attribute-defined-outside-init, + bad-except-order, + bad-format-character, + bad-format-string-key, + bad-format-string, + bad-open-mode, + bad-reversed-sequence, + bad-staticmethod-argument, + bad-str-strip-call, + bad-super-call, + binary-op-exception, + boolean-datetime, + catching-non-exception, + cell-var-from-loop, + confusing-with-statement, + continue-in-finally, + dangerous-default-value, + duplicate-argument-name, + duplicate-bases, + duplicate-except, + duplicate-key, + expression-not-assigned, + format-combined-specification, + format-needs-mapping, + function-redefined, + global-variable-undefined, + import-error, + import-self, + inconsistent-mro, + inherit-non-class, + init-is-generator, + invalid-all-object, + invalid-format-index, + invalid-length-returned, + invalid-sequence-index, + invalid-slice-index, + invalid-slots-object, + invalid-slots, + invalid-unary-operand-type, + logging-too-few-args, + logging-too-many-args, + logging-unsupported-format, + lost-exception, + method-hidden, + misplaced-bare-raise, + misplaced-future, + missing-format-argument-key, + missing-format-attribute, + missing-format-string-key, + no-member, + no-method-argument, + no-name-in-module, + no-self-argument, + no-value-for-parameter, + non-iterator-returned, + non-parent-method-called, + nonexistent-operator, + not-a-mapping, + not-an-iterable, + not-callable, + not-context-manager, + not-in-loop, + pointless-statement, + pointless-string-statement, + raising-bad-type, + raising-non-exception, + redefined-builtin, + redefined-outer-name, + redundant-keyword-arg, + repeated-keyword, + return-arg-in-generator, + return-in-init, + return-outside-function, + signature-differs, + super-init-not-called, + super-method-not-called, + syntax-error, + test-inherits-tests, + too-few-format-args, + too-many-format-args, + too-many-function-args, + translation-of-non-string, + truncated-format-string, + undefined-all-variable, + undefined-loop-variable, + undefined-variable, + unexpected-keyword-arg, + unexpected-special-method-signature, + unpacking-non-sequence, + unreachable, + unsubscriptable-object, + unsupported-binary-operation, + unsupported-membership-test, + unused-format-string-argument, + unused-format-string-key, + used-before-assignment, + using-constant-test, + yield-outside-function, + + astroid-error, + fatal, + method-check-failed, + parse-error, + raw-checker-failed, + + empty-docstring, + invalid-characters-in-docstring, + missing-docstring, + wrong-spelling-in-comment, + wrong-spelling-in-docstring, + + unused-argument, + unused-import, + unused-variable, + + eval-used, + exec-used, + + bad-classmethod-argument, + bad-mcs-classmethod-argument, + bad-mcs-method-argument, + bare-except, + broad-except, + consider-iterating-dictionary, + consider-using-enumerate, + global-at-module-level, + global-variable-not-assigned, + literal-used-as-attribute, + logging-format-interpolation, + logging-not-lazy, + multiple-imports, + multiple-statements, + no-classmethod-decorator, + no-staticmethod-decorator, + protected-access, + redundant-unittest-assert, + reimported, + simplifiable-if-statement, + simplifiable-range, + singleton-comparison, + superfluous-parens, + unidiomatic-typecheck, + unnecessary-lambda, + unnecessary-pass, + unnecessary-semicolon, + unneeded-not, + useless-else-on-loop, + wrong-assert-type, + + deprecated-method, + deprecated-module, + + too-many-boolean-expressions, + too-many-nested-blocks, + too-many-statements, + + wildcard-import, + wrong-import-order, + wrong-import-position, + + missing-final-newline, + mixed-line-endings, + trailing-newlines, + trailing-whitespace, + unexpected-line-ending-format, + + bad-inline-option, + bad-option-value, + deprecated-pragma, + unrecognized-inline-option, + useless-suppression, +disable = + bad-indentation, + broad-exception-raised, + consider-using-f-string, + duplicate-code, + file-ignored, + fixme, + global-statement, + invalid-name, + locally-disabled, + no-else-return, + suppressed-message, + too-few-public-methods, + too-many-ancestors, + too-many-arguments, + too-many-branches, + too-many-instance-attributes, + too-many-lines, + too-many-locals, + too-many-public-methods, + too-many-return-statements, + ungrouped-imports, + unspecified-encoding, + unused-wildcard-import, + use-maxsplit-arg, + + feature-toggle-needs-doc, + illegal-waffle-usage, + + logging-fstring-interpolation, + django-not-configured, + unused-argument, + unsubscriptable-object + +[REPORTS] +output-format = text +reports = no +score = no + +[BASIC] +module-rgx = (([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ +const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$ +class-rgx = [A-Z_][a-zA-Z0-9]+$ +function-rgx = ([a-z_][a-z0-9_]{2,40}|test_[a-z0-9_]+)$ +method-rgx = ([a-z_][a-z0-9_]{2,40}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff|test_[a-z0-9_]+)$ +attr-rgx = [a-z_][a-z0-9_]{2,30}$ +argument-rgx = [a-z_][a-z0-9_]{2,30}$ +variable-rgx = [a-z_][a-z0-9_]{2,30}$ +class-attribute-rgx = ([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ +inlinevar-rgx = [A-Za-z_][A-Za-z0-9_]*$ +good-names = f,i,j,k,db,ex,Run,_,__ +bad-names = foo,bar,baz,toto,tutu,tata +no-docstring-rgx = __.*__$|test_.+|setUp$|setUpClass$|tearDown$|tearDownClass$|Meta$ +docstring-min-length = 5 + +[FORMAT] +max-line-length = 120 +ignore-long-lines = ^\s*(# )?((?)|(\.\. \w+: .*))$ +single-line-if-stmt = no +max-module-lines = 1000 +indent-string = ' ' + +[MISCELLANEOUS] +notes = FIXME,XXX,TODO + +[SIMILARITIES] +min-similarity-lines = 4 +ignore-comments = yes +ignore-docstrings = yes +ignore-imports = no + +[TYPECHECK] +ignore-mixin-members = yes +ignored-classes = SQLObject +unsafe-load-any-extension = yes +generated-members = + REQUEST, + acl_users, + aq_parent, + objects, + DoesNotExist, + can_read, + can_write, + get_url, + size, + content, + status_code, + create, + build, + fields, + tag, + org, + course, + category, + name, + revision, + _meta, + +[VARIABLES] +init-import = no +dummy-variables-rgx = _|dummy|unused|.*_unused +additional-builtins = + +[CLASSES] +defining-attr-methods = __init__,__new__,setUp +valid-classmethod-first-arg = cls +valid-metaclass-classmethod-first-arg = mcs + +[DESIGN] +max-args = 5 +ignored-argument-names = _.* +max-locals = 15 +max-returns = 6 +max-branches = 12 +max-statements = 50 +max-parents = 7 +max-attributes = 7 +min-public-methods = 2 +max-public-methods = 20 + +[IMPORTS] +deprecated-modules = regsub,TERMIOS,Bastion,rexec +import-graph = +ext-import-graph = +int-import-graph = + +[EXCEPTIONS] +overgeneral-exceptions = builtins.Exception + +# 6114ba904f03712e1def5d0f459a5ce5a0927223 diff --git a/pylintrc_tweaks b/pylintrc_tweaks new file mode 100644 index 0000000..f24c494 --- /dev/null +++ b/pylintrc_tweaks @@ -0,0 +1,10 @@ +# pylintrc tweaks for use with edx_lint. +[MASTER] +ignore = migrations +load-plugins = edx_lint.pylint,pylint_django,pylint_celery + +[MESSAGES CONTROL] +disable+= + django-not-configured, + unused-argument, + unsubscriptable-object diff --git a/requirements/base.in b/requirements/base.in new file mode 100644 index 0000000..c02823b --- /dev/null +++ b/requirements/base.in @@ -0,0 +1,6 @@ +# Core requirements for using this application +-c constraints.txt + +django-statici18n +XBlock[django] +litellm \ No newline at end of file diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000..6e546df --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,221 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +aiohappyeyeballs==2.3.4 + # via aiohttp +aiohttp==3.10.0 + # via litellm +aiosignal==1.3.1 + # via aiohttp +annotated-types==0.7.0 + # via pydantic +anyio==4.4.0 + # via + # httpx + # openai +appdirs==1.4.4 + # via fs +asgiref==3.8.1 + # via django +async-timeout==4.0.3 + # via aiohttp +attrs==23.2.0 + # via + # aiohttp + # jsonschema + # referencing +backports-zoneinfo==0.2.1 ; python_version < "3.9" + # via + # -c requirements/constraints.txt + # django +boto3==1.34.151 + # via fs-s3fs +botocore==1.34.151 + # via + # boto3 + # s3transfer +certifi==2024.7.4 + # via + # httpcore + # httpx + # requests +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via litellm +distro==1.9.0 + # via openai +django==4.2.14 + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # django-appconf + # django-statici18n + # openedx-django-pyfs +django-appconf==1.0.6 + # via django-statici18n +django-statici18n==2.5.0 + # via -r requirements/base.in +exceptiongroup==1.2.2 + # via anyio +filelock==3.15.4 + # via huggingface-hub +frozenlist==1.4.1 + # via + # aiohttp + # aiosignal +fs==2.4.16 + # via + # fs-s3fs + # openedx-django-pyfs + # xblock +fs-s3fs==1.1.1 + # via openedx-django-pyfs +fsspec==2024.6.1 + # via huggingface-hub +h11==0.14.0 + # via httpcore +httpcore==1.0.5 + # via httpx +httpx==0.27.0 + # via openai +huggingface-hub==0.24.5 + # via tokenizers +idna==3.7 + # via + # anyio + # httpx + # requests + # yarl +importlib-metadata==6.11.0 + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # litellm +importlib-resources==6.4.0 + # via + # jsonschema + # jsonschema-specifications +jinja2==3.1.4 + # via litellm +jmespath==1.0.1 + # via + # boto3 + # botocore +jsonschema==4.23.0 + # via litellm +jsonschema-specifications==2023.12.1 + # via jsonschema +lazy==1.6 + # via xblock +litellm==1.42.6 + # via -r requirements/base.in +lxml==5.2.2 + # via xblock +mako==1.3.5 + # via xblock +markupsafe==2.1.5 + # via + # jinja2 + # mako + # xblock +multidict==6.0.5 + # via + # aiohttp + # yarl +openai==1.37.1 + # via litellm +openedx-django-pyfs==3.6.0 + # via xblock +packaging==24.1 + # via huggingface-hub +pkgutil-resolve-name==1.3.10 + # via jsonschema +pydantic==2.8.2 + # via + # litellm + # openai +pydantic-core==2.20.1 + # via pydantic +python-dateutil==2.9.0.post0 + # via + # botocore + # xblock +python-dotenv==1.0.1 + # via litellm +pytz==2024.1 + # via xblock +pyyaml==6.0.1 + # via + # huggingface-hub + # xblock +referencing==0.35.1 + # via + # jsonschema + # jsonschema-specifications +regex==2024.7.24 + # via tiktoken +requests==2.32.3 + # via + # huggingface-hub + # litellm + # tiktoken +rpds-py==0.19.1 + # via + # jsonschema + # referencing +s3transfer==0.10.2 + # via boto3 +simplejson==3.19.2 + # via xblock +six==1.16.0 + # via + # fs + # fs-s3fs + # python-dateutil +sniffio==1.3.1 + # via + # anyio + # httpx + # openai +sqlparse==0.5.1 + # via django +tiktoken==0.7.0 + # via litellm +tokenizers==0.19.1 + # via litellm +tqdm==4.66.4 + # via + # huggingface-hub + # openai +typing-extensions==4.12.2 + # via + # annotated-types + # anyio + # asgiref + # huggingface-hub + # openai + # pydantic + # pydantic-core +urllib3==1.26.19 + # via + # botocore + # requests +web-fragments==2.2.0 + # via xblock +webob==1.8.7 + # via xblock +xblock[django]==4.0.1 + # via + # -c requirements/constraints.txt + # -r requirements/base.in +yarl==1.9.4 + # via aiohttp +zipp==3.19.2 + # via + # importlib-metadata + # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/ci.in b/requirements/ci.in new file mode 100644 index 0000000..3c1c718 --- /dev/null +++ b/requirements/ci.in @@ -0,0 +1,4 @@ +# Requirements for running tests in CI +-c constraints.txt + +tox # Virtualenv management for tests diff --git a/requirements/ci.txt b/requirements/ci.txt new file mode 100644 index 0000000..f0993ea --- /dev/null +++ b/requirements/ci.txt @@ -0,0 +1,38 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +cachetools==5.4.0 + # via tox +chardet==5.2.0 + # via tox +colorama==0.4.6 + # via tox +distlib==0.3.8 + # via virtualenv +filelock==3.15.4 + # via + # tox + # virtualenv +packaging==24.1 + # via + # pyproject-api + # tox +platformdirs==4.2.2 + # via + # tox + # virtualenv +pluggy==1.5.0 + # via tox +pyproject-api==1.7.1 + # via tox +tomli==2.0.1 + # via + # pyproject-api + # tox +tox==4.16.0 + # via -r requirements/ci.in +virtualenv==20.26.3 + # via tox diff --git a/requirements/constraints.txt b/requirements/constraints.txt new file mode 100644 index 0000000..4146286 --- /dev/null +++ b/requirements/constraints.txt @@ -0,0 +1,17 @@ +# Version constraints for pip-installation. +# +# This file doesn't install any packages. It specifies version constraints +# that will be applied if a package is needed. +# +# When pinning something here, please provide an explanation of why. Ideally, +# link to other information that will help people in the future to remove the +# pin when possible. Writing an issue against the offending project and +# linking to it here is good. + +# Common constraints for edx repos +-c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + +# For python greater than or equal to 3.9 backports.zoneinfo causing failures +backports.zoneinfo; python_version<"3.9" + +xblock[django]==4.0.1 \ No newline at end of file diff --git a/requirements/dev.in b/requirements/dev.in new file mode 100644 index 0000000..7456111 --- /dev/null +++ b/requirements/dev.in @@ -0,0 +1,6 @@ +# Additional requirements for development of this application +-c constraints.txt + +-r pip-tools.txt # pip-tools and its dependencies, for managing requirements files +-r quality.txt # Core and quality check dependencies +-r ci.txt # dependencies for setting up testing in CI diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..85f0896 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,579 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +aiohappyeyeballs==2.3.4 + # via + # -r requirements/quality.txt + # aiohttp +aiohttp==3.10.0 + # via + # -r requirements/quality.txt + # litellm +aiosignal==1.3.1 + # via + # -r requirements/quality.txt + # aiohttp +annotated-types==0.7.0 + # via + # -r requirements/quality.txt + # pydantic +anyio==4.4.0 + # via + # -r requirements/quality.txt + # httpx + # openai +appdirs==1.4.4 + # via + # -r requirements/quality.txt + # fs +arrow==1.3.0 + # via + # -r requirements/quality.txt + # cookiecutter +asgiref==3.8.1 + # via + # -r requirements/quality.txt + # django +astroid==3.2.4 + # via + # -r requirements/quality.txt + # pylint + # pylint-celery +async-timeout==4.0.3 + # via + # -r requirements/quality.txt + # aiohttp +attrs==23.2.0 + # via + # -r requirements/quality.txt + # aiohttp + # jsonschema + # referencing +backports-zoneinfo==0.2.1 ; python_version < "3.9" + # via + # -c requirements/constraints.txt + # -r requirements/quality.txt + # django +binaryornot==0.4.4 + # via + # -r requirements/quality.txt + # cookiecutter +boto3==1.34.151 + # via + # -r requirements/quality.txt + # fs-s3fs +botocore==1.34.151 + # via + # -r requirements/quality.txt + # boto3 + # s3transfer +build==1.2.1 + # via + # -r requirements/pip-tools.txt + # pip-tools +cachetools==5.4.0 + # via + # -r requirements/ci.txt + # tox +certifi==2024.7.4 + # via + # -r requirements/quality.txt + # httpcore + # httpx + # requests +chardet==5.2.0 + # via + # -r requirements/ci.txt + # -r requirements/quality.txt + # binaryornot + # tox +charset-normalizer==3.3.2 + # via + # -r requirements/quality.txt + # requests +click==8.1.7 + # via + # -r requirements/pip-tools.txt + # -r requirements/quality.txt + # click-log + # code-annotations + # cookiecutter + # edx-lint + # litellm + # pip-tools +click-log==0.4.0 + # via + # -r requirements/quality.txt + # edx-lint +code-annotations==1.8.0 + # via + # -r requirements/quality.txt + # edx-lint +colorama==0.4.6 + # via + # -r requirements/ci.txt + # tox +cookiecutter==2.6.0 + # via + # -r requirements/quality.txt + # xblock-sdk +coverage[toml]==7.6.0 + # via + # -r requirements/quality.txt + # pytest-cov +ddt==1.7.2 + # via -r requirements/quality.txt +dill==0.3.8 + # via + # -r requirements/quality.txt + # pylint +distlib==0.3.8 + # via + # -r requirements/ci.txt + # virtualenv +distro==1.9.0 + # via + # -r requirements/quality.txt + # openai +django==4.2.14 + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # -r requirements/quality.txt + # django-appconf + # django-statici18n + # edx-i18n-tools + # openedx-django-pyfs + # xblock-sdk +django-appconf==1.0.6 + # via + # -r requirements/quality.txt + # django-statici18n +django-statici18n==2.5.0 + # via -r requirements/quality.txt +edx-i18n-tools==1.6.2 + # via -r requirements/quality.txt +edx-lint==5.3.7 + # via -r requirements/quality.txt +exceptiongroup==1.2.2 + # via + # -r requirements/quality.txt + # anyio + # pytest +filelock==3.15.4 + # via + # -r requirements/ci.txt + # -r requirements/quality.txt + # huggingface-hub + # tox + # virtualenv +frozenlist==1.4.1 + # via + # -r requirements/quality.txt + # aiohttp + # aiosignal +fs==2.4.16 + # via + # -r requirements/quality.txt + # fs-s3fs + # openedx-django-pyfs + # xblock +fs-s3fs==1.1.1 + # via + # -r requirements/quality.txt + # openedx-django-pyfs + # xblock-sdk +fsspec==2024.6.1 + # via + # -r requirements/quality.txt + # huggingface-hub +h11==0.14.0 + # via + # -r requirements/quality.txt + # httpcore +httpcore==1.0.5 + # via + # -r requirements/quality.txt + # httpx +httpx==0.27.0 + # via + # -r requirements/quality.txt + # openai +huggingface-hub==0.24.5 + # via + # -r requirements/quality.txt + # tokenizers +idna==3.7 + # via + # -r requirements/quality.txt + # anyio + # httpx + # requests + # yarl +importlib-metadata==6.11.0 + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # -r requirements/pip-tools.txt + # -r requirements/quality.txt + # build + # litellm +importlib-resources==6.4.0 + # via + # -r requirements/quality.txt + # jsonschema + # jsonschema-specifications +iniconfig==2.0.0 + # via + # -r requirements/quality.txt + # pytest +isort==5.13.2 + # via + # -r requirements/quality.txt + # pylint +jinja2==3.1.4 + # via + # -r requirements/quality.txt + # code-annotations + # cookiecutter + # litellm +jmespath==1.0.1 + # via + # -r requirements/quality.txt + # boto3 + # botocore +jsonschema==4.23.0 + # via + # -r requirements/quality.txt + # litellm +jsonschema-specifications==2023.12.1 + # via + # -r requirements/quality.txt + # jsonschema +lazy==1.6 + # via + # -r requirements/quality.txt + # xblock +litellm==1.42.6 + # via -r requirements/quality.txt +lxml==5.2.2 + # via + # -r requirements/quality.txt + # edx-i18n-tools + # xblock + # xblock-sdk +mako==1.3.5 + # via + # -r requirements/quality.txt + # xblock +markdown-it-py==3.0.0 + # via + # -r requirements/quality.txt + # rich +markupsafe==2.1.5 + # via + # -r requirements/quality.txt + # jinja2 + # mako + # xblock +mccabe==0.7.0 + # via + # -r requirements/quality.txt + # pylint +mdurl==0.1.2 + # via + # -r requirements/quality.txt + # markdown-it-py +mock==5.1.0 + # via -r requirements/quality.txt +multidict==6.0.5 + # via + # -r requirements/quality.txt + # aiohttp + # yarl +openai==1.37.1 + # via + # -r requirements/quality.txt + # litellm +openedx-django-pyfs==3.6.0 + # via + # -r requirements/quality.txt + # xblock +packaging==24.1 + # via + # -r requirements/ci.txt + # -r requirements/pip-tools.txt + # -r requirements/quality.txt + # build + # huggingface-hub + # pyproject-api + # pytest + # tox +path==16.16.0 + # via + # -r requirements/quality.txt + # edx-i18n-tools +pbr==6.0.0 + # via + # -r requirements/quality.txt + # stevedore +pip-tools==7.4.1 + # via -r requirements/pip-tools.txt +pkgutil-resolve-name==1.3.10 + # via + # -r requirements/quality.txt + # jsonschema +platformdirs==4.2.2 + # via + # -r requirements/ci.txt + # -r requirements/quality.txt + # pylint + # tox + # virtualenv +pluggy==1.5.0 + # via + # -r requirements/ci.txt + # -r requirements/quality.txt + # pytest + # tox +polib==1.2.0 + # via + # -r requirements/quality.txt + # edx-i18n-tools +pycodestyle==2.12.0 + # via -r requirements/quality.txt +pydantic==2.8.2 + # via + # -r requirements/quality.txt + # litellm + # openai +pydantic-core==2.20.1 + # via + # -r requirements/quality.txt + # pydantic +pygments==2.18.0 + # via + # -r requirements/quality.txt + # rich +pylint==3.2.6 + # via + # -r requirements/quality.txt + # edx-lint + # pylint-celery + # pylint-django + # pylint-plugin-utils +pylint-celery==0.3 + # via + # -r requirements/quality.txt + # edx-lint +pylint-django==2.5.5 + # via + # -r requirements/quality.txt + # edx-lint +pylint-plugin-utils==0.8.2 + # via + # -r requirements/quality.txt + # pylint-celery + # pylint-django +pypng==0.20220715.0 + # via + # -r requirements/quality.txt + # xblock-sdk +pyproject-api==1.7.1 + # via + # -r requirements/ci.txt + # tox +pyproject-hooks==1.1.0 + # via + # -r requirements/pip-tools.txt + # build + # pip-tools +pytest==8.3.2 + # via + # -r requirements/quality.txt + # pytest-cov + # pytest-django +pytest-cov==5.0.0 + # via -r requirements/quality.txt +pytest-django==4.8.0 + # via -r requirements/quality.txt +python-dateutil==2.9.0.post0 + # via + # -r requirements/quality.txt + # arrow + # botocore + # xblock +python-dotenv==1.0.1 + # via + # -r requirements/quality.txt + # litellm +python-slugify==8.0.4 + # via + # -r requirements/quality.txt + # code-annotations + # cookiecutter +pytz==2024.1 + # via + # -r requirements/quality.txt + # xblock +pyyaml==6.0.1 + # via + # -r requirements/quality.txt + # code-annotations + # cookiecutter + # edx-i18n-tools + # huggingface-hub + # xblock +referencing==0.35.1 + # via + # -r requirements/quality.txt + # jsonschema + # jsonschema-specifications +regex==2024.7.24 + # via + # -r requirements/quality.txt + # tiktoken +requests==2.32.3 + # via + # -r requirements/quality.txt + # cookiecutter + # huggingface-hub + # litellm + # tiktoken + # xblock-sdk +rich==13.7.1 + # via + # -r requirements/quality.txt + # cookiecutter +rpds-py==0.19.1 + # via + # -r requirements/quality.txt + # jsonschema + # referencing +s3transfer==0.10.2 + # via + # -r requirements/quality.txt + # boto3 +simplejson==3.19.2 + # via + # -r requirements/quality.txt + # xblock + # xblock-sdk +six==1.16.0 + # via + # -r requirements/quality.txt + # edx-lint + # fs + # fs-s3fs + # python-dateutil +sniffio==1.3.1 + # via + # -r requirements/quality.txt + # anyio + # httpx + # openai +sqlparse==0.5.1 + # via + # -r requirements/quality.txt + # django +stevedore==5.2.0 + # via + # -r requirements/quality.txt + # code-annotations +text-unidecode==1.3 + # via + # -r requirements/quality.txt + # python-slugify +tiktoken==0.7.0 + # via + # -r requirements/quality.txt + # litellm +tokenizers==0.19.1 + # via + # -r requirements/quality.txt + # litellm +tomli==2.0.1 + # via + # -r requirements/ci.txt + # -r requirements/pip-tools.txt + # -r requirements/quality.txt + # build + # coverage + # pip-tools + # pylint + # pyproject-api + # pytest + # tox +tomlkit==0.13.0 + # via + # -r requirements/quality.txt + # pylint +tox==4.16.0 + # via -r requirements/ci.txt +tqdm==4.66.4 + # via + # -r requirements/quality.txt + # huggingface-hub + # openai +types-python-dateutil==2.9.0.20240316 + # via + # -r requirements/quality.txt + # arrow +typing-extensions==4.12.2 + # via + # -r requirements/quality.txt + # annotated-types + # anyio + # asgiref + # astroid + # huggingface-hub + # openai + # pydantic + # pydantic-core + # pylint + # rich +urllib3==1.26.19 + # via + # -r requirements/quality.txt + # botocore + # requests +virtualenv==20.26.3 + # via + # -r requirements/ci.txt + # tox +web-fragments==2.2.0 + # via + # -r requirements/quality.txt + # xblock + # xblock-sdk +webob==1.8.7 + # via + # -r requirements/quality.txt + # xblock + # xblock-sdk +wheel==0.43.0 + # via + # -r requirements/pip-tools.txt + # pip-tools +xblock[django]==4.0.1 + # via + # -c requirements/constraints.txt + # -r requirements/quality.txt + # xblock-sdk +xblock-sdk==0.11.0 + # via -r requirements/quality.txt +yarl==1.9.4 + # via + # -r requirements/quality.txt + # aiohttp +zipp==3.19.2 + # via + # -r requirements/pip-tools.txt + # -r requirements/quality.txt + # importlib-metadata + # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements/pip-tools.in b/requirements/pip-tools.in new file mode 100644 index 0000000..0295d2c --- /dev/null +++ b/requirements/pip-tools.in @@ -0,0 +1,5 @@ +# Just the dependencies to run pip-tools, mainly for the "upgrade" make target + +-c constraints.txt + +pip-tools # Contains pip-compile, used to generate pip requirements files diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt new file mode 100644 index 0000000..3058830 --- /dev/null +++ b/requirements/pip-tools.txt @@ -0,0 +1,34 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +build==1.2.1 + # via pip-tools +click==8.1.7 + # via pip-tools +importlib-metadata==6.11.0 + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # build +packaging==24.1 + # via build +pip-tools==7.4.1 + # via -r requirements/pip-tools.in +pyproject-hooks==1.1.0 + # via + # build + # pip-tools +tomli==2.0.1 + # via + # build + # pip-tools +wheel==0.43.0 + # via pip-tools +zipp==3.19.2 + # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements/pip.in b/requirements/pip.in new file mode 100644 index 0000000..716c6f2 --- /dev/null +++ b/requirements/pip.in @@ -0,0 +1,6 @@ +# Core dependencies for installing other packages +-c constraints.txt + +pip +setuptools +wheel diff --git a/requirements/pip.txt b/requirements/pip.txt new file mode 100644 index 0000000..7b187b8 --- /dev/null +++ b/requirements/pip.txt @@ -0,0 +1,14 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +wheel==0.43.0 + # via -r requirements/pip.in + +# The following packages are considered to be unsafe in a requirements file: +pip==24.2 + # via -r requirements/pip.in +setuptools==72.1.0 + # via -r requirements/pip.in diff --git a/requirements/private.readme b/requirements/private.readme new file mode 100644 index 0000000..5600a10 --- /dev/null +++ b/requirements/private.readme @@ -0,0 +1,15 @@ +# If there are any Python packages you want to keep in your virtualenv beyond +# those listed in the official requirements files, create a "private.in" file +# and list them there. Generate the corresponding "private.txt" file pinning +# all of their indirect dependencies to specific versions as follows: + +# pip-compile private.in + +# This allows you to use "pip-sync" without removing these packages: + +# pip-sync requirements/*.txt + +# "private.in" and "private.txt" aren't checked into git to avoid merge +# conflicts, and the presence of this file allows "private.*" to be +# included in scripted pip-sync usage without requiring that those files be +# created first. diff --git a/requirements/quality.in b/requirements/quality.in new file mode 100644 index 0000000..76e2b39 --- /dev/null +++ b/requirements/quality.in @@ -0,0 +1,7 @@ +# Requirements for code quality checks +-c constraints.txt + +-r test.txt # Core and testing dependencies for this package + +edx-lint # edX pylint rules and plugins +pycodestyle # PEP 8 compliance validation diff --git a/requirements/quality.txt b/requirements/quality.txt new file mode 100644 index 0000000..a8f70cb --- /dev/null +++ b/requirements/quality.txt @@ -0,0 +1,490 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +aiohappyeyeballs==2.3.4 + # via + # -r requirements/test.txt + # aiohttp +aiohttp==3.10.0 + # via + # -r requirements/test.txt + # litellm +aiosignal==1.3.1 + # via + # -r requirements/test.txt + # aiohttp +annotated-types==0.7.0 + # via + # -r requirements/test.txt + # pydantic +anyio==4.4.0 + # via + # -r requirements/test.txt + # httpx + # openai +appdirs==1.4.4 + # via + # -r requirements/test.txt + # fs +arrow==1.3.0 + # via + # -r requirements/test.txt + # cookiecutter +asgiref==3.8.1 + # via + # -r requirements/test.txt + # django +astroid==3.2.4 + # via + # pylint + # pylint-celery +async-timeout==4.0.3 + # via + # -r requirements/test.txt + # aiohttp +attrs==23.2.0 + # via + # -r requirements/test.txt + # aiohttp + # jsonschema + # referencing +backports-zoneinfo==0.2.1 ; python_version < "3.9" + # via + # -c requirements/constraints.txt + # -r requirements/test.txt + # django +binaryornot==0.4.4 + # via + # -r requirements/test.txt + # cookiecutter +boto3==1.34.151 + # via + # -r requirements/test.txt + # fs-s3fs +botocore==1.34.151 + # via + # -r requirements/test.txt + # boto3 + # s3transfer +certifi==2024.7.4 + # via + # -r requirements/test.txt + # httpcore + # httpx + # requests +chardet==5.2.0 + # via + # -r requirements/test.txt + # binaryornot +charset-normalizer==3.3.2 + # via + # -r requirements/test.txt + # requests +click==8.1.7 + # via + # -r requirements/test.txt + # click-log + # code-annotations + # cookiecutter + # edx-lint + # litellm +click-log==0.4.0 + # via edx-lint +code-annotations==1.8.0 + # via edx-lint +cookiecutter==2.6.0 + # via + # -r requirements/test.txt + # xblock-sdk +coverage[toml]==7.6.0 + # via + # -r requirements/test.txt + # pytest-cov +ddt==1.7.2 + # via -r requirements/test.txt +dill==0.3.8 + # via pylint +distro==1.9.0 + # via + # -r requirements/test.txt + # openai +django==4.2.14 + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # -r requirements/test.txt + # django-appconf + # django-statici18n + # edx-i18n-tools + # openedx-django-pyfs + # xblock-sdk +django-appconf==1.0.6 + # via + # -r requirements/test.txt + # django-statici18n +django-statici18n==2.5.0 + # via -r requirements/test.txt +edx-i18n-tools==1.6.2 + # via -r requirements/test.txt +edx-lint==5.3.7 + # via -r requirements/quality.in +exceptiongroup==1.2.2 + # via + # -r requirements/test.txt + # anyio + # pytest +filelock==3.15.4 + # via + # -r requirements/test.txt + # huggingface-hub +frozenlist==1.4.1 + # via + # -r requirements/test.txt + # aiohttp + # aiosignal +fs==2.4.16 + # via + # -r requirements/test.txt + # fs-s3fs + # openedx-django-pyfs + # xblock +fs-s3fs==1.1.1 + # via + # -r requirements/test.txt + # openedx-django-pyfs + # xblock-sdk +fsspec==2024.6.1 + # via + # -r requirements/test.txt + # huggingface-hub +h11==0.14.0 + # via + # -r requirements/test.txt + # httpcore +httpcore==1.0.5 + # via + # -r requirements/test.txt + # httpx +httpx==0.27.0 + # via + # -r requirements/test.txt + # openai +huggingface-hub==0.24.5 + # via + # -r requirements/test.txt + # tokenizers +idna==3.7 + # via + # -r requirements/test.txt + # anyio + # httpx + # requests + # yarl +importlib-metadata==6.11.0 + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # -r requirements/test.txt + # litellm +importlib-resources==6.4.0 + # via + # -r requirements/test.txt + # jsonschema + # jsonschema-specifications +iniconfig==2.0.0 + # via + # -r requirements/test.txt + # pytest +isort==5.13.2 + # via pylint +jinja2==3.1.4 + # via + # -r requirements/test.txt + # code-annotations + # cookiecutter + # litellm +jmespath==1.0.1 + # via + # -r requirements/test.txt + # boto3 + # botocore +jsonschema==4.23.0 + # via + # -r requirements/test.txt + # litellm +jsonschema-specifications==2023.12.1 + # via + # -r requirements/test.txt + # jsonschema +lazy==1.6 + # via + # -r requirements/test.txt + # xblock +litellm==1.42.6 + # via -r requirements/test.txt +lxml==5.2.2 + # via + # -r requirements/test.txt + # edx-i18n-tools + # xblock + # xblock-sdk +mako==1.3.5 + # via + # -r requirements/test.txt + # xblock +markdown-it-py==3.0.0 + # via + # -r requirements/test.txt + # rich +markupsafe==2.1.5 + # via + # -r requirements/test.txt + # jinja2 + # mako + # xblock +mccabe==0.7.0 + # via pylint +mdurl==0.1.2 + # via + # -r requirements/test.txt + # markdown-it-py +mock==5.1.0 + # via -r requirements/test.txt +multidict==6.0.5 + # via + # -r requirements/test.txt + # aiohttp + # yarl +openai==1.37.1 + # via + # -r requirements/test.txt + # litellm +openedx-django-pyfs==3.6.0 + # via + # -r requirements/test.txt + # xblock +packaging==24.1 + # via + # -r requirements/test.txt + # huggingface-hub + # pytest +path==16.16.0 + # via + # -r requirements/test.txt + # edx-i18n-tools +pbr==6.0.0 + # via stevedore +pkgutil-resolve-name==1.3.10 + # via + # -r requirements/test.txt + # jsonschema +platformdirs==4.2.2 + # via pylint +pluggy==1.5.0 + # via + # -r requirements/test.txt + # pytest +polib==1.2.0 + # via + # -r requirements/test.txt + # edx-i18n-tools +pycodestyle==2.12.0 + # via -r requirements/quality.in +pydantic==2.8.2 + # via + # -r requirements/test.txt + # litellm + # openai +pydantic-core==2.20.1 + # via + # -r requirements/test.txt + # pydantic +pygments==2.18.0 + # via + # -r requirements/test.txt + # rich +pylint==3.2.6 + # via + # edx-lint + # pylint-celery + # pylint-django + # pylint-plugin-utils +pylint-celery==0.3 + # via edx-lint +pylint-django==2.5.5 + # via edx-lint +pylint-plugin-utils==0.8.2 + # via + # pylint-celery + # pylint-django +pypng==0.20220715.0 + # via + # -r requirements/test.txt + # xblock-sdk +pytest==8.3.2 + # via + # -r requirements/test.txt + # pytest-cov + # pytest-django +pytest-cov==5.0.0 + # via -r requirements/test.txt +pytest-django==4.8.0 + # via -r requirements/test.txt +python-dateutil==2.9.0.post0 + # via + # -r requirements/test.txt + # arrow + # botocore + # xblock +python-dotenv==1.0.1 + # via + # -r requirements/test.txt + # litellm +python-slugify==8.0.4 + # via + # -r requirements/test.txt + # code-annotations + # cookiecutter +pytz==2024.1 + # via + # -r requirements/test.txt + # xblock +pyyaml==6.0.1 + # via + # -r requirements/test.txt + # code-annotations + # cookiecutter + # edx-i18n-tools + # huggingface-hub + # xblock +referencing==0.35.1 + # via + # -r requirements/test.txt + # jsonschema + # jsonschema-specifications +regex==2024.7.24 + # via + # -r requirements/test.txt + # tiktoken +requests==2.32.3 + # via + # -r requirements/test.txt + # cookiecutter + # huggingface-hub + # litellm + # tiktoken + # xblock-sdk +rich==13.7.1 + # via + # -r requirements/test.txt + # cookiecutter +rpds-py==0.19.1 + # via + # -r requirements/test.txt + # jsonschema + # referencing +s3transfer==0.10.2 + # via + # -r requirements/test.txt + # boto3 +simplejson==3.19.2 + # via + # -r requirements/test.txt + # xblock + # xblock-sdk +six==1.16.0 + # via + # -r requirements/test.txt + # edx-lint + # fs + # fs-s3fs + # python-dateutil +sniffio==1.3.1 + # via + # -r requirements/test.txt + # anyio + # httpx + # openai +sqlparse==0.5.1 + # via + # -r requirements/test.txt + # django +stevedore==5.2.0 + # via code-annotations +text-unidecode==1.3 + # via + # -r requirements/test.txt + # python-slugify +tiktoken==0.7.0 + # via + # -r requirements/test.txt + # litellm +tokenizers==0.19.1 + # via + # -r requirements/test.txt + # litellm +tomli==2.0.1 + # via + # -r requirements/test.txt + # coverage + # pylint + # pytest +tomlkit==0.13.0 + # via pylint +tqdm==4.66.4 + # via + # -r requirements/test.txt + # huggingface-hub + # openai +types-python-dateutil==2.9.0.20240316 + # via + # -r requirements/test.txt + # arrow +typing-extensions==4.12.2 + # via + # -r requirements/test.txt + # annotated-types + # anyio + # asgiref + # astroid + # huggingface-hub + # openai + # pydantic + # pydantic-core + # pylint + # rich +urllib3==1.26.19 + # via + # -r requirements/test.txt + # botocore + # requests +web-fragments==2.2.0 + # via + # -r requirements/test.txt + # xblock + # xblock-sdk +webob==1.8.7 + # via + # -r requirements/test.txt + # xblock + # xblock-sdk +xblock[django]==4.0.1 + # via + # -c requirements/constraints.txt + # -r requirements/test.txt + # xblock-sdk +xblock-sdk==0.11.0 + # via -r requirements/test.txt +yarl==1.9.4 + # via + # -r requirements/test.txt + # aiohttp +zipp==3.19.2 + # via + # -r requirements/test.txt + # importlib-metadata + # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/test.in b/requirements/test.in new file mode 100644 index 0000000..d5bb585 --- /dev/null +++ b/requirements/test.in @@ -0,0 +1,15 @@ +# Requirements for test runs. +-c constraints.txt + +-r base.txt # Core dependencies for this package + +pytest-cov # pytest extension for code coverage statistics +pytest-django # pytest extension for better Django support +ddt # data-driven tests + +mock # required by the workbench +openedx-django-pyfs # required by the workbench + +edx-i18n-tools # For i18n_tool dummy + +xblock-sdk>0.7 # workbench diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 0000000..39a089c --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,404 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# make upgrade +# +aiohappyeyeballs==2.3.4 + # via + # -r requirements/base.txt + # aiohttp +aiohttp==3.10.0 + # via + # -r requirements/base.txt + # litellm +aiosignal==1.3.1 + # via + # -r requirements/base.txt + # aiohttp +annotated-types==0.7.0 + # via + # -r requirements/base.txt + # pydantic +anyio==4.4.0 + # via + # -r requirements/base.txt + # httpx + # openai +appdirs==1.4.4 + # via + # -r requirements/base.txt + # fs +arrow==1.3.0 + # via cookiecutter +asgiref==3.8.1 + # via + # -r requirements/base.txt + # django +async-timeout==4.0.3 + # via + # -r requirements/base.txt + # aiohttp +attrs==23.2.0 + # via + # -r requirements/base.txt + # aiohttp + # jsonschema + # referencing +backports-zoneinfo==0.2.1 ; python_version < "3.9" + # via + # -c requirements/constraints.txt + # -r requirements/base.txt + # django +binaryornot==0.4.4 + # via cookiecutter +boto3==1.34.151 + # via + # -r requirements/base.txt + # fs-s3fs +botocore==1.34.151 + # via + # -r requirements/base.txt + # boto3 + # s3transfer +certifi==2024.7.4 + # via + # -r requirements/base.txt + # httpcore + # httpx + # requests +chardet==5.2.0 + # via binaryornot +charset-normalizer==3.3.2 + # via + # -r requirements/base.txt + # requests +click==8.1.7 + # via + # -r requirements/base.txt + # cookiecutter + # litellm +cookiecutter==2.6.0 + # via xblock-sdk +coverage[toml]==7.6.0 + # via pytest-cov +ddt==1.7.2 + # via -r requirements/test.in +distro==1.9.0 + # via + # -r requirements/base.txt + # openai + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # -r requirements/base.txt + # django-appconf + # django-statici18n + # edx-i18n-tools + # openedx-django-pyfs + # xblock-sdk +django-appconf==1.0.6 + # via + # -r requirements/base.txt + # django-statici18n +django-statici18n==2.5.0 + # via -r requirements/base.txt +edx-i18n-tools==1.6.2 + # via -r requirements/test.in +exceptiongroup==1.2.2 + # via + # -r requirements/base.txt + # anyio + # pytest +filelock==3.15.4 + # via + # -r requirements/base.txt + # huggingface-hub +frozenlist==1.4.1 + # via + # -r requirements/base.txt + # aiohttp + # aiosignal +fs==2.4.16 + # via + # -r requirements/base.txt + # fs-s3fs + # openedx-django-pyfs + # xblock +fs-s3fs==1.1.1 + # via + # -r requirements/base.txt + # openedx-django-pyfs + # xblock-sdk +fsspec==2024.6.1 + # via + # -r requirements/base.txt + # huggingface-hub +h11==0.14.0 + # via + # -r requirements/base.txt + # httpcore +httpcore==1.0.5 + # via + # -r requirements/base.txt + # httpx +httpx==0.27.0 + # via + # -r requirements/base.txt + # openai +huggingface-hub==0.24.5 + # via + # -r requirements/base.txt + # tokenizers +idna==3.7 + # via + # -r requirements/base.txt + # anyio + # httpx + # requests + # yarl +importlib-metadata==6.11.0 + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # -r requirements/base.txt + # litellm +importlib-resources==6.4.0 + # via + # -r requirements/base.txt + # jsonschema + # jsonschema-specifications +iniconfig==2.0.0 + # via pytest +jinja2==3.1.4 + # via + # -r requirements/base.txt + # cookiecutter + # litellm +jmespath==1.0.1 + # via + # -r requirements/base.txt + # boto3 + # botocore +jsonschema==4.23.0 + # via + # -r requirements/base.txt + # litellm +jsonschema-specifications==2023.12.1 + # via + # -r requirements/base.txt + # jsonschema +lazy==1.6 + # via + # -r requirements/base.txt + # xblock +litellm==1.42.6 + # via -r requirements/base.txt +lxml==5.2.2 + # via + # -r requirements/base.txt + # edx-i18n-tools + # xblock + # xblock-sdk +mako==1.3.5 + # via + # -r requirements/base.txt + # xblock +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.5 + # via + # -r requirements/base.txt + # jinja2 + # mako + # xblock +mdurl==0.1.2 + # via markdown-it-py +mock==5.1.0 + # via -r requirements/test.in +multidict==6.0.5 + # via + # -r requirements/base.txt + # aiohttp + # yarl +openai==1.37.1 + # via + # -r requirements/base.txt + # litellm +openedx-django-pyfs==3.6.0 + # via + # -r requirements/base.txt + # -r requirements/test.in + # xblock +packaging==24.1 + # via + # -r requirements/base.txt + # huggingface-hub + # pytest +path==16.16.0 + # via edx-i18n-tools +pkgutil-resolve-name==1.3.10 + # via + # -r requirements/base.txt + # jsonschema +pluggy==1.5.0 + # via pytest +polib==1.2.0 + # via edx-i18n-tools +pydantic==2.8.2 + # via + # -r requirements/base.txt + # litellm + # openai +pydantic-core==2.20.1 + # via + # -r requirements/base.txt + # pydantic +pygments==2.18.0 + # via rich +pypng==0.20220715.0 + # via xblock-sdk +pytest==8.3.2 + # via + # pytest-cov + # pytest-django +pytest-cov==5.0.0 + # via -r requirements/test.in +pytest-django==4.8.0 + # via -r requirements/test.in +python-dateutil==2.9.0.post0 + # via + # -r requirements/base.txt + # arrow + # botocore + # xblock +python-dotenv==1.0.1 + # via + # -r requirements/base.txt + # litellm +python-slugify==8.0.4 + # via cookiecutter +pytz==2024.1 + # via + # -r requirements/base.txt + # xblock +pyyaml==6.0.1 + # via + # -r requirements/base.txt + # cookiecutter + # edx-i18n-tools + # huggingface-hub + # xblock +referencing==0.35.1 + # via + # -r requirements/base.txt + # jsonschema + # jsonschema-specifications +regex==2024.7.24 + # via + # -r requirements/base.txt + # tiktoken +requests==2.32.3 + # via + # -r requirements/base.txt + # cookiecutter + # huggingface-hub + # litellm + # tiktoken + # xblock-sdk +rich==13.7.1 + # via cookiecutter +rpds-py==0.19.1 + # via + # -r requirements/base.txt + # jsonschema + # referencing +s3transfer==0.10.2 + # via + # -r requirements/base.txt + # boto3 +simplejson==3.19.2 + # via + # -r requirements/base.txt + # xblock + # xblock-sdk +six==1.16.0 + # via + # -r requirements/base.txt + # fs + # fs-s3fs + # python-dateutil +sniffio==1.3.1 + # via + # -r requirements/base.txt + # anyio + # httpx + # openai +sqlparse==0.5.1 + # via + # -r requirements/base.txt + # django +text-unidecode==1.3 + # via python-slugify +tiktoken==0.7.0 + # via + # -r requirements/base.txt + # litellm +tokenizers==0.19.1 + # via + # -r requirements/base.txt + # litellm +tomli==2.0.1 + # via + # coverage + # pytest +tqdm==4.66.4 + # via + # -r requirements/base.txt + # huggingface-hub + # openai +types-python-dateutil==2.9.0.20240316 + # via arrow +typing-extensions==4.12.2 + # via + # -r requirements/base.txt + # annotated-types + # anyio + # asgiref + # huggingface-hub + # openai + # pydantic + # pydantic-core + # rich +urllib3==1.26.19 + # via + # -r requirements/base.txt + # botocore + # requests +web-fragments==2.2.0 + # via + # -r requirements/base.txt + # xblock + # xblock-sdk +webob==1.8.7 + # via + # -r requirements/base.txt + # xblock + # xblock-sdk +xblock[django]==4.0.1 + # via + # -c requirements/constraints.txt + # -r requirements/base.txt + # xblock-sdk +xblock-sdk==0.11.0 + # via -r requirements/test.in +yarl==1.9.4 + # via + # -r requirements/base.txt + # aiohttp +zipp==3.19.2 + # via + # -r requirements/base.txt + # importlib-metadata + # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/workbench.txt b/requirements/workbench.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5254944 --- /dev/null +++ b/setup.py @@ -0,0 +1,43 @@ +"""Setup for shortanswer XBlock.""" + +import os + +from setuptools import setup + + +def package_data(pkg, roots): + """Generic function to find package_data. + + All of the files under each of the `roots` will be declared as package + data for package `pkg`. + + """ + data = [] + for root in roots: + for dirname, _, files in os.walk(os.path.join(pkg, root)): + for fname in files: + data.append(os.path.relpath(os.path.join(dirname, fname), pkg)) + + return {pkg: data} + + +setup( + name="xblock-ai-eval", + version="0.1", + description="XBlocks to write short text and code entries with AI-driven evaluation", + license="AGPL v3", + packages=[ + "ai_eval", + ], + install_requires=[ + "XBlock", + "litellm>=1.42", + ], + entry_points={ + "xblock.v1": [ + "shortanswer_ai_eval = ai_eval:ShortAnswerAIEvalXBlock", + "coding_ai_eval = ai_eval:CodingAIEvalXBlock", + ] + }, + package_data=package_data("ai_eval", ["static", "public", "templates"]), +)