From 54e5933a9a91a6a05ff477d21b0b16641f63cd1e Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Tue, 17 Oct 2023 15:15:43 +0300 Subject: [PATCH 1/6] dev, AppAPI 1.1.0 Signed-off-by: Alexander Piskun --- CHANGELOG.md | 10 ++++ examples/as_app/skeleton/requirements.txt | 2 +- examples/as_app/talk_bot/requirements.txt | 2 +- examples/as_app/talk_bot_ai/requirements.txt | 2 +- examples/as_app/talk_bot_ai/src/main.py | 28 ++--------- .../as_app/talk_bot_multi/requirements.txt | 2 +- examples/as_app/to_gif/requirements.txt | 2 +- nc_py_api/_version.py | 2 +- nc_py_api/ex_app/integration_fastapi.py | 48 ++++++++++++++++++- nc_py_api/nextcloud.py | 13 +++++ 10 files changed, 80 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6176b5a..3ec800f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## [0.4.1 - 2023-10-17] + +### Added + +- Support for the new AppAPI endpoint `/init` and automatically downloading models from `huggingface`. #151 + +### Changed + +- All examples were adjusted to changes in AppAPI. + ## [0.4.0 - 2023-10-15] As the project moves closer to `beta`, final unification changes are being made. diff --git a/examples/as_app/skeleton/requirements.txt b/examples/as_app/skeleton/requirements.txt index 3fac8ce1..20e27d1b 100644 --- a/examples/as_app/skeleton/requirements.txt +++ b/examples/as_app/skeleton/requirements.txt @@ -1 +1 @@ -nc_py_api[app]>=0.3.0 +nc_py_api[app]>=0.4.1 diff --git a/examples/as_app/talk_bot/requirements.txt b/examples/as_app/talk_bot/requirements.txt index 3fac8ce1..20e27d1b 100644 --- a/examples/as_app/talk_bot/requirements.txt +++ b/examples/as_app/talk_bot/requirements.txt @@ -1 +1 @@ -nc_py_api[app]>=0.3.0 +nc_py_api[app]>=0.4.1 diff --git a/examples/as_app/talk_bot_ai/requirements.txt b/examples/as_app/talk_bot_ai/requirements.txt index 3d9fc12c..dfa5a75e 100644 --- a/examples/as_app/talk_bot_ai/requirements.txt +++ b/examples/as_app/talk_bot_ai/requirements.txt @@ -1,4 +1,4 @@ -nc_py_api[app]>=0.3.0 +nc_py_api[app]>=0.4.1 transformers>=4.33 torch torchvision diff --git a/examples/as_app/talk_bot_ai/src/main.py b/examples/as_app/talk_bot_ai/src/main.py index 65d27d3b..68f3e40d 100644 --- a/examples/as_app/talk_bot_ai/src/main.py +++ b/examples/as_app/talk_bot_ai/src/main.py @@ -1,13 +1,10 @@ """Example of an application that uses Python Transformers library with Talk Bot APIs.""" - -# This line should be on top before any import of the "Transformers" library. -from nc_py_api.ex_app import persist_transformers_cache # noqa # isort:skip import re -from threading import Thread from typing import Annotated import requests from fastapi import BackgroundTasks, Depends, FastAPI +from huggingface_hub import snapshot_download from transformers import pipeline from nc_py_api import NextcloudApp, talk_bot @@ -15,15 +12,14 @@ APP = FastAPI() AI_BOT = talk_bot.TalkBot("/ai_talk_bot", "AI talk bot", "Usage: `@assistant What sounds do cats make?`") -MODEL_NAME = "MBZUAI/LaMini-Flan-T5-77M" -MODEL_INIT_THREAD = None +MODEL_NAME = "MBZUAI/LaMini-Flan-T5-783M" def ai_talk_bot_process_request(message: talk_bot.TalkBotMessage): r = re.search(r"@assistant\s(.*)", message.object_content["message"], re.IGNORECASE) if r is None: return - model = pipeline("text2text-generation", model=MODEL_NAME) + model = pipeline("text2text-generation", model=snapshot_download(MODEL_NAME, local_files_only=True)) response_text = model(r.group(1), max_length=64, do_sample=True)[0]["generated_text"] AI_BOT.send_message(response_text, message) @@ -47,25 +43,9 @@ def enabled_handler(enabled: bool, nc: NextcloudApp) -> str: return "" -def download_models(): - pipeline("text2text-generation", model=MODEL_NAME) - - -def heartbeat_handler() -> str: - global MODEL_INIT_THREAD - print("heartbeat_handler: called") - if MODEL_INIT_THREAD is None: - MODEL_INIT_THREAD = Thread(target=download_models) - MODEL_INIT_THREAD.start() - print("heartbeat_handler: started initialization thread") - r = "init" if MODEL_INIT_THREAD.is_alive() else "ok" - print(f"heartbeat_handler: result={r}") - return r - - @APP.on_event("startup") def initialization(): - set_handlers(APP, enabled_handler, heartbeat_handler) + set_handlers(APP, enabled_handler, models_to_fetch=[MODEL_NAME]) if __name__ == "__main__": diff --git a/examples/as_app/talk_bot_multi/requirements.txt b/examples/as_app/talk_bot_multi/requirements.txt index 3fac8ce1..20e27d1b 100644 --- a/examples/as_app/talk_bot_multi/requirements.txt +++ b/examples/as_app/talk_bot_multi/requirements.txt @@ -1 +1 @@ -nc_py_api[app]>=0.3.0 +nc_py_api[app]>=0.4.1 diff --git a/examples/as_app/to_gif/requirements.txt b/examples/as_app/to_gif/requirements.txt index a98523bb..e7bab5ea 100644 --- a/examples/as_app/to_gif/requirements.txt +++ b/examples/as_app/to_gif/requirements.txt @@ -1,4 +1,4 @@ -nc_py_api[app]>=0.3.0 +nc_py_api[app]>=0.4.1 pygifsicle imageio opencv-python diff --git a/nc_py_api/_version.py b/nc_py_api/_version.py index 84a58560..f70a7d3b 100644 --- a/nc_py_api/_version.py +++ b/nc_py_api/_version.py @@ -1,3 +1,3 @@ """Version of nc_py_api.""" -__version__ = "0.4.0" +__version__ = "0.4.1.dev0" diff --git a/nc_py_api/ex_app/integration_fastapi.py b/nc_py_api/ex_app/integration_fastapi.py index 8ea19d9a..41b5ac55 100644 --- a/nc_py_api/ex_app/integration_fastapi.py +++ b/nc_py_api/ex_app/integration_fastapi.py @@ -6,11 +6,22 @@ import json import typing -from fastapi import Depends, FastAPI, HTTPException, Request, responses, status +import tqdm +from fastapi import ( + BackgroundTasks, + Depends, + FastAPI, + HTTPException, + Request, + responses, + status, +) +from huggingface_hub import snapshot_download from .._misc import get_username_secret_from_headers from ..nextcloud import NextcloudApp from ..talk_bot import TalkBotMessage, get_bot_secret +from .misc import persistent_storage def nc_app(request: Request) -> NextcloudApp: @@ -45,14 +56,42 @@ def set_handlers( fast_api_app: FastAPI, enabled_handler: typing.Callable[[bool, NextcloudApp], str], heartbeat_handler: typing.Optional[typing.Callable[[], str]] = None, + init_handler: typing.Optional[typing.Callable[[], None]] = None, + models_to_fetch: typing.Optional[list[str]] = None, + models_download_params: typing.Optional[dict] = None, ): """Defines handlers for the application. :param fast_api_app: FastAPI() call return value. :param enabled_handler: ``Required``, callback which will be called for `enabling`/`disabling` app event. :param heartbeat_handler: Optional, callback that will be called for the `heartbeat` deploy event. + :param init_handler: Optional, callback that will be called for the `init` event. + :param models_to_fetch: Optional, dictionary describing which models should be downloaded during `init`. + :param models_download_params: Optional, parameters to pass to ``snapshot_download``. + + .. note:: If ``init_handler`` is specified, it is up to a developer to send an application init progress status. + AppAPI will only call `enabled_handler` after it receives ``100`` as initialization status progress. """ + def fetch_models_task(models: dict): + class TqdmProgress(tqdm.tqdm): + def display(self, msg=None, pos=None): + if init_handler is None: + a = min(int((self.n * 100 / self.total) / len(models)), 100) + # NextcloudApp().update_init_status(a) + print(a) + return super().display(msg, pos) + + params = models_download_params if models_download_params else {} + if "max_workers" not in params: + params["max_workers"] = 2 + if "cache_dir" not in params: + params["cache_dir"] = persistent_storage() + for model in models: + snapshot_download(model, tqdm_class=TqdmProgress, **params) # noqa + if init_handler is None: + NextcloudApp().update_init_status(100) + @fast_api_app.put("/enabled") def enabled_callback( enabled: bool, @@ -67,3 +106,10 @@ def heartbeat_callback(): if heartbeat_handler is not None: return_status = heartbeat_handler() return responses.JSONResponse(content={"status": return_status}, status_code=200) + + @fast_api_app.post("/init") + def init_callback(background_tasks: BackgroundTasks): + background_tasks.add_task(fetch_models_task, models_to_fetch if models_to_fetch else {}) + if init_handler is not None: + init_handler() + return responses.JSONResponse(content={}, status_code=200) diff --git a/nc_py_api/nextcloud.py b/nc_py_api/nextcloud.py index d1a8f17b..296ab830 100644 --- a/nc_py_api/nextcloud.py +++ b/nc_py_api/nextcloud.py @@ -255,3 +255,16 @@ def request_sign_check(self, request: Request) -> bool: print(e) return False return True + + def update_init_status(self, progress: int) -> None: + """Updates initialization status of installation. + + :param progress: a number from ``0`` to ``100`` indicating the percentage of application readiness for work. + After sending ``100`` AppAPI will enable the application. + """ + self._session.ocs( + method="PUT", + path=f"/index.php/apps/app_api/apps/status/{self._session.cfg.app_name}", + json={"progress": progress}, + not_parse=True, + ) From 6c0d891db3e5f9422d86ed7776b6e72afc523c8f Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Tue, 17 Oct 2023 17:19:29 +0300 Subject: [PATCH 2/6] correct importing Signed-off-by: Alexander Piskun --- nc_py_api/ex_app/integration_fastapi.py | 48 +++++++++++++------------ nc_py_api/nextcloud.py | 4 +-- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/nc_py_api/ex_app/integration_fastapi.py b/nc_py_api/ex_app/integration_fastapi.py index 41b5ac55..67fd1be5 100644 --- a/nc_py_api/ex_app/integration_fastapi.py +++ b/nc_py_api/ex_app/integration_fastapi.py @@ -6,7 +6,6 @@ import json import typing -import tqdm from fastapi import ( BackgroundTasks, Depends, @@ -16,7 +15,6 @@ responses, status, ) -from huggingface_hub import snapshot_download from .._misc import get_username_secret_from_headers from ..nextcloud import NextcloudApp @@ -66,31 +64,37 @@ def set_handlers( :param enabled_handler: ``Required``, callback which will be called for `enabling`/`disabling` app event. :param heartbeat_handler: Optional, callback that will be called for the `heartbeat` deploy event. :param init_handler: Optional, callback that will be called for the `init` event. - :param models_to_fetch: Optional, dictionary describing which models should be downloaded during `init`. - :param models_download_params: Optional, parameters to pass to ``snapshot_download``. - .. note:: If ``init_handler`` is specified, it is up to a developer to send an application init progress status. - AppAPI will only call `enabled_handler` after it receives ``100`` as initialization status progress. + .. note:: If ``init_handler`` is specified, it is up to a developer to set the application init progress status. + AppAPI will only call `enabled_handler` after it receives ``100`` as initialization status progress. + + :param models_to_fetch: Dictionary describing which models should be downloaded during `init`. + + .. note:: ``tqdm`` and ``huggingface_hub`` packages should be present for automatic models fetching. + + :param models_download_params: Parameters to pass to ``snapshot_download`` function from **huggingface_hub**. """ def fetch_models_task(models: dict): - class TqdmProgress(tqdm.tqdm): - def display(self, msg=None, pos=None): - if init_handler is None: - a = min(int((self.n * 100 / self.total) / len(models)), 100) - # NextcloudApp().update_init_status(a) - print(a) - return super().display(msg, pos) - - params = models_download_params if models_download_params else {} - if "max_workers" not in params: - params["max_workers"] = 2 - if "cache_dir" not in params: - params["cache_dir"] = persistent_storage() - for model in models: - snapshot_download(model, tqdm_class=TqdmProgress, **params) # noqa + if models: + from huggingface_hub import snapshot_download # pylint: disable=C0415 + from tqdm import tqdm # pylint: disable=C0415 + + class TqdmProgress(tqdm): + def display(self, msg=None, pos=None): + if init_handler is None: + NextcloudApp().set_init_status(min(int((self.n * 100 / self.total) / len(models)), 100)) + return super().display(msg, pos) + + params = models_download_params if models_download_params else {} + if "max_workers" not in params: + params["max_workers"] = 2 + if "cache_dir" not in params: + params["cache_dir"] = persistent_storage() + for model in models: + snapshot_download(model, tqdm_class=TqdmProgress, **params) # noqa if init_handler is None: - NextcloudApp().update_init_status(100) + NextcloudApp().set_init_status(100) @fast_api_app.put("/enabled") def enabled_callback( diff --git a/nc_py_api/nextcloud.py b/nc_py_api/nextcloud.py index 296ab830..ba468fcd 100644 --- a/nc_py_api/nextcloud.py +++ b/nc_py_api/nextcloud.py @@ -256,8 +256,8 @@ def request_sign_check(self, request: Request) -> bool: return False return True - def update_init_status(self, progress: int) -> None: - """Updates initialization status of installation. + def set_init_status(self, progress: int) -> None: + """Sets state of the app initialization. :param progress: a number from ``0`` to ``100`` indicating the percentage of application readiness for work. After sending ``100`` AppAPI will enable the application. From fdf774eb258df4c29ad105152e4dbb8e1817c226 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Tue, 17 Oct 2023 17:27:05 +0300 Subject: [PATCH 3/6] correct importing(2) Signed-off-by: Alexander Piskun --- nc_py_api/ex_app/integration_fastapi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nc_py_api/ex_app/integration_fastapi.py b/nc_py_api/ex_app/integration_fastapi.py index 67fd1be5..d8d54388 100644 --- a/nc_py_api/ex_app/integration_fastapi.py +++ b/nc_py_api/ex_app/integration_fastapi.py @@ -77,8 +77,8 @@ def set_handlers( def fetch_models_task(models: dict): if models: - from huggingface_hub import snapshot_download # pylint: disable=C0415 - from tqdm import tqdm # pylint: disable=C0415 + from huggingface_hub import snapshot_download # noqa isort:skip pylint: disable=C0415 disable=E0401 + from tqdm import tqdm # noqa isort:skip pylint: disable=C0415 disable=E0401 class TqdmProgress(tqdm): def display(self, msg=None, pos=None): From afe5001444f95eea4be73cd42aa65f46a44fc013 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Tue, 17 Oct 2023 19:11:56 +0300 Subject: [PATCH 4/6] AppAPI 1.1.0 `/init` initial tests Signed-off-by: Alexander Piskun --- nc_py_api/ex_app/integration_fastapi.py | 6 ++--- pyproject.toml | 1 + tests/_install_init_handler_models.py | 29 +++++++++++++++++++++++++ tests/_tests_at_the_end.py | 23 ++++++++++++++++++++ 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 tests/_install_init_handler_models.py diff --git a/nc_py_api/ex_app/integration_fastapi.py b/nc_py_api/ex_app/integration_fastapi.py index d8d54388..fc645b5e 100644 --- a/nc_py_api/ex_app/integration_fastapi.py +++ b/nc_py_api/ex_app/integration_fastapi.py @@ -70,7 +70,7 @@ def set_handlers( :param models_to_fetch: Dictionary describing which models should be downloaded during `init`. - .. note:: ``tqdm`` and ``huggingface_hub`` packages should be present for automatic models fetching. + .. note:: ```huggingface_hub`` package should be present for automatic models fetching. :param models_download_params: Parameters to pass to ``snapshot_download`` function from **huggingface_hub**. """ @@ -95,6 +95,8 @@ def display(self, msg=None, pos=None): snapshot_download(model, tqdm_class=TqdmProgress, **params) # noqa if init_handler is None: NextcloudApp().set_init_status(100) + else: + init_handler() @fast_api_app.put("/enabled") def enabled_callback( @@ -114,6 +116,4 @@ def heartbeat_callback(): @fast_api_app.post("/init") def init_callback(background_tasks: BackgroundTasks): background_tasks.add_task(fetch_models_task, models_to_fetch if models_to_fetch else {}) - if init_handler is not None: - init_handler() return responses.JSONResponse(content={}, status_code=200) diff --git a/pyproject.toml b/pyproject.toml index 58179f06..0860b894 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ dev = [ ] dev-min = [ "coverage", + "huggingface_hub", "pillow", "pre-commit", "pylint", diff --git a/tests/_install_init_handler_models.py b/tests/_install_init_handler_models.py new file mode 100644 index 00000000..9d0f8ef0 --- /dev/null +++ b/tests/_install_init_handler_models.py @@ -0,0 +1,29 @@ +from fastapi import FastAPI +from huggingface_hub import snapshot_download + +from nc_py_api import NextcloudApp, ex_app + +APP = FastAPI() +MODEL_NAME = "MBZUAI/LaMini-T5-61M" + + +def enabled_handler(_enabled: bool, _nc: NextcloudApp) -> str: + if _enabled: + try: + snapshot_download(MODEL_NAME, local_files_only=True) + except Exception: # noqa + return "model not found" + return "" + + +def init_handler(): + NextcloudApp().set_init_status(100) + + +@APP.on_event("startup") +def initialization(): + ex_app.set_handlers(APP, enabled_handler, init_handler=init_handler, models_to_fetch=[MODEL_NAME]) + + +if __name__ == "__main__": + ex_app.run_app("_install_init_handler_models:APP", log_level="warning") diff --git a/tests/_tests_at_the_end.py b/tests/_tests_at_the_end.py index 04cec84d..0631678b 100644 --- a/tests/_tests_at_the_end.py +++ b/tests/_tests_at_the_end.py @@ -28,3 +28,26 @@ def test_ex_app_enable_disable(nc_client, nc_app): assert nc_client.apps.ex_app_is_enabled("nc_py_api") is True finally: r.terminate() + + +def test_install_init_handler_models(nc_client, nc_app): + child_environment = os.environ.copy() + child_environment["APP_PORT"] = os.environ.get("APP_PORT", "9009") + r = Popen( + [ + sys.executable, + os.path.join(os.path.dirname(os.path.abspath(__file__)), "_install_init_handler_models.py"), + ], + env=child_environment, + cwd=os.getcwd(), + ) + url = f"http://127.0.0.1:{child_environment['APP_PORT']}/heartbeat" + try: + if check_heartbeat(url, '"status":"ok"', 15, 0.3): + raise RuntimeError("`_install_init_handler_models` can not start.") + if nc_client.apps.ex_app_is_enabled("nc_py_api"): + nc_client.apps.ex_app_disable("nc_py_api") + nc_client.apps.ex_app_enable("nc_py_api") + assert nc_client.apps.ex_app_is_enabled("nc_py_api") is True + finally: + r.terminate() From 08fed2254512a02b00e0aceb77924abaddbbb745 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Thu, 19 Oct 2023 17:30:59 +0300 Subject: [PATCH 5/6] appmode: added new `error` param to `set_init_status` Signed-off-by: Alexander Piskun --- .run/TalkBotAI (27).run.xml | 29 +++++++++++++++++++ ...lkBotAI.run.xml => TalkBotAI (28).run.xml} | 2 +- .../{ToGif(28).run.xml => ToGif (28).run.xml} | 4 +-- examples/as_app/talk_bot_ai/Makefile | 18 ++++++++---- nc_py_api/nextcloud.py | 10 +++++-- 5 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 .run/TalkBotAI (27).run.xml rename .run/{TalkBotAI.run.xml => TalkBotAI (28).run.xml} (92%) rename .run/{ToGif(28).run.xml => ToGif (28).run.xml} (92%) diff --git a/.run/TalkBotAI (27).run.xml b/.run/TalkBotAI (27).run.xml new file mode 100644 index 00000000..ae38223c --- /dev/null +++ b/.run/TalkBotAI (27).run.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/.run/TalkBotAI.run.xml b/.run/TalkBotAI (28).run.xml similarity index 92% rename from .run/TalkBotAI.run.xml rename to .run/TalkBotAI (28).run.xml index e65a81d9..43e78023 100644 --- a/.run/TalkBotAI.run.xml +++ b/.run/TalkBotAI (28).run.xml @@ -1,5 +1,5 @@ - +