diff --git a/.env.example b/.env.example index 26dd0e3..9345342 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ # FastAPI IS_DEBUG=False -API_KEY=exampe_key +API_KEY=example_key DEFAULT_MODEL_PATH=./ml_model/models # Hugging FaceModel QUESTION_ANSWER_MODEL=deepset/roberta-base-squad2 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3678db8..619b502 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.8.2 ENV PYTHONUNBUFFERED 1 -EXPOSE 80 +EXPOSE 8000 WORKDIR /app COPY requirements.txt ./ @@ -12,4 +12,4 @@ RUN pip install --upgrade pip && \ COPY . ./ ENV PYTHONPATH huggingfastapi -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] \ No newline at end of file +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/README.md b/README.md index e7615d5..7440322 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,25 @@ Project structure for development and production. Installation and setup instructions to run the development mode model and serve a local RESTful API endpoint. +## Project structure + +Files related to application are in the `huggingfastapi` or `tests` directories. +Application parts are: + + huggingfastapi + ├── api - Main API. + │   └── routes - Web routes. + ├── core - Application configuration, startup events, logging. + ├── models - Pydantic models for api. + ├── services - NLP logics. + └── main.py - FastAPI application creation and configuration. + │ + tests - Codes without test are is an illusion + + ## Requirements -Python 3.6+ +Python 3.7+ ## Installation Install the required packages in your local environment (ideally virtualenv, conda, etc.). @@ -25,22 +41,23 @@ source venv/bin/activate make install ``` -## Runnning Localhost - -`make run` - -## Deploy app +#### Runnning Localhost -`make deploy` - -## Running Tests +```sh +make run +``` -`make test` +#### Deploy app -## Runnning Easter Egg +```sh +make deploy +``` -`make easter` +#### Running Tests +```sh +make test +``` ## Setup 1. Duplicate the `.env.example` file and rename it to `.env` @@ -48,13 +65,13 @@ make install 2. In the `.env` file configure the `API_KEY` entry. The key is used for authenticating our API.
Execute script to generate .env, and replace `example_key`with the UUID generated: + ```bash make generate_dot_env python -c "import uuid;print(str(uuid.uuid4()))" ``` - ## Run It 1. Start your app with: @@ -81,19 +98,7 @@ tox This runs tests and coverage for Python 3.8 and Flake8, Bandit. -## Project structure - -Files related to application are in the `huggingfastapi` or `tests` directories. -Application parts are: - - huggingfastapi - ├── huggingfastapi - web related stuff. - │   └── routes - web routes. - ├── core - application configuration, startup events, logging. - ├── models - pydantic models for api. - ├── services - logic that is not just crud related. - └── main.py - FastAPI application creation and configuration. - │ - tests - pytest - \ No newline at end of file +# TODO +[ ] Change make to invoke +[ ] Add endpoint for uploading text file and questions \ No newline at end of file diff --git a/huggingfastapi/services/ml_downloader.py b/huggingfastapi/services/ml_downloader.py deleted file mode 100644 index 4799d2a..0000000 --- a/huggingfastapi/services/ml_downloader.py +++ /dev/null @@ -1,47 +0,0 @@ -''' -Directly download Hugging Face Model to a local directory -''' -import sys - -def downloader(): - from transformers import AutoTokenizer - from transformers import AutoModelForQuestionAnswering - from transformers import pipeline - - from huggingfastapi.services.utils import ModelLoader - from huggingfastapi.core.config import ( - DEFAULT_MODEL_PATH, - QUESTION_ANSWER_MODEL - ) - - try: - # Question Answer Model - print(f"[+] Loading Question Answer Model: {QUESTION_ANSWER_MODEL}") - qa_tokenizer, qa_model = ModelLoader( - model_name=QUESTION_ANSWER_MODEL, - model_directory=DEFAULT_MODEL_PATH, - tokenizer_loader=AutoTokenizer, - model_loader=AutoModelForQuestionAnswering, - ).retrieve() - except: # Any Error Exit - sys.exit('Could not to download or load models!') - - # Test Loading and the Results - context = "This is a tale of a tiny snail and a great big grey blue humpback whale" - questions = ["What is the color of the humpback whale?", "What kind of a whale is it?"] - - nlp = pipeline("question-answering", model=qa_model, tokenizer=qa_tokenizer) - - print(f"\nTesting Question & Answers Using {QUESTION_ANSWER_MODEL}\n") - for question in questions: - QA_input = {"question": question, "context": context} - response = nlp(QA_input) - print(f"Question: {question}") - print(f"Answer : {response.get('answer')}") - print(f"Details: {response}\n") - - return 0 - -if __name__ == "__main__": - sys.exit(download()) - diff --git a/huggingfastapi/services/nlp.py b/huggingfastapi/services/nlp.py index 9b9f67f..e741f88 100644 --- a/huggingfastapi/services/nlp.py +++ b/huggingfastapi/services/nlp.py @@ -41,7 +41,7 @@ def _pre_process(self, payload: QAPredictionPayload) -> List: def _post_process(self, prediction: Dict) -> QAPredictionResult: logger.debug("Post-processing prediction.") - qa = QuestionPredictionResult(**prediction) + qa = QAPredictionResult(**prediction) return qa diff --git a/ml_model/model_description.md b/ml_model/model_description.md index ae6e696..01602cf 100644 --- a/ml_model/model_description.md +++ b/ml_model/model_description.md @@ -1,5 +1,15 @@ -## Model Description +## NLP Model Description -### Generate Questions or Answers given Text +Model: https://huggingface.co/deepset/roberta-base-squad2 + +## Authors +Branden Chan: branden.chan [at] deepset.ai Timo Möller: timo.moeller [at] deepset.ai Malte Pietsch: malte.pietsch [at] deepset.ai Tanay Soni: tanay.soni [at] deepset.ai + +## Settings +Language model: roberta-base +Language: English +Downstream-task: Extractive QA +Training data: SQuAD 2.0 +Eval data: SQuAD 2.0 diff --git a/tests/conftest.py b/tests/conftest.py index c59185e..d1bc322 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,8 +3,7 @@ from starlette.testclient import TestClient -environ["API_KEY"] = "72c8e5a5-bb07-4c35-8115-f0c4c60eb790" -environ["DEFAULT_MODEL_PATH"] = "./ml_model/model.pkl" +environ["API_KEY"] = "example_key" from huggingfastapi.main import get_app # noqa: E402 diff --git a/tests/test_api/test_api_auth.py b/tests/test_api/test_api_auth.py index d1ab314..904e112 100644 --- a/tests/test_api/test_api_auth.py +++ b/tests/test_api/test_api_auth.py @@ -2,14 +2,16 @@ def test_auth_using_prediction_api_no_apikey_header(test_client) -> None: - response = test_client.post("/api/model/predict") + response = test_client.post("/api/v1/question") assert response.status_code == 400 assert response.json() == {"detail": messages.NO_API_KEY} def test_auth_using_prediction_api_wrong_apikey_header(test_client) -> None: response = test_client.post( - "/api/model/predict", json={"image": "test"}, headers={"token": "WRONG_TOKEN"} + "/api/v1/question", + json={"context": "test", "question": "test"}, + headers={"token": "WRONG_TOKEN"}, ) assert response.status_code == 401 assert response.json() == {"detail": messages.AUTH_REQ} diff --git a/tests/test_api/test_prediction.py b/tests/test_api/test_prediction.py index 202457d..c4fe30b 100644 --- a/tests/test_api/test_prediction.py +++ b/tests/test_api/test_prediction.py @@ -3,16 +3,22 @@ def test_prediction(test_client) -> None: response = test_client.post( - "/api/model/predict", - json={"text": "hello world!", "model_version": "0.1.0"}, - headers={"token": str(config.API_KEY)}, + "/api/v1/question", + json={"context": "two plus two equal four", "question": "What is four?"}, + headers={"token": "example_key"}, ) assert response.status_code == 200 - assert "model_version" in response.json() + assert "score" in response.json() def test_prediction_nopayload(test_client) -> None: response = test_client.post( - "/api/model/predict", json={}, headers={"token": str(config.API_KEY)} + "/api/v1/question", json={}, headers={"token": "example_key"} ) - assert response.status_code == 422 + ## if nopayload, default Hitchhiker's Guide to the Galaxy example is sent + # context:"42 is the answer to life, the universe and everything." + # question:"What is the answer to life?" + # if no default, assert response.status_code == 422 + + data = response.json() + assert data["answer"] == "42" diff --git a/tests/test_service/test_models.py b/tests/test_service/test_models.py index 2971771..c6f4c92 100644 --- a/tests/test_service/test_models.py +++ b/tests/test_service/test_models.py @@ -3,18 +3,19 @@ from huggingfastapi.core import config from huggingfastapi.models.payload import QAPredictionPayload from huggingfastapi.models.prediction import QAPredictionResult -from huggingfastapi.services.models import QAModel +from huggingfastapi.services.nlp import QAModel def test_prediction(test_client) -> None: model_path = config.DEFAULT_MODEL_PATH - tpp = QAPredictionPayload.parse_obj( - {"text": "hello world!", "model_version": "0.1.0"} + qa = QAPredictionPayload.parse_obj( + {"context": "two plus two equal four", "question": "What is four?"} ) tm = QAModel(model_path) - result = tm.predict(tpp) + result = tm.predict(qa) assert isinstance(result, QAPredictionResult) + assert result.answer == "two plus two" def test_prediction_no_payload(test_client) -> None: