From eef79a519602e0759eb78bbd205ee00303be705a Mon Sep 17 00:00:00 2001 From: Yeuoly Date: Fri, 20 Sep 2024 21:35:19 +0800 Subject: [PATCH] feat: support install plugin --- api/controllers/console/__init__.py | 11 +++++- api/core/plugin/entities/plugin_daemon.py | 16 +++++++- api/core/plugin/manager/base.py | 27 +++++++------ api/core/plugin/manager/plugin.py | 46 +++++++++++++++++++++++ 4 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 api/core/plugin/manager/plugin.py diff --git a/api/controllers/console/__init__.py b/api/controllers/console/__init__.py index f36703192a..1cf987050a 100644 --- a/api/controllers/console/__init__.py +++ b/api/controllers/console/__init__.py @@ -56,4 +56,13 @@ from .explore import ( from .tag import tags # Import workspace controllers -from .workspace import account, load_balancing_config, members, model_providers, models, tool_providers, workspace, plugin +from .workspace import ( + account, + load_balancing_config, + members, + model_providers, + models, + plugin, + tool_providers, + workspace, +) diff --git a/api/core/plugin/entities/plugin_daemon.py b/api/core/plugin/entities/plugin_daemon.py index ca8ea0f780..63c839a4e4 100644 --- a/api/core/plugin/entities/plugin_daemon.py +++ b/api/core/plugin/entities/plugin_daemon.py @@ -1,8 +1,9 @@ +from enum import Enum from typing import Generic, Optional, TypeVar from pydantic import BaseModel -T = TypeVar("T", bound=(BaseModel | dict)) +T = TypeVar("T", bound=(BaseModel | dict | bool)) class PluginDaemonBasicResponse(BaseModel, Generic[T]): @@ -13,3 +14,16 @@ class PluginDaemonBasicResponse(BaseModel, Generic[T]): code: int message: str data: Optional[T] + + +class InstallPluginMessage(BaseModel): + """ + Message for installing a plugin. + """ + class Event(Enum): + Info = "info" + Done = "done" + Error = "error" + + event: Event + data: str \ No newline at end of file diff --git a/api/core/plugin/manager/base.py b/api/core/plugin/manager/base.py index 704afe71d1..f6b44d05dd 100644 --- a/api/core/plugin/manager/base.py +++ b/api/core/plugin/manager/base.py @@ -12,12 +12,17 @@ from core.plugin.entities.plugin_daemon import PluginDaemonBasicResponse plugin_daemon_inner_api_baseurl = dify_config.PLUGIN_API_URL plugin_daemon_inner_api_key = dify_config.PLUGIN_API_KEY -T = TypeVar("T", bound=(BaseModel | dict)) +T = TypeVar("T", bound=(BaseModel | dict | bool)) class BasePluginManager: def _request( - self, method: str, path: str, headers: dict | None = None, data: bytes | None = None, stream: bool = False + self, + method: str, + path: str, + headers: dict | None = None, + data: bytes | dict | None = None, + stream: bool = False, ) -> requests.Response: """ Make a request to the plugin daemon inner API. @@ -29,7 +34,7 @@ class BasePluginManager: return response def _stream_request( - self, method: str, path: str, headers: dict | None = None, data: bytes | None = None + self, method: str, path: str, headers: dict | None = None, data: bytes | dict | None = None ) -> Generator[bytes, None, None]: """ Make a stream request to the plugin daemon inner API @@ -43,7 +48,7 @@ class BasePluginManager: path: str, type: type[T], headers: dict | None = None, - data: bytes | None = None, + data: bytes | dict | None = None, ) -> Generator[T, None, None]: """ Make a stream request to the plugin daemon inner API and yield the response as a model. @@ -61,7 +66,7 @@ class BasePluginManager: return type(**response.json()) def _request_with_plugin_daemon_response( - self, method: str, path: str, type: type[T], headers: dict | None = None, data: bytes | None = None + self, method: str, path: str, type: type[T], headers: dict | None = None, data: bytes | dict | None = None ) -> T: """ Make a request to the plugin daemon inner API and return the response as a model. @@ -72,11 +77,11 @@ class BasePluginManager: raise ValueError(f"got error from plugin daemon: {rep.message}, code: {rep.code}") if rep.data is None: raise ValueError("got empty data from plugin daemon") - + return rep.data - + def _request_with_plugin_daemon_response_stream( - self, method: str, path: str, type: type[T], headers: dict | None = None, data: bytes | None = None + self, method: str, path: str, type: type[T], headers: dict | None = None, data: bytes | dict | None = None ) -> Generator[T, None, None]: """ Make a stream request to the plugin daemon inner API and yield the response as a model. @@ -85,7 +90,7 @@ class BasePluginManager: line_data = json.loads(line) rep = PluginDaemonBasicResponse[type](**line_data) if rep.code != 0: - raise Exception(f"got error from plugin daemon: {rep.message}, code: {rep.code}") + raise ValueError(f"got error from plugin daemon: {rep.message}, code: {rep.code}") if rep.data is None: - raise Exception("got empty data from plugin daemon") - yield rep.data \ No newline at end of file + raise ValueError("got empty data from plugin daemon") + yield rep.data diff --git a/api/core/plugin/manager/plugin.py b/api/core/plugin/manager/plugin.py new file mode 100644 index 0000000000..85e865bb93 --- /dev/null +++ b/api/core/plugin/manager/plugin.py @@ -0,0 +1,46 @@ +from collections.abc import Generator +from urllib.parse import quote + +from core.plugin.entities.plugin_daemon import InstallPluginMessage +from core.plugin.manager.base import BasePluginManager + + +class PluginInstallationManager(BasePluginManager): + def fetch_plugin_by_identifier(self, tenant_id: str, identifier: str) -> bool: + # urlencode the identifier + + identifier = quote(identifier) + return self._request_with_plugin_daemon_response( + "GET", f"/plugin/{tenant_id}/fetch/identifier?plugin_unique_identifier={identifier}", bool + ) + + def install_from_pkg(self, tenant_id: str, pkg: bytes) -> Generator[InstallPluginMessage, None, None]: + """ + Install a plugin from a package. + """ + # using multipart/form-data to encode body + body = {"dify_pkg": ("dify_pkg", pkg, "application/octet-stream")} + + return self._request_with_plugin_daemon_response_stream( + "POST", f"/plugin/{tenant_id}/install/pkg", InstallPluginMessage, data=body + ) + + def install_from_identifier(self, tenant_id: str, identifier: str) -> bool: + """ + Install a plugin from an identifier. + """ + identifier = quote(identifier) + # exception will be raised if the request failed + self._request_with_plugin_daemon_response( + "POST", + f"/plugin/{tenant_id}/install/identifier", + dict, + headers={ + "Content-Type": "application/json", + }, + data={ + "plugin_unique_identifier": identifier, + }, + ) + + return True