Skip to content

Commit

Permalink
persistent storage + Transformers AI (#123)
Browse files Browse the repository at this point in the history
An example of a Nextcloud Talk bot that uses a very small language
model, and the cache with this model is in the application's persistent
storage and persists between application updates.
---------
Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
  • Loading branch information
bigcat88 authored Sep 14, 2023
1 parent 85f2ab5 commit 4f31654
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 0 deletions.
29 changes: 29 additions & 0 deletions .run/TalkBotAI.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="TalkBotAI" type="PythonConfigurationType" factoryName="Python">
<module name="nc_py_api" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="APP_ID" value="talk_bot_ai" />
<env name="APP_PORT" value="9034" />
<env name="APP_SECRET" value="12345" />
<env name="APP_VERSION" value="1.0.0" />
<env name="NEXTCLOUD_URL" value="http://nextcloud.local" />
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/examples/as_app/talk_bot_AI/src/main.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project will be documented in this file.

## [0.2.1 - 2023-09-14]

### Added

- NextcloudApp: `ex_app.persistent_storage` function that returns path for the Application persistent storage.
- NextcloudApp: `from nc_py_api.ex_app import persist_transformers_cache` - automatic use of persistent app directory for the AI models caching.

## [0.2.0 - 2023-09-13]

### Added
Expand Down
10 changes: 10 additions & 0 deletions examples/as_app/talk_bot_AI/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM python:3.11-bookworm

COPY requirements.txt /
ADD /src/ /app/

RUN \
python3 -m pip install -r requirements.txt && rm -rf ~/.cache && rm requirements.txt

WORKDIR /app
ENTRYPOINT ["python3", "main.py"]
28 changes: 28 additions & 0 deletions examples/as_app/talk_bot_AI/HOW_TO_INSTALL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
How To Install
==============

Currently, while AppAPI hasn't been published on the App Store, and App Store support hasn't been added yet,
installation is a little bit tricky.

Steps to Install:

1. [Install AppAPI](https://cloud-py-api.github.io/app_api/Installation.html)
2. Create a deployment daemon according to the [instructions](https://cloud-py-api.github.io/app_api/CreationOfDeployDaemon.html#create-deploy-daemon) of the AppPI
3. php occ app_api:app:deploy talk_bot_ai "daemon_deploy_name" \
--info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/talk_bot_ai/appinfo/info.xml

to deploy a docker image with Bot to docker.

4. php occ app_api:app:register talk_bot_ai "daemon_deploy_name" -e --force-scopes \
--info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/talk_bot_ai/appinfo/info.xml

to call its **enable** handler and accept all required API scopes by default.


In a few months
===============

1. Install AppAPI from App Store
2. Configure Deploy Daemon with GUI provided by AppAPI
3. Go to External Applications page in Nextcloud UI
4. Find this bot in a list and press "Install" and "Enable" buttons, like with usual Applications.
49 changes: 49 additions & 0 deletions examples/as_app/talk_bot_AI/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.DEFAULT_GOAL := help

.PHONY: help
help:
@echo "Welcome to TalkBotAI example. Please use \`make <target>\` where <target> is one of"
@echo " "
@echo " Next commands are only for dev environment with nextcloud-docker-dev!"
@echo " They should run from the host you are developing on(with activated venv) and not in the container with Nextcloud!"
@echo " "
@echo " build-push build image and upload to ghcr.io"
@echo " "
@echo " deploy deploy example to registered 'docker_dev'"
@echo " "
@echo " run28 install TalkBotAI for Nextcloud 28"
@echo " run27 install TalkBotAI for Nextcloud 27"
@echo " "
@echo " For development of this example use PyCharm run configurations. Development is always set for last Nextcloud."
@echo " First run 'TalkBotAI' and then 'make manual_register', after that you can use/debug/develop it and easy test."
@echo " "
@echo " manual_register perform registration of running 'TalkBotAI' into the 'manual_install' deploy daemon."

.PHONY: build-push
build-push:
docker login ghcr.io
docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag ghcr.io/cloud-py-api/talk_bot_ai:latest .

.PHONY: deploy
deploy:
docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:deploy talk_bot_ai docker_dev \
--info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/talk_bot_ai/appinfo/info.xml

.PHONY: run28
run28:
docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:unregister talk_bot_ai --silent || true
docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:register talk_bot_ai docker_dev -e --force-scopes \
--info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/talk_bot_ai/appinfo/info.xml

.PHONY: run27
run27:
docker exec master-stable27-1 sudo -u www-data php occ app_api:app:unregister talk_bot_ai --silent || true
docker exec master-stable27-1 sudo -u www-data php occ app_api:app:register talk_bot_ai docker_dev -e --force-scopes \
--info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/talk_bot_ai/appinfo/info.xml

.PHONY: manual_register
manual_register:
docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:unregister talk_bot_ai --silent || true
docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:register talk_bot_ai manual_install --json-info \
"{\"appid\":\"talk_bot_ai\",\"name\":\"TalkBotAI\",\"daemon_config_name\":\"manual_install\",\"version\":\"1.0.0\",\"secret\":\"12345\",\"host\":\"host.docker.internal\",\"port\":9034,\"scopes\":{\"required\":[\"TALK\", \"TALK_BOT\"],\"optional\":[]},\"protocol\":\"http\",\"system_app\":0}" \
-e --force-scopes
38 changes: 38 additions & 0 deletions examples/as_app/talk_bot_AI/appinfo/info.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0"?>
<info>
<id>talk_bot_ai</id>
<name>TalkBotAI</name>
<summary>Nextcloud TalkBotAI Example</summary>
<description>
<![CDATA[Example of the Nextcloud Talk Bot + LLM written in python]]>
</description>
<version>1.0.0</version>
<licence>MIT</licence>
<author mail="andrey18106x@gmail.com" homepage="https://github.com/andrey18106">Andrey Borysenko</author>
<author mail="bigcat88@icloud.com" homepage="https://github.com/bigcat88">Alexander Piskun</author>
<namespace>TalkBotAIExample</namespace>
<category>tools</category>
<website>https://github.com/cloud-py-api/nc_py_api</website>
<bugs>https://github.com/cloud-py-api/nc_py_api/issues</bugs>
<repository type="git">https://github.com/cloud-py-api/nc_py_api</repository>
<dependencies>
<nextcloud min-version="27" max-version="28"/>
</dependencies>
<ex-app>
<docker-install>
<registry>ghcr.io</registry>
<image>cloud-py-api/talk_bot_ai</image>
<image-tag>latest</image-tag>
</docker-install>
<scopes>
<required>
<value>TALK</value>
<value>TALK_BOT</value>
</required>
<optional>
</optional>
</scopes>
<protocol>http</protocol>
<system>0</system>
</ex-app>
</info>
5 changes: 5 additions & 0 deletions examples/as_app/talk_bot_AI/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
nc_py_api[app]>=0.2.1
transformers>=4.33
torch
torchvision
torchaudio
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 72 additions & 0 deletions examples/as_app/talk_bot_AI/src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""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 transformers import pipeline

from nc_py_api import NextcloudApp, talk_bot
from nc_py_api.ex_app import run_app, set_handlers, talk_bot_app

APP = FastAPI()
AI_BOT = talk_bot.TalkBot("/ai_talk_bot", "AI talk bot", "Usage: `@ai What sounds do cats make?`")
MODEL_NAME = "MBZUAI/LaMini-Flan-T5-77M"
MODEL_INIT_THREAD = None


def ai_talk_bot_process_request(message: talk_bot.TalkBotMessage):
r = re.search(r"@ai\s(.*)", message.object_content["message"], re.IGNORECASE)
if r is None:
return
model = pipeline("text2text-generation", model="MBZUAI/LaMini-Flan-T5-77M")
response_text = model(r.group(1), max_length=64, do_sample=True)[0]["generated_text"]
AI_BOT.send_message(response_text, message)


@APP.post("/ai_talk_bot")
async def ai_talk_bot(
message: Annotated[talk_bot.TalkBotMessage, Depends(talk_bot_app)],
background_tasks: BackgroundTasks,
):
if message.object_name == "message":
background_tasks.add_task(ai_talk_bot_process_request, message)
return requests.Response()


def enabled_handler(enabled: bool, nc: NextcloudApp) -> str:
print(f"enabled={enabled}")
try:
AI_BOT.enabled_handler(enabled, nc)
except Exception as e:
return str(e)
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)


if __name__ == "__main__":
run_app("main:APP", log_level="trace")
1 change: 1 addition & 0 deletions nc_py_api/ex_app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""All possible ExApp stuff for NextcloudApp that can be used."""
from .defs import ApiScope, LogLvl
from .integration_fastapi import nc_app, set_handlers, talk_bot_app
from .misc import persistent_storage
from .ui.files import UiActionFileInfo, UiFileActionHandlerInfo
from .uvicorn_fastapi import run_app
25 changes: 25 additions & 0 deletions nc_py_api/ex_app/misc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Different miscellaneous optimization/helper functions for the Nextcloud Applications."""

import os
from sys import platform


def persistent_storage() -> str:
"""Returns the path to directory, which is permanent storage and is not deleted when the application is updated."""
return os.getenv("APP_PERSISTENT_STORAGE", _get_app_cache_dir())


def _get_app_cache_dir() -> str:
sys_platform = platform.lower()
root_cache_path = (
os.path.normpath(os.environ["LOCALAPPDATA"])
if sys_platform == "win32"
else (
os.path.expanduser("~/Library/Caches")
if sys_platform == "darwin"
else os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
)
)
r = os.path.join(root_cache_path, os.environ["APP_ID"])
os.makedirs(r, exist_ok=True)
return r
7 changes: 7 additions & 0 deletions nc_py_api/ex_app/persist_transformers_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Import this file to automatically point TRANSFORMERS_CACHE to your application's persistent storage."""

import os

from .misc import persistent_storage

os.environ["TRANSFORMERS_CACHE"] = persistent_storage()
8 changes: 8 additions & 0 deletions tests/actual_tests/misc_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import os

import pytest

Expand Down Expand Up @@ -66,3 +67,10 @@ def test_ocs_response_headers(nc):
def test_nc_iso_time_to_datetime():
parsed_time = nc_iso_time_to_datetime("invalid")
assert parsed_time == datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)


def test_persist_transformers_cache(nc_app):
assert "TRANSFORMERS_CACHE" not in os.environ
from nc_py_api.ex_app import persist_transformers_cache # noqa

assert os.environ["TRANSFORMERS_CACHE"]

0 comments on commit 4f31654

Please sign in to comment.