From e03a01dca5d0db0e2c5fd9be91362dc129bb65d2 Mon Sep 17 00:00:00 2001 From: Piero Savastano Date: Thu, 7 Sep 2023 18:39:45 +0200 Subject: [PATCH] plugin search endpoint and tests --- core/cat/mad_hatter/registry.py | 35 +++++++ core/cat/routes/plugins.py | 55 +++++++---- .../tests/routes/plugins/test_plugins_info.py | 49 +++++----- .../plugins/test_plugins_install_uninstall.py | 49 +++++----- .../routes/plugins/test_plugins_registry.py | 91 +++++++++++++++++++ 5 files changed, 214 insertions(+), 65 deletions(-) create mode 100644 core/cat/mad_hatter/registry.py create mode 100644 core/tests/routes/plugins/test_plugins_registry.py diff --git a/core/cat/mad_hatter/registry.py b/core/cat/mad_hatter/registry.py new file mode 100644 index 00000000..93cff5e8 --- /dev/null +++ b/core/cat/mad_hatter/registry.py @@ -0,0 +1,35 @@ +import requests + +from cat.log import log + + +async def registry_search_plugins( + query: str = None, + #author: str = None, + #tag: str = None, +): + + registry_url = "https://registry.cheshirecat.ai" + + try: + if query: + # search plugins + url = f"{registry_url}/search" + payload = { + "query": query + } + response = requests.post(url, json=payload) + return response.json() + else: + # list plugins as sorted by registry (no search) + url = f"{registry_url}/plugins" + params = { + "page": 1, + "page_size": 1000, + } + response = requests.get(url, params=params) + return response.json()["plugins"] + + except Exception as e: + log(e, "ERROR") + return [] diff --git a/core/cat/routes/plugins.py b/core/cat/routes/plugins.py index 8692158b..6d095000 100644 --- a/core/cat/routes/plugins.py +++ b/core/cat/routes/plugins.py @@ -4,45 +4,60 @@ from tempfile import NamedTemporaryFile from fastapi import Body, Request, APIRouter, HTTPException, UploadFile, BackgroundTasks from cat.log import log +from cat.mad_hatter.registry import registry_search_plugins from urllib.parse import urlparse import requests router = APIRouter() -async def get_registry_list(): - try: - response = requests.get("https://registry.cheshirecat.ai/plugins?page=1&page_size=1000") - if response.status_code == 200: - return response.json()["plugins"] - else: - return [] - except Exception as e: - log(e, "ERROR") - return [] # GET plugins @router.get("/") -async def get_available_plugins(request: Request) -> Dict: +async def get_available_plugins( + request: Request, + query: str = None, + #author: str = None, to be activated in case of more granular search + #tag: str = None, to be activated in case of more granular search +) -> Dict: """List available plugins""" - # access cat instance + # retrieve plugins from official repo + registry_plugins = await registry_search_plugins(query) + # index registry plugins by url + registry_plugins_index = {} + for p in registry_plugins: + plugin_url = p["url"] + registry_plugins_index[plugin_url] = p + + # get active plugins ccat = request.app.state.ccat - active_plugins = ccat.mad_hatter.load_active_plugins_from_db() - # plugins are managed by the MadHatter class - plugins = [] + # list installed plugins' manifest + installed_plugins = [] for p in ccat.mad_hatter.plugins.values(): + + # get manifest manifest = deepcopy(p.manifest) # we make a copy to avoid modifying the plugin obj manifest["active"] = p.id in active_plugins # pass along if plugin is active or not - plugins.append(manifest) + + # filter by query + plugin_text = [str(field) for field in manifest.values()] + plugin_text = " ".join(plugin_text).lower() + if (query is None) or (query.lower() in plugin_text): + installed_plugins.append(manifest) - # retrieve plugins from official repo - registry = await get_registry_list() + # do not show already installed plugins among registry plugins + registry_plugins_index.pop( manifest["plugin_url"], None ) return { - "installed": plugins, - "registry": registry + "filters": { + "query": query, + #"author": author, to be activated in case of more granular search + #"tag": tag, to be activated in case of more granular search + }, + "installed": installed_plugins, + "registry": list(registry_plugins_index.values()) } diff --git a/core/tests/routes/plugins/test_plugins_info.py b/core/tests/routes/plugins/test_plugins_info.py index 856030cc..574c5a9c 100644 --- a/core/tests/routes/plugins/test_plugins_info.py +++ b/core/tests/routes/plugins/test_plugins_info.py @@ -1,42 +1,45 @@ import os import time -import pytest -import shutil -from tests.utils import key_in_json -@pytest.mark.parametrize("key", ["installed", "registry"]) -def test_list_plugins(client, key): - # Act - response = client.get("/plugins") +def test_list_plugins(client): - response_json = response.json() + response = client.get("/plugins") + json = response.json() - # Assert assert response.status_code == 200 - assert key_in_json(key, response_json) - assert response_json["installed"][0]["id"] == "core_plugin" - assert response_json["installed"][0]["active"] == True + for key in ["filters", "installed", "registry"]: + assert key in json.keys() + # query + for key in ["query"]: # ["query", "author", "tag"]: + assert key in json["filters"].keys() + + # installed + assert json["installed"][0]["id"] == "core_plugin" + assert json["installed"][0]["active"] == True -@pytest.mark.parametrize("keys", ["data"]) -def test_get_plugin_id(client, keys): - # Act + # registry (see more registry tests in `./test_plugins_registry.py`) + assert type(json["registry"] == list) + assert len(json["registry"]) > 0 + + +def test_get_plugin_id(client): + response = client.get("/plugins/core_plugin") - response_json = response.json() + json = response.json() - assert key_in_json(keys, response_json) - assert response_json["data"] is not None - assert response_json["data"]["id"] == "core_plugin" - assert response_json["data"]["active"] == True + assert "data" in json.keys() + assert json["data"] is not None + assert json["data"]["id"] == "core_plugin" + assert json["data"]["active"] == True def test_get_non_existent_plugin(client): response = client.get("/plugins/no_plugin") - response_json = response.json() + json = response.json() assert response.status_code == 404 - assert response_json["detail"]["error"] == "Plugin not found" - + assert json["detail"]["error"] == "Plugin not found" \ No newline at end of file diff --git a/core/tests/routes/plugins/test_plugins_install_uninstall.py b/core/tests/routes/plugins/test_plugins_install_uninstall.py index ca2d5874..899ac949 100644 --- a/core/tests/routes/plugins/test_plugins_install_uninstall.py +++ b/core/tests/routes/plugins/test_plugins_install_uninstall.py @@ -6,8 +6,30 @@ from fixture_just_installed_plugin import just_installed_plugin -# TODO: these test cases should be splitted in different test functions, with apppropriate setup/teardown -def test_plugin_install_upload_zip(client, just_installed_plugin): +def test_plugin_uninstall(client, just_installed_plugin): + + # during tests, the cat uses a different folder for plugins + mock_plugin_final_folder = "tests/mocks/mock_plugin_folder/mock_plugin" + + # remove plugin via endpoint (will delete also plugin folder in mock_plugin_folder) + response = client.delete("/plugins/mock_plugin") + assert response.status_code == 200 + + # mock_plugin is not installed in the cat (check both via endpoint and filesystem) + response = client.get("/plugins") + installed_plugins_names = list(map(lambda p: p["id"], response.json()["installed"])) + assert "mock_plugin" not in installed_plugins_names + assert not os.path.exists(mock_plugin_final_folder) # plugin folder removed from disk + + # plugin tool disappeared + tools = get_embedded_tools(client) + assert len(tools) == 1 + tool_names = list(map(lambda t: t["metadata"]["name"], tools)) + assert "mock_tool" not in tool_names + assert "get_the_time" in tool_names # from core_plugin + + +def test_plugin_install_from_zip(client, just_installed_plugin): # during tests, the cat uses a different folder for plugins mock_plugin_final_folder = "tests/mocks/mock_plugin_folder/mock_plugin" @@ -32,26 +54,9 @@ def test_plugin_install_upload_zip(client, just_installed_plugin): tool_names = list(map(lambda t: t["metadata"]["name"], tools)) assert "mock_tool" in tool_names assert "get_the_time" in tool_names # from core_plugin - -def test_plugin_uninstall(client, just_installed_plugin): - # during tests, the cat uses a different folder for plugins - mock_plugin_final_folder = "tests/mocks/mock_plugin_folder/mock_plugin" +def test_plugin_install_from_registry(client): - # remove plugin via endpoint (will delete also plugin folder in mock_plugin_folder) - response = client.delete("/plugins/mock_plugin") - assert response.status_code == 200 - - # mock_plugin is not installed in the cat (check both via endpoint and filesystem) - response = client.get("/plugins") - installed_plugins_names = list(map(lambda p: p["id"], response.json()["installed"])) - assert "mock_plugin" not in installed_plugins_names - assert not os.path.exists(mock_plugin_final_folder) # plugin folder removed from disk - - # plugin tool disappeared - tools = get_embedded_tools(client) - assert len(tools) == 1 - tool_names = list(map(lambda t: t["metadata"]["name"], tools)) - assert "mock_tool" not in tool_names - assert "get_the_time" in tool_names # from core_plugin + # TODO: install plugin from registry + pass diff --git a/core/tests/routes/plugins/test_plugins_registry.py b/core/tests/routes/plugins/test_plugins_registry.py new file mode 100644 index 00000000..fe8d7161 --- /dev/null +++ b/core/tests/routes/plugins/test_plugins_registry.py @@ -0,0 +1,91 @@ +import os + +# TODO: registry responses here should be mocked, at the moment we are actually calling the service + +def test_list_registry_plugins(client): + + response = client.get("/plugins") + json = response.json() + + assert response.status_code == 200 + assert "registry" in json.keys() + assert type(json["registry"] == list) + assert len(json["registry"]) > 0 + + # registry (see more registry tests in `./test_plugins_registry.py`) + assert type(json["registry"] == list) + assert len(json["registry"]) > 0 + + # query + for key in ["query"]: # ["query", "author", "tag"]: + assert key in json["filters"].keys() + + +def test_list_registry_plugins_by_query(client): + + params = { + "query": "podcast" + } + response = client.get("/plugins", params=params) + json = response.json() + print(json) + + assert response.status_code == 200 + assert json["filters"]["query"] == params["query"] + assert len(json["registry"]) > 0 # found registry plugins with text + for plugin in json["registry"]: + plugin_text = plugin["name"] + plugin["description"] + assert params["query"] in plugin_text # verify searched text + + +# TOOD: these tests are to be activated when also search by tag and author is activated in core +''' +def test_list_registry_plugins_by_author(client): + + params = { + "author": "Nicola Corbellini" + } + response = client.get("/plugins", params=params) + json = response.json() + + assert response.status_code == 200 + assert json["filters"]["author"] == params["query"] + assert len(json["registry"]) > 0 # found registry plugins with author + for plugin in json["registry"]: + assert params["author"] in plugin["author_name"] # verify author + + +def test_list_registry_plugins_by_tag(client): + + params = { + "tag": "llm" + } + response = client.get("/plugins", params=params) + json = response.json() + + assert response.status_code == 200 + assert json["filters"]["tag"] == params["tag"] + assert len(json["registry"]) > 0 # found registry plugins with tag + for plugin in json["registry"]: + plugin_tags = plugin["tags"].split(", ") + assert params["tag"] in plugin_tags # verify tag +''' + + +# take away from the list of availbale registry plugins, the ones that are already installed +def test_list_registry_plugins_without_duplicating_installed_plugins(client): + + # 1. install plugin from registry + # TODO !!! + + # 2. get available plugins searching for the one just installed + params = { + "query": "podcast" + } + response = client.get("/plugins", params=params) + json = response.json() + + # 3. plugin should show up among installed by not among registry ones + assert response.status_code == 200 + # TODO plugin compares in installed!!! + # TODO plugin does not appear in registry!!! \ No newline at end of file