From f421d1975ddfe1a98884838edccdbc4e80af4c38 Mon Sep 17 00:00:00 2001 From: shaohuzhang1 Date: Thu, 15 Aug 2024 17:17:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=87=BD=E6=95=B0=E5=BA=93=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/application/flow/step_node/__init__.py | 5 +- .../step_node/function_lib_node/__init__.py | 9 + .../function_lib_node/i_function_lib_node.py | 42 ++++ .../function_lib_node/impl/__init__.py | 9 + .../impl/base_function_lib_node.py | 92 +++++++ .../flow/step_node/function_node/__init__.py | 9 + .../function_node/i_function_node.py | 60 +++++ .../step_node/function_node/impl/__init__.py | 9 + .../function_node/impl/base_function_node.py | 75 ++++++ apps/application/flow/workflow_manage.py | 2 +- apps/common/field/common.py | 15 ++ apps/common/util/function_code.py | 93 ++++++++ .../dataset/serializers/common_serializers.py | 2 +- apps/function_lib/__init__.py | 0 apps/function_lib/admin.py | 3 + apps/function_lib/apps.py | 6 + apps/function_lib/migrations/0001_initial.py | 34 +++ apps/function_lib/migrations/__init__.py | 0 apps/function_lib/models/__init__.py | 8 + apps/function_lib/models/function.py | 29 +++ .../serializers/function_lib_serializer.py | 191 +++++++++++++++ .../swagger_api/function_lib_api.py | 168 +++++++++++++ apps/function_lib/tests.py | 3 + apps/function_lib/urls.py | 12 + apps/function_lib/views/__init__.py | 9 + apps/function_lib/views/function_lib_views.py | 91 +++++++ apps/smartdoc/conf.py | 3 +- apps/smartdoc/settings/base.py | 3 +- apps/smartdoc/urls.py | 5 +- installer/Dockerfile | 5 +- pyproject.toml | 5 +- ui/package.json | 1 + ui/src/api/function-lib.ts | 82 +++++++ ui/src/api/type/function-lib.ts | 9 + ui/src/assets/icon_function_outlined.svg | 12 + .../ai-chat/ExecutionDetailDialog.vue | 21 ++ ui/src/components/codemirror-editor/index.vue | 24 ++ ui/src/components/index.ts | 2 + ui/src/enums/workflow.ts | 4 +- ui/src/request/index.ts | 5 +- ui/src/router/modules/function-lib.ts | 17 ++ ui/src/styles/app.scss | 4 + ui/src/styles/element-plus.scss | 20 +- ui/src/views/application-workflow/index.vue | 105 ++++++-- .../component/FieldFormDialog.vue | 109 +++++++++ .../component/FunctionDebugDrawer.vue | 150 ++++++++++++ .../component/FunctionFormDrawer.vue | 224 ++++++++++++++++++ ui/src/views/function-lib/index.vue | 142 +++++++++++ ui/src/views/template/component/ModelCard.vue | 6 +- ui/src/workflow/common/data.ts | 40 +++- ui/src/workflow/common/validate.ts | 7 +- .../workflow/icons/function-lib-node-icon.vue | 6 + ui/src/workflow/icons/function-node-icon.vue | 6 + .../workflow/nodes/function-lib-node/index.ts | 12 + .../nodes/function-lib-node/index.vue | 121 ++++++++++ ui/src/workflow/nodes/function-node/index.ts | 12 + ui/src/workflow/nodes/function-node/index.vue | 200 ++++++++++++++++ 57 files changed, 2299 insertions(+), 39 deletions(-) create mode 100644 apps/application/flow/step_node/function_lib_node/__init__.py create mode 100644 apps/application/flow/step_node/function_lib_node/i_function_lib_node.py create mode 100644 apps/application/flow/step_node/function_lib_node/impl/__init__.py create mode 100644 apps/application/flow/step_node/function_lib_node/impl/base_function_lib_node.py create mode 100644 apps/application/flow/step_node/function_node/__init__.py create mode 100644 apps/application/flow/step_node/function_node/i_function_node.py create mode 100644 apps/application/flow/step_node/function_node/impl/__init__.py create mode 100644 apps/application/flow/step_node/function_node/impl/base_function_node.py create mode 100644 apps/common/util/function_code.py create mode 100644 apps/function_lib/__init__.py create mode 100644 apps/function_lib/admin.py create mode 100644 apps/function_lib/apps.py create mode 100644 apps/function_lib/migrations/0001_initial.py create mode 100644 apps/function_lib/migrations/__init__.py create mode 100644 apps/function_lib/models/__init__.py create mode 100644 apps/function_lib/models/function.py create mode 100644 apps/function_lib/serializers/function_lib_serializer.py create mode 100644 apps/function_lib/swagger_api/function_lib_api.py create mode 100644 apps/function_lib/tests.py create mode 100644 apps/function_lib/urls.py create mode 100644 apps/function_lib/views/__init__.py create mode 100644 apps/function_lib/views/function_lib_views.py create mode 100644 ui/src/api/function-lib.ts create mode 100644 ui/src/api/type/function-lib.ts create mode 100644 ui/src/assets/icon_function_outlined.svg create mode 100644 ui/src/components/codemirror-editor/index.vue create mode 100644 ui/src/router/modules/function-lib.ts create mode 100644 ui/src/views/function-lib/component/FieldFormDialog.vue create mode 100644 ui/src/views/function-lib/component/FunctionDebugDrawer.vue create mode 100644 ui/src/views/function-lib/component/FunctionFormDrawer.vue create mode 100644 ui/src/views/function-lib/index.vue create mode 100644 ui/src/workflow/icons/function-lib-node-icon.vue create mode 100644 ui/src/workflow/icons/function-node-icon.vue create mode 100644 ui/src/workflow/nodes/function-lib-node/index.ts create mode 100644 ui/src/workflow/nodes/function-lib-node/index.vue create mode 100644 ui/src/workflow/nodes/function-node/index.ts create mode 100644 ui/src/workflow/nodes/function-node/index.vue diff --git a/apps/application/flow/step_node/__init__.py b/apps/application/flow/step_node/__init__.py index b3692fa8b..1d5af03ca 100644 --- a/apps/application/flow/step_node/__init__.py +++ b/apps/application/flow/step_node/__init__.py @@ -12,8 +12,11 @@ from .question_node import * from .search_dataset_node import * from .start_node import * from .direct_reply_node import * +from .function_lib_node import * +from .function_node import * -node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode, BaseConditionNode, BaseReplyNode] +node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode, BaseConditionNode, BaseReplyNode, + BaseFunctionNodeNode, BaseFunctionLibNodeNode] def get_node(node_type): diff --git a/apps/application/flow/step_node/function_lib_node/__init__.py b/apps/application/flow/step_node/function_lib_node/__init__.py new file mode 100644 index 000000000..7422965c3 --- /dev/null +++ b/apps/application/flow/step_node/function_lib_node/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: __init__.py + @date:2024/8/8 17:45 + @desc: +""" +from .impl import * \ No newline at end of file diff --git a/apps/application/flow/step_node/function_lib_node/i_function_lib_node.py b/apps/application/flow/step_node/function_lib_node/i_function_lib_node.py new file mode 100644 index 000000000..0d1d2b828 --- /dev/null +++ b/apps/application/flow/step_node/function_lib_node/i_function_lib_node.py @@ -0,0 +1,42 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: i_function_lib_node.py + @date:2024/8/8 16:21 + @desc: +""" +from typing import Type + +from rest_framework import serializers + +from application.flow.i_step_node import INode, NodeResult +from common.field.common import ObjectField +from common.util.field_message import ErrMessage + + +class InputField(serializers.Serializer): + name = serializers.CharField(required=True, error_messages=ErrMessage.char('变量名')) + value = ObjectField(required=True, error_messages=ErrMessage.char("变量值"), model_type_list=[str, list]) + + +class FunctionLibNodeParamsSerializer(serializers.Serializer): + function_lib_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid('函数库id')) + input_field_list = InputField(required=True, many=True) + is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean('是否返回内容')) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + + +class IFunctionLibNode(INode): + type = 'function-lib-node' + + def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: + return FunctionLibNodeParamsSerializer + + def _run(self): + return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data) + + def execute(self, function_lib_id, input_field_list, **kwargs) -> NodeResult: + pass diff --git a/apps/application/flow/step_node/function_lib_node/impl/__init__.py b/apps/application/flow/step_node/function_lib_node/impl/__init__.py new file mode 100644 index 000000000..96681474f --- /dev/null +++ b/apps/application/flow/step_node/function_lib_node/impl/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: __init__.py + @date:2024/8/8 17:48 + @desc: +""" +from .base_function_lib_node import BaseFunctionLibNodeNode diff --git a/apps/application/flow/step_node/function_lib_node/impl/base_function_lib_node.py b/apps/application/flow/step_node/function_lib_node/impl/base_function_lib_node.py new file mode 100644 index 000000000..31c1ab3d3 --- /dev/null +++ b/apps/application/flow/step_node/function_lib_node/impl/base_function_lib_node.py @@ -0,0 +1,92 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: base_function_lib_node.py + @date:2024/8/8 17:49 + @desc: +""" +import json +import time +from typing import Dict + +from django.db.models import QuerySet + +from application.flow.i_step_node import NodeResult +from application.flow.step_node.function_lib_node.i_function_lib_node import IFunctionLibNode +from common.exception.app_exception import AppApiException +from common.util.function_code import FunctionExecutor +from function_lib.models.function import FunctionLib +from smartdoc.const import CONFIG + +function_executor = FunctionExecutor(CONFIG.get('SANDBOX')) + + +def write_context(step_variable: Dict, global_variable: Dict, node, workflow): + if step_variable is not None: + for key in step_variable: + node.context[key] = step_variable[key] + if workflow.is_result() and 'result' in step_variable: + result = step_variable['result'] + '\n' + yield result + workflow.answer += result + node.context['run_time'] = time.time() - node.context['start_time'] + + +def get_field_value(debug_field_list, name, is_required): + result = [field for field in debug_field_list if field.get('name') == name] + if len(result) > 0: + return result[-1]['value'] + if is_required: + raise AppApiException(500, f"{name}字段未设置值") + return None + + +def convert_value(name: str, value, _type, is_required, source, node): + if not is_required and value is None: + return None + if source == 'reference': + value = node.workflow_manage.get_reference_field( + value[0], + value[1:]) + return value + try: + if _type == 'int': + return int(value) + if _type == 'float': + return float(value) + if _type == 'dict': + return json.loads(value) + if _type == 'array': + return json.loads(value) + return value + except Exception as e: + raise AppApiException(500, f'字段:{name}类型:{_type}值:{value}类型转换错误') + + +class BaseFunctionLibNodeNode(IFunctionLibNode): + def execute(self, function_lib_id, input_field_list, **kwargs) -> NodeResult: + function_lib = QuerySet(FunctionLib).filter(id=function_lib_id).first() + params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'), + field.get('is_required'), + field.get('source'), self) + for field in + [{'value': get_field_value(input_field_list, field.get('name'), field.get('is_required'), + ), **field} + for field in + function_lib.input_field_list]} + self.context['params'] = params + result = function_executor.exec_code(function_lib.code, params) + return NodeResult({'result': result}, {}, _write_context=write_context) + + def get_details(self, index: int, **kwargs): + return { + 'name': self.node.properties.get('stepName'), + "index": index, + "result": self.context.get('result'), + "params": self.context.get('params'), + 'run_time': self.context.get('run_time'), + 'type': self.node.type, + 'status': self.status, + 'err_message': self.err_message + } diff --git a/apps/application/flow/step_node/function_node/__init__.py b/apps/application/flow/step_node/function_node/__init__.py new file mode 100644 index 000000000..ebfbe8d8b --- /dev/null +++ b/apps/application/flow/step_node/function_node/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: __init__.py.py + @date:2024/8/13 10:43 + @desc: +""" +from .impl import * \ No newline at end of file diff --git a/apps/application/flow/step_node/function_node/i_function_node.py b/apps/application/flow/step_node/function_node/i_function_node.py new file mode 100644 index 000000000..30e6c96e9 --- /dev/null +++ b/apps/application/flow/step_node/function_node/i_function_node.py @@ -0,0 +1,60 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: i_function_lib_node.py + @date:2024/8/8 16:21 + @desc: +""" +import re +from typing import Type + +from django.core import validators +from rest_framework import serializers + +from application.flow.i_step_node import INode, NodeResult +from common.exception.app_exception import AppApiException +from common.field.common import ObjectField +from common.util.field_message import ErrMessage + + +class InputField(serializers.Serializer): + name = serializers.CharField(required=True, error_messages=ErrMessage.char('变量名')) + is_required = serializers.BooleanField(required=True, error_messages=ErrMessage.boolean("是否必填")) + type = serializers.CharField(required=True, error_messages=ErrMessage.char("类型"), validators=[ + validators.RegexValidator(regex=re.compile("^string|int|dict|array|float$"), + message="字段只支持string|int|dict|array|float", code=500) + ]) + source = serializers.CharField(required=True, error_messages=ErrMessage.char("来源"), validators=[ + validators.RegexValidator(regex=re.compile("^custom|reference$"), + message="字段只支持custom|reference", code=500) + ]) + value = ObjectField(required=True, error_messages=ErrMessage.char("变量值"), model_type_list=[str, list]) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + is_required = self.data.get('is_required') + if is_required and self.data.get('value') is None: + raise AppApiException(500, f'{self.data.get("name")}必填') + + +class FunctionNodeParamsSerializer(serializers.Serializer): + input_field_list = InputField(required=True, many=True) + code = serializers.CharField(required=True, error_messages=ErrMessage.char("函数")) + is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean('是否返回内容')) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + + +class IFunctionNode(INode): + type = 'function-node' + + def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: + return FunctionNodeParamsSerializer + + def _run(self): + return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data) + + def execute(self, input_field_list, code, **kwargs) -> NodeResult: + pass diff --git a/apps/application/flow/step_node/function_node/impl/__init__.py b/apps/application/flow/step_node/function_node/impl/__init__.py new file mode 100644 index 000000000..1a096368f --- /dev/null +++ b/apps/application/flow/step_node/function_node/impl/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: __init__.py.py + @date:2024/8/13 11:19 + @desc: +""" +from .base_function_node import BaseFunctionNodeNode diff --git a/apps/application/flow/step_node/function_node/impl/base_function_node.py b/apps/application/flow/step_node/function_node/impl/base_function_node.py new file mode 100644 index 000000000..fe0bdb3f8 --- /dev/null +++ b/apps/application/flow/step_node/function_node/impl/base_function_node.py @@ -0,0 +1,75 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: base_function_lib_node.py + @date:2024/8/8 17:49 + @desc: +""" +import json +import time + +from typing import Dict + +from application.flow.i_step_node import NodeResult +from application.flow.step_node.function_node.i_function_node import IFunctionNode +from common.exception.app_exception import AppApiException +from common.util.function_code import FunctionExecutor +from smartdoc.const import CONFIG + +function_executor = FunctionExecutor(CONFIG.get('SANDBOX')) + + +def write_context(step_variable: Dict, global_variable: Dict, node, workflow): + if step_variable is not None: + for key in step_variable: + node.context[key] = step_variable[key] + if workflow.is_result() and 'result' in step_variable: + result = step_variable['result'] + '\n' + yield result + workflow.answer += result + node.context['run_time'] = time.time() - node.context['start_time'] + + +def convert_value(name: str, value, _type, is_required, source, node): + if not is_required and value is None: + return None + if source == 'reference': + value = node.workflow_manage.get_reference_field( + value[0], + value[1:]) + return value + try: + if _type == 'int': + return int(value) + if _type == 'float': + return float(value) + if _type == 'dict': + return json.loads(value) + if _type == 'array': + return json.loads(value) + return value + except Exception as e: + raise AppApiException(500, f'字段:{name}类型:{_type}值:{value}类型转换错误') + + +class BaseFunctionNodeNode(IFunctionNode): + def execute(self, input_field_list, code, **kwargs) -> NodeResult: + params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'), + field.get('is_required'), field.get('source'), self) + for field in input_field_list} + result = function_executor.exec_code(code, params) + self.context['params'] = params + return NodeResult({'result': result}, {}, _write_context=write_context) + + def get_details(self, index: int, **kwargs): + return { + 'name': self.node.properties.get('stepName'), + "index": index, + "result": self.context.get('result'), + "params": self.context.get('params'), + 'run_time': self.context.get('run_time'), + 'type': self.node.type, + 'status': self.status, + 'err_message': self.err_message + } diff --git a/apps/application/flow/workflow_manage.py b/apps/application/flow/workflow_manage.py index cfc0d6404..7e38d2192 100644 --- a/apps/application/flow/workflow_manage.py +++ b/apps/application/flow/workflow_manage.py @@ -40,7 +40,7 @@ class Node: self.__setattr__(keyword, kwargs.get(keyword)) -end_nodes = ['ai-chat-node', 'reply-node'] +end_nodes = ['ai-chat-node', 'reply-node', 'function-node', 'function-lib-node'] class Flow: diff --git a/apps/common/field/common.py b/apps/common/field/common.py index b64277560..3025ec5d8 100644 --- a/apps/common/field/common.py +++ b/apps/common/field/common.py @@ -9,6 +9,21 @@ from rest_framework import serializers +class ObjectField(serializers.Field): + def __init__(self, model_type_list, **kwargs): + self.model_type_list = model_type_list + super().__init__(**kwargs) + + def to_internal_value(self, data): + for model_type in self.model_type_list: + if isinstance(data, model_type): + return data + self.fail('message类型错误', value=data) + + def to_representation(self, value): + return value + + class InstanceField(serializers.Field): def __init__(self, model_type, **kwargs): self.model_type = model_type diff --git a/apps/common/util/function_code.py b/apps/common/util/function_code.py new file mode 100644 index 000000000..9aeb66c09 --- /dev/null +++ b/apps/common/util/function_code.py @@ -0,0 +1,93 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: function_code.py + @date:2024/8/7 16:11 + @desc: +""" +import os +import subprocess +import sys +import time +import uuid +from textwrap import dedent + +from diskcache import Cache + +from smartdoc.const import PROJECT_DIR + +python_directory = sys.executable + + +class FunctionExecutor: + def __init__(self, sandbox=False): + self.sandbox = sandbox + if sandbox: + self.sandbox_path = '/opt/maxkb/app/sandbox' + self.user = 'sandbox' + else: + self.sandbox_path = os.path.join(PROJECT_DIR, 'data', 'sandbox') + self.user = None + self._createdir() + if self.sandbox: + os.system(f"chown -R {self.user}:{self.user} {self.sandbox_path}") + + def _createdir(self): + old_mask = os.umask(0o077) + try: + os.makedirs(self.sandbox_path, 0o700, exist_ok=True) + finally: + os.umask(old_mask) + + def exec_code(self, code_str, keywords): + _id = str(uuid.uuid1()) + success = '{"code":200,"msg":"成功","data":exec_result}' + err = '{"code":500,"msg":str(e),"data":None}' + path = r'' + self.sandbox_path + '' + _exec_code = f""" +try: + locals_v={'{}'} + keywords={keywords} + globals_v=globals() + exec({dedent(code_str)!a}, globals_v, locals_v) + f_name, f = locals_v.popitem() + for local in locals_v: + globals_v[local] = locals_v[local] + exec_result=f(**keywords) + from diskcache import Cache + cache = Cache({path!a}) + cache.set({_id!a},{success}) +except Exception as e: + from diskcache import Cache + cache = Cache({path!a}) + cache.set({_id!a},{err}) +""" + if self.sandbox: + subprocess_result = self._exec_sandbox(_exec_code, _id) + else: + subprocess_result = self._exec(_exec_code) + if subprocess_result.returncode == 1: + raise Exception(subprocess_result.stderr) + cache = Cache(self.sandbox_path) + result = cache.get(_id) + cache.delete(_id) + if result.get('code') == 200: + return result.get('data') + raise Exception(result.get('msg')) + + def _exec_sandbox(self, _code, _id): + exec_python_file = f'{self.sandbox_path}/{_id}.py' + with open(exec_python_file, 'w') as file: + file.write(_code) + os.system(f"chown {self.user}:{self.user} {exec_python_file}") + subprocess_result = subprocess.run( + ['su', '-c', python_directory + ' ' + exec_python_file, self.user], + text=True, + capture_output=True) + os.remove(exec_python_file) + return subprocess_result + + @staticmethod + def _exec(_code): + return subprocess.run([python_directory, '-c', _code], text=True, capture_output=True) diff --git a/apps/dataset/serializers/common_serializers.py b/apps/dataset/serializers/common_serializers.py index 06120454c..51b4afefa 100644 --- a/apps/dataset/serializers/common_serializers.py +++ b/apps/dataset/serializers/common_serializers.py @@ -146,7 +146,7 @@ def get_embedding_model_by_dataset_id_list(dataset_id_list: List): def get_embedding_model_by_dataset_id(dataset_id: str): dataset = QuerySet(DataSet).select_related('embedding_mode').filter(id=dataset_id).first() - return ModelManage.get_model(dataset.embedding_mode_id, lambda _id: get_model(dataset.embedding_mode)) + return ModelManage.get_model(str(dataset.embedding_mode_id), lambda _id: get_model(dataset.embedding_mode)) def get_embedding_model_by_dataset(dataset): diff --git a/apps/function_lib/__init__.py b/apps/function_lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/function_lib/admin.py b/apps/function_lib/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/apps/function_lib/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/function_lib/apps.py b/apps/function_lib/apps.py new file mode 100644 index 000000000..11957d6cf --- /dev/null +++ b/apps/function_lib/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FunctionLibConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'function_lib' diff --git a/apps/function_lib/migrations/0001_initial.py b/apps/function_lib/migrations/0001_initial.py new file mode 100644 index 000000000..bb2fd60e9 --- /dev/null +++ b/apps/function_lib/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.15 on 2024-08-13 10:04 + +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('users', '0004_alter_user_email'), + ] + + operations = [ + migrations.CreateModel( + name='FunctionLib', + fields=[ + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), + ('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), + ('name', models.CharField(max_length=64, verbose_name='函数名称')), + ('desc', models.CharField(max_length=128, verbose_name='描述')), + ('code', models.CharField(max_length=102400, verbose_name='python代码')), + ('input_field_list', django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(default=dict, verbose_name='输入字段'), default=list, size=None, verbose_name='输入字段列表')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='用户id')), + ], + options={ + 'db_table': 'function_lib', + }, + ), + ] diff --git a/apps/function_lib/migrations/__init__.py b/apps/function_lib/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/function_lib/models/__init__.py b/apps/function_lib/models/__init__.py new file mode 100644 index 000000000..a68550e90 --- /dev/null +++ b/apps/function_lib/models/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: __init__.py.py + @date:2024/8/2 14:55 + @desc: +""" diff --git a/apps/function_lib/models/function.py b/apps/function_lib/models/function.py new file mode 100644 index 000000000..d41c6e9bb --- /dev/null +++ b/apps/function_lib/models/function.py @@ -0,0 +1,29 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: function_lib.py + @date:2024/8/2 14:59 + @desc: +""" +import uuid + +from django.contrib.postgres.fields import ArrayField +from django.db import models + +from common.mixins.app_model_mixin import AppModelMixin +from users.models import User + + +class FunctionLib(AppModelMixin): + id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id") + user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户id") + name = models.CharField(max_length=64, verbose_name="函数名称") + desc = models.CharField(max_length=128, verbose_name="描述") + code = models.CharField(max_length=102400, verbose_name="python代码") + input_field_list = ArrayField(verbose_name="输入字段列表", + base_field=models.JSONField(verbose_name="输入字段", default=dict) + , default=list) + + class Meta: + db_table = "function_lib" diff --git a/apps/function_lib/serializers/function_lib_serializer.py b/apps/function_lib/serializers/function_lib_serializer.py new file mode 100644 index 000000000..1000c2fa9 --- /dev/null +++ b/apps/function_lib/serializers/function_lib_serializer.py @@ -0,0 +1,191 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: function_lib_serializer.py + @date:2024/8/2 17:35 + @desc: +""" +import json +import re +import uuid + +from django.core import validators +from django.db.models import QuerySet +from rest_framework import serializers + +from common.db.search import page_search +from common.exception.app_exception import AppApiException +from common.util.field_message import ErrMessage +from common.util.function_code import FunctionExecutor +from function_lib.models.function import FunctionLib +from smartdoc.const import CONFIG + +function_executor = FunctionExecutor(CONFIG.get('SANDBOX')) + + +class FunctionLibModelSerializer(serializers.ModelSerializer): + class Meta: + model = FunctionLib + fields = ['id', 'name', 'desc', 'code', 'input_field_list', + 'create_time', 'update_time'] + + +class FunctionLibInputField(serializers.Serializer): + name = serializers.CharField(required=True, error_messages=ErrMessage.char('变量名')) + is_required = serializers.BooleanField(required=True, error_messages=ErrMessage.boolean("是否必填")) + type = serializers.CharField(required=True, error_messages=ErrMessage.char("类型"), validators=[ + validators.RegexValidator(regex=re.compile("^string|int|dict|array|float$"), + message="字段只支持string|int|dict|array|float", code=500) + ]) + source = serializers.CharField(required=True, error_messages=ErrMessage.char("来源"), validators=[ + validators.RegexValidator(regex=re.compile("^custom|reference$"), + message="字段只支持custom|reference", code=500) + ]) + + +class DebugField(serializers.Serializer): + name = serializers.CharField(required=True, error_messages=ErrMessage.char('变量名')) + value = serializers.CharField(required=False, allow_blank=True, allow_null=True, + error_messages=ErrMessage.char("变量值")) + + +class DebugInstance(serializers.Serializer): + debug_field_list = DebugField(required=True, many=True) + input_field_list = FunctionLibInputField(required=True, many=True) + code = serializers.CharField(required=True, error_messages=ErrMessage.char("函数内容")) + + +class EditFunctionLib(serializers.Serializer): + name = serializers.CharField(required=False, error_messages=ErrMessage.char("函数名称")) + + desc = serializers.CharField(required=False, error_messages=ErrMessage.char("函数描述")) + + code = serializers.CharField(required=False, error_messages=ErrMessage.char("函数内容")) + + input_field_list = FunctionLibInputField(required=False, many=True) + + +class CreateFunctionLib(serializers.Serializer): + name = serializers.CharField(required=True, error_messages=ErrMessage.char("函数名称")) + + desc = serializers.CharField(required=False, allow_null=True, allow_blank=True, + error_messages=ErrMessage.char("函数描述")) + + code = serializers.CharField(required=True, error_messages=ErrMessage.char("函数内容")) + + input_field_list = FunctionLibInputField(required=True, many=True) + + +class FunctionLibSerializer(serializers.Serializer): + class Query(serializers.Serializer): + name = serializers.CharField(required=False, allow_null=True, allow_blank=True, + error_messages=ErrMessage.char("函数名称")) + + desc = serializers.CharField(required=False, allow_null=True, allow_blank=True, + error_messages=ErrMessage.char("函数描述")) + + user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id")) + + def get_query_set(self): + query_set = QuerySet(FunctionLib).filter(user_id=self.data.get('user_id')) + if self.data.get('name') is not None: + query_set = query_set.filter(name=self.data.get('name')) + if self.data.get('desc') is not None: + query_set = query_set.filter(name=self.data.get('desc')) + query_set = query_set.order_by("-create_time") + return query_set + + def list(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + return [FunctionLibModelSerializer(item).data for item in self.get_query_set()] + + def page(self, current_page: int, page_size: int, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + return page_search(current_page, page_size, self.get_query_set(), + post_records_handler=lambda row: FunctionLibModelSerializer(row).data) + + class Create(serializers.Serializer): + user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id")) + + def insert(self, instance, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + CreateFunctionLib(data=instance).is_valid(raise_exception=True) + function_lib = FunctionLib(id=uuid.uuid1(), name=instance.get('name'), desc=instance.get('desc'), + code=instance.get('code'), + user_id=self.data.get('user_id'), + input_field_list=instance.get('input_field_list')) + function_lib.save() + return FunctionLibModelSerializer(function_lib).data + + class Debug(serializers.Serializer): + user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id")) + + def debug(self, debug_instance, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + DebugInstance(data=debug_instance).is_valid(raise_exception=True) + input_field_list = debug_instance.get('input_field_list') + code = debug_instance.get('code') + debug_field_list = debug_instance.get('debug_field_list') + params = {field.get('name'): self.convert_value(field.get('name'), field.get('value'), field.get('type'), + field.get('is_required')) + for field in + [{'value': self.get_field_value(debug_field_list, field.get('name'), field.get('is_required')), + **field} for field in + input_field_list]} + return function_executor.exec_code(code, params) + + @staticmethod + def get_field_value(debug_field_list, name, is_required): + result = [field for field in debug_field_list if field.get('name') == name] + if len(result) > 0: + return result[-1].get('value') + if is_required: + raise AppApiException(500, f"{name}字段未设置值") + return None + + @staticmethod + def convert_value(name: str, value: str, _type: str, is_required: bool): + if not is_required and value is None: + return None + try: + if _type == 'int': + return int(value) + if _type == 'float': + return float(value) + if _type == 'dict': + return json.loads(value) + if _type == 'array': + return json.loads(value) + return value + except Exception as e: + raise AppApiException(500, f'字段:{name}类型:{_type}值:{value}类型转换错误') + + class Operate(serializers.Serializer): + id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("函数id")) + user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id")) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + if not QuerySet(FunctionLib).filter(id=self.data.get('id'), user_id=self.data.get('user_id')).exists(): + raise AppApiException(500, '函数不存在') + + def edit(self, instance, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + EditFunctionLib(data=instance).is_valid(raise_exception=True) + edit_field_list = ['name', 'desc', 'code', 'input_field_list'] + edit_dict = {field: instance.get(field) for field in edit_field_list if ( + field in instance and instance.get(field) is not None)} + QuerySet(FunctionLib).filter(id=self.data.get('id')).update(**edit_dict) + return self.one(False) + + def one(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first() + return FunctionLibModelSerializer(function_lib).data diff --git a/apps/function_lib/swagger_api/function_lib_api.py b/apps/function_lib/swagger_api/function_lib_api.py new file mode 100644 index 000000000..ce396c6da --- /dev/null +++ b/apps/function_lib/swagger_api/function_lib_api.py @@ -0,0 +1,168 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: function_lib_api.py + @date:2024/8/2 17:11 + @desc: +""" +from drf_yasg import openapi + +from common.mixins.api_mixin import ApiMixin + + +class FunctionLibApi(ApiMixin): + @staticmethod + def get_response_body_api(): + return openapi.Schema( + type=openapi.TYPE_OBJECT, + required=['id', 'name', 'desc', 'code', 'input_field_list', 'create_time', + 'update_time'], + properties={ + 'id': openapi.Schema(type=openapi.TYPE_STRING, title="", description="主键id"), + 'name': openapi.Schema(type=openapi.TYPE_STRING, title="函数名称", description="函数名称"), + 'desc': openapi.Schema(type=openapi.TYPE_STRING, title="函数描述", description="函数描述"), + 'code': openapi.Schema(type=openapi.TYPE_STRING, title="函数内容", description="函数内容"), + 'input_field_list': openapi.Schema(type=openapi.TYPE_STRING, title="输入字段", description="输入字段"), + 'create_time': openapi.Schema(type=openapi.TYPE_STRING, title="创建时间", description="创建时间"), + 'update_time': openapi.Schema(type=openapi.TYPE_STRING, title="修改时间", description="修改时间"), + } + ) + + class Query(ApiMixin): + @staticmethod + def get_request_params_api(): + return [openapi.Parameter(name='name', + in_=openapi.IN_QUERY, + type=openapi.TYPE_STRING, + required=False, + description='函数名称'), + openapi.Parameter(name='desc', + in_=openapi.IN_QUERY, + type=openapi.TYPE_STRING, + required=False, + description='函数描述') + ] + + class Debug(ApiMixin): + @staticmethod + def get_request_body_api(): + return openapi.Schema( + type=openapi.TYPE_OBJECT, + required=[], + properties={ + 'debug_field_list': openapi.Schema(type=openapi.TYPE_ARRAY, + description="输入变量列表", + items=openapi.Schema(type=openapi.TYPE_OBJECT, + required=[], + properties={ + 'name': openapi.Schema( + type=openapi.TYPE_STRING, + title="变量名", + description="变量名"), + 'value': openapi.Schema( + type=openapi.TYPE_STRING, + title="变量值", + description="变量值"), + })), + 'code': openapi.Schema(type=openapi.TYPE_STRING, title="函数内容", description="函数内容"), + 'input_field_list': openapi.Schema(type=openapi.TYPE_ARRAY, + description="输入变量列表", + items=openapi.Schema(type=openapi.TYPE_OBJECT, + required=['name', 'is_required', 'source'], + properties={ + 'name': openapi.Schema( + type=openapi.TYPE_STRING, + title="变量名", + description="变量名"), + 'is_required': openapi.Schema( + type=openapi.TYPE_BOOLEAN, + title="是否必填", + description="是否必填"), + 'type': openapi.Schema( + type=openapi.TYPE_STRING, + title="字段类型", + description="字段类型 string|int|dict|array|float" + ), + 'source': openapi.Schema( + type=openapi.TYPE_STRING, + title="来源", + description="来源只支持custom|reference"), + + })) + } + ) + + class Edit(ApiMixin): + @staticmethod + def get_request_body_api(): + return openapi.Schema( + type=openapi.TYPE_OBJECT, + required=[], + properties={ + 'name': openapi.Schema(type=openapi.TYPE_STRING, title="函数名称", description="函数名称"), + 'desc': openapi.Schema(type=openapi.TYPE_STRING, title="函数描述", description="函数描述"), + 'code': openapi.Schema(type=openapi.TYPE_STRING, title="函数内容", description="函数内容"), + 'input_field_list': openapi.Schema(type=openapi.TYPE_ARRAY, + description="输入变量列表", + items=openapi.Schema(type=openapi.TYPE_OBJECT, + required=[], + properties={ + 'name': openapi.Schema( + type=openapi.TYPE_STRING, + title="变量名", + description="变量名"), + 'is_required': openapi.Schema( + type=openapi.TYPE_BOOLEAN, + title="是否必填", + description="是否必填"), + 'type': openapi.Schema( + type=openapi.TYPE_STRING, + title="字段类型", + description="字段类型 string|int|dict|array|float" + ), + 'source': openapi.Schema( + type=openapi.TYPE_STRING, + title="来源", + description="来源只支持custom|reference"), + + })) + } + ) + + class Create(ApiMixin): + @staticmethod + def get_request_body_api(): + return openapi.Schema( + type=openapi.TYPE_OBJECT, + required=['name', 'code', 'input_field_list'], + properties={ + 'name': openapi.Schema(type=openapi.TYPE_STRING, title="函数名称", description="函数名称"), + 'desc': openapi.Schema(type=openapi.TYPE_STRING, title="函数描述", description="函数描述"), + 'code': openapi.Schema(type=openapi.TYPE_STRING, title="函数内容", description="函数内容"), + 'input_field_list': openapi.Schema(type=openapi.TYPE_ARRAY, + description="输入变量列表", + items=openapi.Schema(type=openapi.TYPE_OBJECT, + required=['name', 'is_required', 'source'], + properties={ + 'name': openapi.Schema( + type=openapi.TYPE_STRING, + title="变量名", + description="变量名"), + 'is_required': openapi.Schema( + type=openapi.TYPE_BOOLEAN, + title="是否必填", + description="是否必填"), + 'type': openapi.Schema( + type=openapi.TYPE_STRING, + title="字段类型", + description="字段类型 string|int|dict|array|float" + ), + 'source': openapi.Schema( + type=openapi.TYPE_STRING, + title="来源", + description="来源只支持custom|reference"), + + })) + } + ) diff --git a/apps/function_lib/tests.py b/apps/function_lib/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/apps/function_lib/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/function_lib/urls.py b/apps/function_lib/urls.py new file mode 100644 index 000000000..a393fd552 --- /dev/null +++ b/apps/function_lib/urls.py @@ -0,0 +1,12 @@ +from django.urls import path + +from . import views + +app_name = "function_lib" +urlpatterns = [ + path('function_lib', views.FunctionLibView.as_view()), + path('function_lib/debug', views.FunctionLibView.Debug.as_view()), + path('function_lib/', views.FunctionLibView.Operate.as_view()), + path("function_lib//", views.FunctionLibView.Page.as_view(), + name="function_lib_page"), +] diff --git a/apps/function_lib/views/__init__.py b/apps/function_lib/views/__init__.py new file mode 100644 index 000000000..aadec353e --- /dev/null +++ b/apps/function_lib/views/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: __init__.py + @date:2024/8/2 14:53 + @desc: +""" +from .function_lib_views import * diff --git a/apps/function_lib/views/function_lib_views.py b/apps/function_lib/views/function_lib_views.py new file mode 100644 index 000000000..a3c87fb87 --- /dev/null +++ b/apps/function_lib/views/function_lib_views.py @@ -0,0 +1,91 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: function_lib_views.py + @date:2024/8/2 17:08 + @desc: +""" +from drf_yasg.utils import swagger_auto_schema +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.views import APIView + +from common.auth import TokenAuth, has_permissions +from common.constants.permission_constants import RoleConstants +from common.response import result +from function_lib.serializers.function_lib_serializer import FunctionLibSerializer +from function_lib.swagger_api.function_lib_api import FunctionLibApi + + +class FunctionLibView(APIView): + authentication_classes = [TokenAuth] + + @action(methods=["GET"], detail=False) + @swagger_auto_schema(operation_summary="获取函数列表", + operation_id="获取函数列表", + tags=["函数库"], + manual_parameters=FunctionLibApi.Query.get_request_params_api()) + @has_permissions(RoleConstants.ADMIN, RoleConstants.USER) + def get(self, request: Request): + return result.success( + FunctionLibSerializer.Query( + data={'name': request.query_params.get('name'), + 'desc': request.query_params.get('desc'), + 'user_id': request.user.id}).list()) + + @action(methods=['POST'], detail=False) + @swagger_auto_schema(operation_summary="创建函数", + operation_id="创建函数", + request_body=FunctionLibApi.Create.get_request_body_api(), + tags=['函数库']) + @has_permissions(RoleConstants.ADMIN, RoleConstants.USER) + def post(self, request: Request): + return result.success(FunctionLibSerializer.Create(data={'user_id': request.user.id}).insert(request.data)) + + class Debug(APIView): + authentication_classes = [TokenAuth] + + @action(methods=['POST'], detail=False) + @swagger_auto_schema(operation_summary="调试函数", + operation_id="调试函数", + request_body=FunctionLibApi.Debug.get_request_body_api(), + tags=['函数库']) + @has_permissions(RoleConstants.ADMIN, RoleConstants.USER) + def post(self, request: Request): + return result.success( + FunctionLibSerializer.Debug(data={'user_id': request.user.id}).debug( + request.data)) + + class Operate(APIView): + authentication_classes = [TokenAuth] + + @action(methods=['PUT'], detail=False) + @swagger_auto_schema(operation_summary="修改函数", + operation_id="修改函数", + request_body=FunctionLibApi.Edit.get_request_body_api(), + tags=['函数库']) + @has_permissions(RoleConstants.ADMIN, RoleConstants.USER) + def put(self, request: Request, function_lib_id: str): + return result.success( + FunctionLibSerializer.Operate(data={'user_id': request.user.id, 'id': function_lib_id}).edit( + request.data)) + + class Page(APIView): + authentication_classes = [TokenAuth] + + @action(methods=['GET'], detail=False) + @swagger_auto_schema(operation_summary="分页获取函数列表", + operation_id="分页获取函数列表", + manual_parameters=result.get_page_request_params( + FunctionLibApi.Query.get_request_params_api()), + responses=result.get_page_api_response(FunctionLibApi.get_response_body_api()), + tags=['函数库']) + @has_permissions(RoleConstants.ADMIN, RoleConstants.USER) + def get(self, request: Request, current_page: int, page_size: int): + return result.success( + FunctionLibSerializer.Query( + data={'name': request.query_params.get('name'), + 'desc': request.query_params.get('desc'), + 'user_id': request.user.id}).page( + current_page, page_size)) diff --git a/apps/smartdoc/conf.py b/apps/smartdoc/conf.py index 7b0188988..a7665149f 100644 --- a/apps/smartdoc/conf.py +++ b/apps/smartdoc/conf.py @@ -87,7 +87,8 @@ class Config(dict): "EMBEDDING_MODEL_PATH": os.path.join(PROJECT_DIR, 'models'), # 向量库配置 "VECTOR_STORE_NAME": 'pg_vector', - "DEBUG": False + "DEBUG": False, + 'SANDBOX': False } diff --git a/apps/smartdoc/settings/base.py b/apps/smartdoc/settings/base.py index 0113ef855..f01e0587f 100644 --- a/apps/smartdoc/settings/base.py +++ b/apps/smartdoc/settings/base.py @@ -41,7 +41,8 @@ INSTALLED_APPS = [ "drf_yasg", # swagger 接口 'django_filters', # 条件过滤 'django_apscheduler', - 'common' + 'common', + 'function_lib' ] diff --git a/apps/smartdoc/urls.py b/apps/smartdoc/urls.py index 8a1e9bf2f..aa3b563eb 100644 --- a/apps/smartdoc/urls.py +++ b/apps/smartdoc/urls.py @@ -11,7 +11,7 @@ Class-based views 1. Add an import: forms other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf - 1. Import the include() function: forms django.urls import include, path + 1. Import the include() function_lib: forms django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ import os @@ -34,7 +34,8 @@ urlpatterns = [ path("api/", include("users.urls")), path("api/", include("dataset.urls")), path("api/", include("setting.urls")), - path("api/", include("application.urls")) + path("api/", include("application.urls")), + path("api/", include("function_lib.urls")) ] diff --git a/installer/Dockerfile b/installer/Dockerfile index 331f542df..7e2f25992 100644 --- a/installer/Dockerfile +++ b/installer/Dockerfile @@ -17,7 +17,9 @@ RUN apt-get update && \ COPY . /opt/maxkb/app RUN mkdir -p /opt/maxkb/app /opt/maxkb/model /opt/maxkb/conf && \ - rm -rf /opt/maxkb/app/ui + rm -rf /opt/maxkb/app/ui &&\ + useradd sandbox && mkdir /opt/maxkb/app/sandbox && chown sandbox:sandbox /opt/maxkb/app/sandbox && chmod 700 /opt/maxkb/app/sandbox \ + COPY --from=web-build ui /opt/maxkb/app/ui WORKDIR /opt/maxkb/app RUN python3 -m venv /opt/py3 && \ @@ -41,6 +43,7 @@ ENV MAXKB_VERSION="${DOCKER_IMAGE_TAG} (build at ${BUILD_AT}, commit: ${GITHUB_C MAXKB_DB_PASSWORD=Password123@postgres \ MAXKB_EMBEDDING_MODEL_NAME=/opt/maxkb/model/embedding/shibing624_text2vec-base-chinese \ MAXKB_EMBEDDING_MODEL_PATH=/opt/maxkb/model/embedding \ + MAXKB_SANDBOX=true \ LANG=en_US.UTF-8 \ PATH=/opt/py3/bin:$PATH \ POSTGRES_USER=root \ diff --git a/pyproject.toml b/pyproject.toml index f7d5b025d..135cc3511 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["shaohuzhang1 "] readme = "README.md" [tool.poetry.dependencies] -python = "^3.11" +python = ">=3.11,<3.12" django = "4.2.15" djangorestframework = "^3.15.2" drf-yasg = "1.21.7" @@ -45,6 +45,9 @@ xlrd = "^2.0.1" gunicorn = "^22.0.0" python-daemon = "3.0.1" gevent = "^24.2.1" +restrictedpython = "^7.2" +cgroups = "^0.1.0" +wasm-exec = "^0.1.9" boto3 = "^1.34.151" langchain-aws = "^0.1.13" diff --git a/ui/package.json b/ui/package.json index 811070a72..89a31f101 100644 --- a/ui/package.json +++ b/ui/package.json @@ -18,6 +18,7 @@ "@logicflow/extension": "^1.2.27", "@vueuse/core": "^10.9.0", "axios": "^0.28.0", + "codemirror-editor-vue3": "^2.7.0", "cropperjs": "^1.6.2", "echarts": "^5.5.0", "element-plus": "^2.5.6", diff --git a/ui/src/api/function-lib.ts b/ui/src/api/function-lib.ts new file mode 100644 index 000000000..b9ddc8e87 --- /dev/null +++ b/ui/src/api/function-lib.ts @@ -0,0 +1,82 @@ +import { Result } from '@/request/Result' +import { get, post, del, put } from '@/request/index' +import type { pageRequest } from '@/api/type/common' +import type { functionLibData } from '@/api/type/function-lib' +import { type Ref } from 'vue' + +const prefix = '/function_lib' + +/** + * 获取函数列表 + * param { + "name": "string", + } + */ +const getAllFunctionLib: (param?: any, loading?: Ref) => Promise> = ( + param, + loading +) => { + return get(`${prefix}`, param || {}, loading) +} + +/** + * 获取分页函数列表 + * page { + "current_page": "string", + "page_size": "string", + } + * param { + "name": "string", + } + */ +const getFunctionLib: ( + page: pageRequest, + param: any, + loading?: Ref +) => Promise> = (page, param, loading) => { + return get(`${prefix}/${page.current_page}/${page.page_size}`, param, loading) +} + +/** + * 创建函数 + * @param 参数 + */ +const postFunctionLib: (data: functionLibData, loading?: Ref) => Promise> = ( + data, + loading +) => { + return post(`${prefix}`, data, undefined, loading) +} + +/** + * 修改函数 + * @param 参数 + + */ +const putFunctionLib: ( + function_lib_id: string, + data: functionLibData, + loading?: Ref +) => Promise> = (function_lib_id, data, loading) => { + return put(`${prefix}/${function_lib_id}`, data, undefined, loading) +} + +/** + * 调试函数 + * @param 参数 + + */ +const postFunctionLibDebug: (data: any, loading?: Ref) => Promise> = ( + data: any, + loading +) => { + return post(`${prefix}/debug`, data, undefined, loading) +} + +export default { + getFunctionLib, + postFunctionLib, + putFunctionLib, + postFunctionLibDebug, + getAllFunctionLib +} diff --git a/ui/src/api/type/function-lib.ts b/ui/src/api/type/function-lib.ts new file mode 100644 index 000000000..ff58d0a7a --- /dev/null +++ b/ui/src/api/type/function-lib.ts @@ -0,0 +1,9 @@ +interface functionLibData { + id?: String + name: String + desc: String + code?: String + input_field_list?: Array +} + +export type { functionLibData } diff --git a/ui/src/assets/icon_function_outlined.svg b/ui/src/assets/icon_function_outlined.svg new file mode 100644 index 000000000..dbdef4c24 --- /dev/null +++ b/ui/src/assets/icon_function_outlined.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ui/src/components/ai-chat/ExecutionDetailDialog.vue b/ui/src/components/ai-chat/ExecutionDetailDialog.vue index 58e8bef41..d90a3b507 100644 --- a/ui/src/components/ai-chat/ExecutionDetailDialog.vue +++ b/ui/src/components/ai-chat/ExecutionDetailDialog.vue @@ -141,6 +141,27 @@ + + + - + diff --git a/ui/src/workflow/icons/function-node-icon.vue b/ui/src/workflow/icons/function-node-icon.vue new file mode 100644 index 000000000..e6e84a801 --- /dev/null +++ b/ui/src/workflow/icons/function-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/nodes/function-lib-node/index.ts b/ui/src/workflow/nodes/function-lib-node/index.ts new file mode 100644 index 000000000..475818c1f --- /dev/null +++ b/ui/src/workflow/nodes/function-lib-node/index.ts @@ -0,0 +1,12 @@ +import FunctionLibNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class FunctionLibNode extends AppNode { + constructor(props: any) { + super(props, FunctionLibNodeVue) + } +} +export default { + type: 'function-lib-node', + model: AppNodeModel, + view: FunctionLibNode +} diff --git a/ui/src/workflow/nodes/function-lib-node/index.vue b/ui/src/workflow/nodes/function-lib-node/index.vue new file mode 100644 index 000000000..572d01671 --- /dev/null +++ b/ui/src/workflow/nodes/function-lib-node/index.vue @@ -0,0 +1,121 @@ + + + diff --git a/ui/src/workflow/nodes/function-node/index.ts b/ui/src/workflow/nodes/function-node/index.ts new file mode 100644 index 000000000..ab3f36edf --- /dev/null +++ b/ui/src/workflow/nodes/function-node/index.ts @@ -0,0 +1,12 @@ +import FunctionNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class FunctionLibCustomNode extends AppNode { + constructor(props: any) { + super(props, FunctionNodeVue) + } +} +export default { + type: 'function-node', + model: AppNodeModel, + view: FunctionLibCustomNode +} diff --git a/ui/src/workflow/nodes/function-node/index.vue b/ui/src/workflow/nodes/function-node/index.vue new file mode 100644 index 000000000..51ed65be6 --- /dev/null +++ b/ui/src/workflow/nodes/function-node/index.vue @@ -0,0 +1,200 @@ + + +