diff --git a/.vscode/settings.json b/.vscode/settings.json index 03a850a..4e5b3c7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "boto", "botocore", + "plotly", "pydantic", "pyplot", "sklearn", diff --git a/example/config.py b/example/config.py index 143bb15..88cc87f 100644 --- a/example/config.py +++ b/example/config.py @@ -1 +1 @@ -model_key = "domain-prediction-v2" +model_key = "small-domain-prediction-v2" diff --git a/example/dash-test.py b/example/dash-test.py new file mode 100644 index 0000000..e69de29 diff --git a/good_ai/src/good_ai/good_ai/config.py b/good_ai/src/good_ai/good_ai/config.py index 9e501dc..85e4be0 100644 --- a/good_ai/src/good_ai/good_ai/config.py +++ b/good_ai/src/good_ai/good_ai/config.py @@ -1,14 +1,13 @@ -import logging -import os +METRICS_PATH = "/metrics" -logger = logging.getLogger("good_ai") +# logger = logging.getLogger("good_ai") -PRODUCTION_KEY = "production" +# PRODUCTION_KEY = "production" -environment = os.environ.get("ENVIRONMENT", PRODUCTION_KEY).lower() -is_production = environment == PRODUCTION_KEY +# environment = os.environ.get("ENVIRONMENT", PRODUCTION_KEY).lower() +# is_production = environment == PRODUCTION_KEY -if is_production: - logger.info("Running in production mode ✅") -else: - logger.warn("Running in development mode ‼️") +# if is_production: +# logger.info("Running in production mode ✅") +# else: +# logger.warn("Running in development mode ‼️") diff --git a/good_ai/src/good_ai/good_ai/deploy/create_fastapi_app.py b/good_ai/src/good_ai/good_ai/deploy/create_fastapi_app.py new file mode 100644 index 0000000..0767d16 --- /dev/null +++ b/good_ai/src/good_ai/good_ai/deploy/create_fastapi_app.py @@ -0,0 +1,35 @@ +from fastapi import FastAPI, status +from fastapi.middleware.wsgi import WSGIMiddleware +from fastapi.openapi.docs import get_swagger_ui_html +from fastapi.responses import RedirectResponse +from starlette.responses import HTMLResponse + +from ..config import METRICS_PATH +from ..helper import snake_case_to_text +from ..metrics import create_dash_app +from ..views import HealthCheckResponse + + +def create_fastapi_app(function_name: str) -> FastAPI: + app = FastAPI( + title=snake_case_to_text(function_name), + description=f"REST API wrapper for interacting with the '{function_name}' function.", + docs_url=None, + redoc_url=None, + ) + + @app.get("/", include_in_schema=False) + def redirect_to_entrypoint() -> RedirectResponse: + return RedirectResponse("/metrics") + + app.mount(METRICS_PATH, WSGIMiddleware(create_dash_app(function_name))) + + @app.get("/docs", include_in_schema=False) + def custom_swagger_ui_html() -> HTMLResponse: + return get_swagger_ui_html(openapi_url="openapi.json", title=app.title) + + @app.get("/health", status_code=status.HTTP_200_OK) + def check_health() -> HealthCheckResponse: + return HealthCheckResponse(is_healthy=True) + + return app diff --git a/good_ai/src/good_ai/good_ai/deploy/serve.py b/good_ai/src/good_ai/good_ai/deploy/serve.py index c1a7ebf..ddfee06 100644 --- a/good_ai/src/good_ai/good_ai/deploy/serve.py +++ b/good_ai/src/good_ai/good_ai/deploy/serve.py @@ -1,15 +1,14 @@ from typing import Any, Callable import uvicorn -from fastapi import FastAPI, status -from fastapi.openapi.docs import get_swagger_ui_html -from fastapi.responses import RedirectResponse -from starlette.responses import HTMLResponse +from fastapi import status from typing_extensions import Never +from good_ai.good_ai.deploy.create_fastapi_app import create_fastapi_app + from ..set_default_config import set_default_config_if_uninitialized from ..tracing import TracingContext -from ..views import HealthCheckResponse, Trace +from ..views import Trace def serve( @@ -17,24 +16,7 @@ def serve( ) -> Never: set_default_config_if_uninitialized() - app = FastAPI( - title=function.__name__.capitalize().replace("_", " "), - description=f"REST API wrapper for interacting with the {function.__name__} function.", - docs_url=None, - redoc_url=None, - ) - - @app.get("/", include_in_schema=False) - def redirect_to_docs() -> RedirectResponse: - return RedirectResponse("/docs") - - @app.get("/docs", include_in_schema=False) - def custom_swagger_ui_html() -> HTMLResponse: - return get_swagger_ui_html(openapi_url="openapi.json", title=app.title) - - @app.get("/health", status_code=status.HTTP_200_OK) - def check_health() -> HealthCheckResponse: - return HealthCheckResponse(is_healthy=True) + app = create_fastapi_app(function.__name__) @app.post("/score", status_code=status.HTTP_200_OK, response_model=Trace) def process(input: Any) -> Trace: diff --git a/good_ai/src/good_ai/good_ai/helper/__init__.py b/good_ai/src/good_ai/good_ai/helper/__init__.py new file mode 100644 index 0000000..4fdd18c --- /dev/null +++ b/good_ai/src/good_ai/good_ai/helper/__init__.py @@ -0,0 +1 @@ +from .snake_case_to_text import snake_case_to_text diff --git a/good_ai/src/good_ai/good_ai/helper/snake_case_to_text.py b/good_ai/src/good_ai/good_ai/helper/snake_case_to_text.py new file mode 100644 index 0000000..1ba7835 --- /dev/null +++ b/good_ai/src/good_ai/good_ai/helper/snake_case_to_text.py @@ -0,0 +1,2 @@ +def snake_case_to_text(snake_case: str) -> str: + return snake_case.capitalize().replace("_", " ") diff --git a/good_ai/src/good_ai/good_ai/metrics/__init__.py b/good_ai/src/good_ai/good_ai/metrics/__init__.py new file mode 100644 index 0000000..dfa9e7d --- /dev/null +++ b/good_ai/src/good_ai/good_ai/metrics/__init__.py @@ -0,0 +1 @@ +from .create_dash_app import create_dash_app diff --git a/good_ai/src/good_ai/good_ai/metrics/create_dash_app.py b/good_ai/src/good_ai/good_ai/metrics/create_dash_app.py new file mode 100644 index 0000000..a261888 --- /dev/null +++ b/good_ai/src/good_ai/good_ai/metrics/create_dash_app.py @@ -0,0 +1,48 @@ +import pandas as pd +import plotly.express as px +from dash import Dash, dcc, html +from flask import Flask + +from ..config import METRICS_PATH +from ..helper import snake_case_to_text + + +def create_dash_app(function_name: str) -> Flask: + app = Dash(function_name, requests_pathname_prefix=METRICS_PATH + "/") + + markdown_text = f""" + # {snake_case_to_text(function_name)} - metrics + > A human-friendly framework for robust end-to-end AI deployments + + ## Using the API + + You can find the available endpoints at [/docs](/docs). + + ## Metrics + + The recent traces and aggregated metrics are presented below. + """ + + df = pd.read_csv( + "https://gist.githubusercontent.com/chriddyp/5d1ea79569ed194d432e56108a04d188/raw/a9f9e8076b837d541398e999dcbac2b2826a81f8/gdp-life-exp-2007.csv" + ) + + fig = px.scatter( + df, + x="gdp per capita", + y="life expectancy", + size="population", + color="continent", + hover_name="country", + log_x=True, + size_max=60, + ) + + app.layout = html.Div( + [ + dcc.Markdown(children=markdown_text), + dcc.Graph(id="life-exp-vs-gdp", figure=fig), + ] + ) + + return app.server diff --git a/good_ai/src/good_ai/good_ai/views/trace.py b/good_ai/src/good_ai/good_ai/views/trace.py index 25809ea..2d0c988 100644 --- a/good_ai/src/good_ai/good_ai/views/trace.py +++ b/good_ai/src/good_ai/good_ai/views/trace.py @@ -1,4 +1,5 @@ from typing import Any, List +from uuid import uuid4 from pydantic import BaseModel @@ -6,6 +7,7 @@ from .model import Model class Trace(BaseModel): + id = str(uuid4()) created: str execution_time_ms: float input: Any diff --git a/good_ai/src/good_ai/open_s3/large_file.py b/good_ai/src/good_ai/open_s3/large_file.py index a742487..137ffa4 100644 --- a/good_ai/src/good_ai/open_s3/large_file.py +++ b/good_ai/src/good_ai/open_s3/large_file.py @@ -218,24 +218,26 @@ class LargeFile: copy(str(path), str(Path(tmp) / self._local_name)) - logger.info(f"Compressing {self._local_name}") - shutil.make_archive( - str(Path(tmp) / self._local_name), - "gztar", - tmp, - ) + with tempfile.TemporaryDirectory() as tmp2: + # A directory has to be zipped and it cannot contain the output of the zipping + logger.info(f"Compressing {self._local_name}") + shutil.make_archive( + str(Path(tmp2) / self._local_name), + "gztar", + tmp, + ) - logger.info(f"Uploading {self._local_name} to S3 from {path}") + logger.info(f"Uploading {self._local_name} to S3 from {path}") - file_to_be_uploaded = Path(tmp) / f"{self._local_name}.tar.gz" - self._client.upload_file( - Filename=str(file_to_be_uploaded), - Bucket=self.bucket_name, - Key=self._s3_name, - Callback=None - if hide_progress - else UploadProgressBar(path=file_to_be_uploaded, logger=logger), - ) + file_to_be_uploaded = Path(tmp2) / f"{self._local_name}.tar.gz" + self._client.upload_file( + Filename=str(file_to_be_uploaded), + Bucket=self.bucket_name, + Key=self._s3_name, + Callback=None + if hide_progress + else UploadProgressBar(path=file_to_be_uploaded, logger=logger), + ) self.clean_up() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d98428c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,126 @@ +anyio==3.5.0 +appnope==0.1.2 +asgiref==3.5.0 +asttokens==2.0.5 +attrs==21.4.0 +autoflake==1.4 +backcall==0.2.0 +beautifulsoup4==4.10.0 +black==22.3.0 +blis==0.7.7 +boto3==1.21.32 +botocore==1.24.32 +Brotli==1.0.9 +catalogue==2.0.7 +certifi==2021.10.8 +charset-normalizer==2.0.12 +click==8.0.4 +cycler==0.11.0 +cymem==2.0.6 +dash==2.3.1 +dash-core-components==2.0.0 +dash-html-components==2.0.0 +dash-table==5.0.0 +debugpy==1.6.0 +decorator==5.1.1 +devtools==0.8.0 +dill==0.3.4 +en-core-web-lg @ https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.2.0/en_core_web_lg-3.2.0-py3-none-any.whl +entrypoints==0.4 +executing==0.8.3 +fastapi==0.75.1 +flake8==4.0.1 +Flask==2.1.1 +Flask-Compress==1.11 +fonttools==4.31.2 +h11==0.13.0 +idna==3.3 +importlib-metadata==4.11.3 +iniconfig==1.1.1 +ipykernel==6.11.0 +ipython==8.2.0 +isort==5.10.1 +itsdangerous==2.1.2 +jedi==0.18.1 +Jinja2==3.1.1 +jmespath==1.0.0 +joblib==1.1.0 +jupyter-client==7.2.1 +jupyter-core==4.9.2 +kiwisolver==1.4.2 +langcodes==3.3.0 +langdetect==1.0.9 +language-data==1.1 +lxml==4.8.0 +marisa-trie==0.7.7 +MarkupSafe==2.1.1 +matplotlib==3.5.1 +matplotlib-inline==0.1.3 +mccabe==0.6.1 +multiprocess==0.70.12.2 +murmurhash==1.0.6 +mypy==0.942 +mypy-extensions==0.4.3 +nest-asyncio==1.5.4 +numpy==1.22.3 +packaging==21.3 +pandas==1.4.1 +parso==0.8.3 +pathspec==0.9.0 +pathy==0.6.1 +pexpect==4.8.0 +pickleshare==0.7.5 +Pillow==9.1.0 +platformdirs==2.5.1 +plotly==5.7.0 +pluggy==1.0.0 +preshed==3.0.6 +prompt-toolkit==3.0.28 +psutil==5.9.0 +ptyprocess==0.7.0 +pure-eval==0.2.2 +py==1.11.0 +pycodestyle==2.8.0 +pydantic==1.8.2 +pyflakes==2.4.0 +Pygments==2.11.2 +pyparsing==3.0.7 +pytest==7.1.1 +python-dateutil==2.8.2 +pytz==2022.1 +pyzmq==22.3.0 +requests==2.27.1 +s3transfer==0.5.2 +scikit-learn==1.0.2 +scipy==1.8.0 +six==1.16.0 +smart-open==5.2.1 +sniffio==1.2.0 +soupsieve==2.3.1 +spacy==3.2.4 +spacy-legacy==3.0.9 +spacy-loggers==1.0.2 +srsly==2.4.2 +stack-data==0.2.0 +starlette==0.17.1 +tenacity==8.0.1 +thinc==8.0.15 +threadpoolctl==3.1.0 +tinydb==4.7.0 +tokenize-rt==4.2.1 +tomli==2.0.1 +tornado==6.1 +tqdm==4.63.1 +traitlets==5.1.1 +typer==0.4.1 +types-requests==2.27.16 +types-ujson==4.2.1 +types-urllib3==1.26.11 +typing_extensions==4.1.1 +Unidecode==1.3.4 +urllib3==1.26.9 +uvicorn==0.17.6 +wasabi==0.9.1 +wcwidth==0.2.5 +Werkzeug==2.1.1 +zipp==3.8.0