diff --git a/api/.env.example b/api/.env.example index 573c8bf90c..bdea20c5ff 100644 --- a/api/.env.example +++ b/api/.env.example @@ -238,3 +238,6 @@ WORKFLOW_CALL_MAX_DEPTH=5 # App configuration APP_MAX_EXECUTION_TIME=1200 +# Plugin configuration +PLUGIN_INNER_API_URL=http://127.0.0.1:5002 +PLUGIN_INNER_API_KEY=lYkiYYT6owG+71oLerGzA7GXCgOT++6ovaezWAjpCjf+Sjc3ZtU+qUEi diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index bd0ef983c4..2012b56c62 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -47,6 +47,19 @@ class CodeExecutionSandboxConfig(BaseSettings): default='dify-sandbox', ) +class PluginConfig(BaseSettings): + """ + Plugin configs + """ + PLUGIN_INNER_API_URL: str = Field( + description='Plugin inner API URL', + default='http://plugin:8194', + ) + + PLUGIN_INNER_API_KEY: str = Field( + description='Plugin inner API key', + default='dify-inner-api-key', + ) class EndpointConfig(BaseSettings): """ @@ -431,6 +444,7 @@ class FeatureConfig( AppExecutionConfig, BillingConfig, CodeExecutionSandboxConfig, + PluginConfig, DataSetConfig, EndpointConfig, FileAccessConfig, diff --git a/api/controllers/inner_api/__init__.py b/api/controllers/inner_api/__init__.py index ad49a649ca..32770aa0de 100644 --- a/api/controllers/inner_api/__init__.py +++ b/api/controllers/inner_api/__init__.py @@ -5,5 +5,5 @@ from libs.external_api import ExternalApi bp = Blueprint('inner_api', __name__, url_prefix='/inner/api') api = ExternalApi(bp) +from .plugin import plugin from .workspace import workspace - diff --git a/api/controllers/inner_api/plugin/__init__.py b/api/controllers/inner_api/plugin/__init__.py new file mode 100644 index 0000000000..fd1918e6b2 --- /dev/null +++ b/api/controllers/inner_api/plugin/__init__.py @@ -0,0 +1 @@ +from .plugin import * \ No newline at end of file diff --git a/api/controllers/inner_api/plugin/plugin.py b/api/controllers/inner_api/plugin/plugin.py new file mode 100644 index 0000000000..6ce860b877 --- /dev/null +++ b/api/controllers/inner_api/plugin/plugin.py @@ -0,0 +1,59 @@ + +from flask_restful import Resource, reqparse + +from controllers.console.setup import setup_required +from controllers.inner_api import api +from controllers.inner_api.plugin.wraps import get_tenant +from controllers.inner_api.wraps import plugin_inner_api_only +from libs.helper import compact_generate_response +from models.account import Tenant +from services.plugin.plugin_invoke_service import PluginInvokeService + + +class PluginInvokeModelApi(Resource): + @setup_required + @plugin_inner_api_only + @get_tenant + def post(self, user_id: str, tenant_model: Tenant): + parser = reqparse.RequestParser() + parser.add_argument('provider', type=dict, required=True, location='json') + parser.add_argument('model', type=dict, required=True, location='json') + parser.add_argument('parameters', type=dict, required=True, location='json') + + args = parser.parse_args() + + +class PluginInvokeToolApi(Resource): + @setup_required + @plugin_inner_api_only + @get_tenant + def post(self, user_id: str, tenant_model: Tenant): + parser = reqparse.RequestParser() + parser.add_argument('provider', type=dict, required=True, location='json') + parser.add_argument('tool', type=dict, required=True, location='json') + parser.add_argument('parameters', type=dict, required=True, location='json') + + args = parser.parse_args() + + response = PluginInvokeService.invoke_tool(user_id, tenant_model, + args['provider'], args['tool'], + args['parameters']) + return compact_generate_response(response) + + +class PluginInvokeNodeApi(Resource): + @setup_required + @plugin_inner_api_only + @get_tenant + def post(self, user_id: str, tenant_model: Tenant): + parser = reqparse.RequestParser() + args = parser.parse_args() + + return { + 'message': 'success' + } + + +api.add_resource(PluginInvokeModelApi, '/invoke/model') +api.add_resource(PluginInvokeToolApi, '/invoke/tool') +api.add_resource(PluginInvokeNodeApi, '/invoke/node') diff --git a/api/controllers/inner_api/plugin/wraps.py b/api/controllers/inner_api/plugin/wraps.py new file mode 100644 index 0000000000..d604e84a9d --- /dev/null +++ b/api/controllers/inner_api/plugin/wraps.py @@ -0,0 +1,47 @@ +from collections.abc import Callable +from functools import wraps +from typing import Optional + +from flask_restful import reqparse + +from extensions.ext_database import db +from models.account import Tenant + + +def get_tenant(view: Optional[Callable] = None): + def decorator(view_func): + @wraps(view_func) + def decorated_view(*args, **kwargs): + # fetch json body + parser = reqparse.RequestParser() + parser.add_argument('tenant_id', type=str, required=True, location='json') + parser.add_argument('user_id', type=str, required=True, location='json') + + kwargs = parser.parse_args() + + user_id = kwargs.get('user_id') + tenant_id = kwargs.get('tenant_id') + + del kwargs['tenant_id'] + del kwargs['user_id'] + + try: + tenant_model = db.session.query(Tenant).filter( + Tenant.id == tenant_id, + ).first() + except Exception: + raise ValueError('tenant not found') + + if not tenant_model: + raise ValueError('tenant not found') + + kwargs['tenant_model'] = tenant_model + kwargs['user_id'] = user_id + + return view_func(*args, **kwargs) + return decorated_view + + if view is None: + return decorator + else: + return decorator(view) diff --git a/api/controllers/inner_api/workspace/workspace.py b/api/controllers/inner_api/workspace/workspace.py index 06610d8933..791d37f868 100644 --- a/api/controllers/inner_api/workspace/workspace.py +++ b/api/controllers/inner_api/workspace/workspace.py @@ -2,7 +2,7 @@ from flask_restful import Resource, reqparse from controllers.console.setup import setup_required from controllers.inner_api import api -from controllers.inner_api.wraps import inner_api_only +from controllers.inner_api.wraps import enterprise_inner_api_only from events.tenant_event import tenant_was_created from models.account import Account from services.account_service import TenantService @@ -11,7 +11,7 @@ from services.account_service import TenantService class EnterpriseWorkspace(Resource): @setup_required - @inner_api_only + @enterprise_inner_api_only def post(self): parser = reqparse.RequestParser() parser.add_argument('name', type=str, required=True, location='json') diff --git a/api/controllers/inner_api/wraps.py b/api/controllers/inner_api/wraps.py index 07cd38bc85..0793a67eec 100644 --- a/api/controllers/inner_api/wraps.py +++ b/api/controllers/inner_api/wraps.py @@ -5,11 +5,12 @@ from hmac import new as hmac_new from flask import abort, current_app, request +from configs import dify_config from extensions.ext_database import db from models.model import EndUser -def inner_api_only(view): +def enterprise_inner_api_only(view): @wraps(view) def decorated(*args, **kwargs): if not current_app.config['INNER_API']: @@ -25,7 +26,7 @@ def inner_api_only(view): return decorated -def inner_api_user_auth(view): +def enterprise_inner_api_user_auth(view): @wraps(view) def decorated(*args, **kwargs): if not current_app.config['INNER_API']: @@ -59,3 +60,18 @@ def inner_api_user_auth(view): return view(*args, **kwargs) return decorated + +def plugin_inner_api_only(view): + @wraps(view) + def decorated(*args, **kwargs): + if not dify_config.PLUGIN_INNER_API_KEY: + abort(404) + + # get header 'X-Inner-Api-Key' + inner_api_key = request.headers.get('X-Inner-Api-Key') + if not inner_api_key or inner_api_key != dify_config.PLUGIN_INNER_API_KEY: + abort(404) + + return view(*args, **kwargs) + + return decorated \ No newline at end of file diff --git a/api/services/plugin/plugin_invoke_service.py b/api/services/plugin/plugin_invoke_service.py new file mode 100644 index 0000000000..131de1ec1a --- /dev/null +++ b/api/services/plugin/plugin_invoke_service.py @@ -0,0 +1,16 @@ +from collections.abc import Generator +from typing import Any + +from core.tools.entities.tool_entities import ToolInvokeMessage +from models.account import Tenant + + +class PluginInvokeService: + @classmethod + def invoke_tool(cls, user_id: str, tenant: Tenant, + tool_provider: str, tool_name: str, + tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]: + """ + Invokes a tool with the given user ID and tool parameters. + """ + \ No newline at end of file