Skip to content

Commit

Permalink
plugin search endpoint and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
pieroit committed Sep 7, 2023
1 parent 970693e commit e03a01d
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 65 deletions.
35 changes: 35 additions & 0 deletions core/cat/mad_hatter/registry.py
Original file line number Diff line number Diff line change
@@ -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 []
55 changes: 35 additions & 20 deletions core/cat/routes/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}


Expand Down
49 changes: 26 additions & 23 deletions core/tests/routes/plugins/test_plugins_info.py
Original file line number Diff line number Diff line change
@@ -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"
49 changes: 27 additions & 22 deletions core/tests/routes/plugins/test_plugins_install_uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
91 changes: 91 additions & 0 deletions core/tests/routes/plugins/test_plugins_registry.py
Original file line number Diff line number Diff line change
@@ -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!!!

0 comments on commit e03a01d

Please sign in to comment.