feat: 函数库功能

This commit is contained in:
shaohuzhang1 2024-08-15 17:17:25 +08:00
parent 845225cce1
commit f421d1975d
57 changed files with 2299 additions and 39 deletions

View File

@ -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):

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file __init__.py
@date2024/8/8 17:45
@desc:
"""
from .impl import *

View File

@ -0,0 +1,42 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file i_function_lib_node.py
@date2024/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

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file __init__.py
@date2024/8/8 17:48
@desc:
"""
from .base_function_lib_node import BaseFunctionLibNodeNode

View File

@ -0,0 +1,92 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file base_function_lib_node.py
@date2024/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
}

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file __init__.py.py
@date2024/8/13 10:43
@desc:
"""
from .impl import *

View File

@ -0,0 +1,60 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file i_function_lib_node.py
@date2024/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

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file __init__.py.py
@date2024/8/13 11:19
@desc:
"""
from .base_function_node import BaseFunctionNodeNode

View File

@ -0,0 +1,75 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file base_function_lib_node.py
@date2024/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
}

View File

@ -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:

View File

@ -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

View File

@ -0,0 +1,93 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file function_code.py
@date2024/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)

View File

@ -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):

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class FunctionLibConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'function_lib'

View File

@ -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',
},
),
]

View File

View File

@ -0,0 +1,8 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file __init__.py.py
@date2024/8/2 14:55
@desc:
"""

View File

@ -0,0 +1,29 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file function_lib.py
@date2024/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"

View File

@ -0,0 +1,191 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file function_lib_serializer.py
@date2024/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

View File

@ -0,0 +1,168 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file function_lib_api.py
@date2024/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"),
}))
}
)

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
apps/function_lib/urls.py Normal file
View File

@ -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/<str:function_lib_id>', views.FunctionLibView.Operate.as_view()),
path("function_lib/<int:current_page>/<int:page_size>", views.FunctionLibView.Page.as_view(),
name="function_lib_page"),
]

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file __init__.py
@date2024/8/2 14:53
@desc:
"""
from .function_lib_views import *

View File

@ -0,0 +1,91 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file function_lib_views.py
@date2024/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))

View File

@ -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
}

View File

@ -41,7 +41,8 @@ INSTALLED_APPS = [
"drf_yasg", # swagger 接口
'django_filters', # 条件过滤
'django_apscheduler',
'common'
'common',
'function_lib'
]

View File

@ -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"))
]

View File

@ -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 \

View File

@ -6,7 +6,7 @@ authors = ["shaohuzhang1 <shaohu.zhang@fit2cloud.com>"]
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"

View File

@ -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",

View File

@ -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<boolean>) => Promise<Result<any>> = (
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<boolean>
) => Promise<Result<any>> = (page, param, loading) => {
return get(`${prefix}/${page.current_page}/${page.page_size}`, param, loading)
}
/**
*
* @param
*/
const postFunctionLib: (data: functionLibData, loading?: Ref<boolean>) => Promise<Result<any>> = (
data,
loading
) => {
return post(`${prefix}`, data, undefined, loading)
}
/**
*
* @param
*/
const putFunctionLib: (
function_lib_id: string,
data: functionLibData,
loading?: Ref<boolean>
) => Promise<Result<any>> = (function_lib_id, data, loading) => {
return put(`${prefix}/${function_lib_id}`, data, undefined, loading)
}
/**
*
* @param
*/
const postFunctionLibDebug: (data: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
data: any,
loading
) => {
return post(`${prefix}/debug`, data, undefined, loading)
}
export default {
getFunctionLib,
postFunctionLib,
putFunctionLib,
postFunctionLibDebug,
getAllFunctionLib
}

View File

@ -0,0 +1,9 @@
interface functionLibData {
id?: String
name: String
desc: String
code?: String
input_field_list?: Array<any>
}
export type { functionLibData }

View File

@ -0,0 +1,12 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6260_56998)">
<path d="M8.45989 5.8569L8.57477 5.86299C8.79559 5.87481 8.96834 6.05752 8.96834 6.27866V6.82427C8.96834 7.07408 8.75005 7.26525 8.50052 7.25364C8.42661 7.2502 8.35257 7.24833 8.27834 7.24833C8.06408 7.24833 7.92822 7.30022 7.84948 7.38496C7.77816 7.4623 7.73167 7.61024 7.73167 7.85333V8.13167H8.55167C8.78179 8.13167 8.96834 8.31821 8.96834 8.54833V9.38881C8.96834 9.61893 8.78179 9.80547 8.55167 9.80547H7.73167V13.9133C7.73167 14.1435 7.54512 14.33 7.31501 14.33H6.32501C6.09489 14.33 5.90834 14.1435 5.90834 13.9133V9.80547H5.13859C4.90847 9.80547 4.72192 9.61893 4.72192 9.38881V8.54833C4.72192 8.31821 4.90847 8.13167 5.13859 8.13167H5.90834V7.74833C5.90834 7.19278 6.08218 6.73149 6.43374 6.37176C6.78941 6.00781 7.32033 5.83333 8.01 5.83333C8.15999 5.83333 8.30995 5.84119 8.45989 5.8569Z" fill="white"/>
<path d="M12.4626 9.47701L11.5695 8.15211C11.492 8.03721 11.3625 7.96833 11.224 7.96833H10.1483C9.81198 7.96833 9.61424 8.34622 9.80596 8.62253L11.3934 10.9103L9.61243 13.5148C9.42332 13.7913 9.62135 14.1667 9.95637 14.1667H11.0006C11.1403 14.1667 11.2708 14.0966 11.3479 13.9801L12.4396 12.3325L13.5313 13.9801C13.6085 14.0966 13.7389 14.1667 13.8786 14.1667H14.9522C15.2886 14.1667 15.4864 13.7886 15.2945 13.5123L13.4629 10.8753L15.044 8.62451C15.238 8.34841 15.0405 7.96833 14.7031 7.96833H13.6913C13.552 7.96833 13.4219 8.03796 13.3446 8.15387L12.4626 9.47701Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.9226 2.74408C18.7663 2.5878 18.5543 2.5 18.3333 2.5H1.66665C1.44563 2.5 1.23367 2.5878 1.07739 2.74408C0.92111 2.90036 0.833313 3.11232 0.833313 3.33333V16.6667C0.833313 16.8877 0.92111 17.0996 1.07739 17.2559C1.23367 17.4122 1.44563 17.5 1.66665 17.5H18.3333C18.5543 17.5 18.7663 17.4122 18.9226 17.2559C19.0788 17.0996 19.1666 16.8877 19.1666 16.6667V3.33333C19.1666 3.11232 19.0788 2.90036 18.9226 2.74408ZM2.49998 4.16667H17.5V15.8333H2.49998V4.16667Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_6260_56998">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -141,6 +141,27 @@
</div>
</div>
</template>
<!-- 函数库 -->
<template
v-if="
item.type === WorkflowType.FunctionLib ||
item.type === WorkflowType.FunctionLibCustom
"
>
<div class="card-never border-r-4 mt-8">
<h5 class="p-8-12">输入</h5>
<div class="p-8-12 border-t-dashed lighter pre-wrap">
{{ item.params || '-' }}
</div>
</div>
<div class="card-never border-r-4 mt-8">
<h5 class="p-8-12">输出</h5>
<div class="p-8-12 border-t-dashed lighter pre-wrap">
{{ item.result || '-' }}
</div>
</div>
</template>
</template>
<template v-else>
<div class="card-never border-r-4">

View File

@ -0,0 +1,24 @@
<template>
<Codemirror v-bind="$attrs" border :height="200" :option="cmOptions" />
</template>
<script setup lang="ts">
import Codemirror from 'codemirror-editor-vue3'
import 'codemirror/mode/python/python.js'
defineOptions({ name: 'CodemirrorEditor' })
const cmOptions = {
mode: 'text/x-python',
autoRefresh: true
}
</script>
<style lang="scss" scoped>
.codemirror-container.bordered {
border: 1px solid #bbbfc4;
border-radius: 4px;
}
.CodeMirror-gutters {
left: 0 !important;
}
</style>

View File

@ -22,6 +22,7 @@ import MdPreview from './markdown/MdPreview.vue'
import LogoFull from './logo/LogoFull.vue'
import LogoIcon from './logo/LogoIcon.vue'
import SendIcon from './logo/SendIcon.vue'
import CodemirrorEditor from './codemirror-editor/index.vue'
export default {
install(app: App) {
@ -48,5 +49,6 @@ export default {
app.component(LogoFull.name, LogoFull)
app.component(LogoIcon.name, LogoIcon)
app.component(SendIcon.name, SendIcon)
app.component(CodemirrorEditor.name, CodemirrorEditor)
}
}

View File

@ -5,5 +5,7 @@ export enum WorkflowType {
SearchDataset = 'search-dataset-node',
Question = 'question-node',
Condition = 'condition-node',
Reply = 'reply-node'
Reply = 'reply-node',
FunctionLib = 'function-lib-node',
FunctionLibCustom = 'function-node'
}

View File

@ -40,7 +40,10 @@ instance.interceptors.response.use(
(response: any) => {
if (response.data) {
if (response.data.code !== 200 && !(response.data instanceof Blob)) {
if (!response.config.url.includes('/valid')) {
if (
!response.config.url.includes('/valid') &&
!response.config.url.includes('/function_lib/debug')
) {
MsgError(response.data.message)
return Promise.reject(response.data)
}

View File

@ -0,0 +1,17 @@
import Layout from '@/layout/layout-template/DetailLayout.vue'
const functionLibRouter = {
path: '/function-lib',
name: 'function-lib',
meta: { title: '函数库', permission: 'APPLICATION:READ' },
redirect: '/function-lib',
component: () => import('@/layout/layout-template/AppLayout.vue'),
children: [
{
path: '/function-lib',
name: 'function-lib',
component: () => import('@/views/function-lib/index.vue')
}
]
}
export default functionLibRouter

View File

@ -480,6 +480,10 @@ h5 {
background: #3370ff;
}
.avatar-green {
background: #34c724;
}
.success {
color: var(--el-color-success);
}

View File

@ -1,5 +1,5 @@
:root {
--el-color-primary: #3370FF;
--el-color-primary: #3370ff;
--el-menu-item-height: 45px;
--el-box-shadow-light: 0px 2px 4px 0px rgba(31, 35, 41, 0.12);
--el-border-color: #dee0e3;
@ -362,11 +362,25 @@
}
// 提示横幅
.el-alert__title {
color: var(--el-text-color-regular) !important;
font-weight: 400;
}
.el-alert--warning.is-light {
background-color: #ffe7cc;
color: var(--app-text-color);
font-weight: 400;
.el-alert__icon {
color: #ff8800;
}
}
.el-alert--success.is-light {
background-color: #d6f4d3;
.el-alert__icon {
color: #34c724;
}
}
.el-alert--danger.is-light {
background-color: #fddbda;
.el-alert__icon {
color: #f54a45;
}
}

View File

@ -11,9 +11,7 @@
>
</div>
<div>
<el-button icon="Plus" @click="showPopover = !showPopover" v-click-outside="clickoutside">
添加组件
</el-button>
<el-button icon="Plus" @click="showPopover = !showPopover"> 添加组件 </el-button>
<el-button @click="clickShowDebug" :disabled="showDebug">
<AppIcon iconName="app-play-outlined" class="mr-4"></AppIcon>
调试</el-button
@ -27,21 +25,63 @@
</div>
<!-- 下拉框 -->
<el-collapse-transition>
<div v-show="showPopover" class="workflow-dropdown-menu border border-r-4">
<h5 class="title">基础组件</h5>
<template v-for="(item, index) in menuNodes" :key="index">
<div
class="workflow-dropdown-item cursor flex p-8-12"
@click="clickNodes(item)"
@mousedown="onmousedown(item)"
>
<component :is="iconComponent(`${item.type}-icon`)" class="mr-8 mt-4" :size="32" />
<div class="pre-wrap">
<div class="lighter">{{ item.label }}</div>
<el-text type="info" size="small">{{ item.text }}</el-text>
<div
v-show="showPopover"
class="workflow-dropdown-menu border border-r-4"
v-click-outside="clickoutside"
>
<el-tabs v-model="activeName" class="workflow-dropdown-tabs">
<el-tab-pane label="基础组件" name="base">
<template v-for="(item, index) in menuNodes" :key="index">
<div
class="workflow-dropdown-item cursor flex p-8-12"
@click="clickNodes(item)"
@mousedown="onmousedown(item)"
>
<component :is="iconComponent(`${item.type}-icon`)" class="mr-8 mt-4" :size="32" />
<div class="pre-wrap">
<div class="lighter">{{ item.label }}</div>
<el-text type="info" size="small">{{ item.text }}</el-text>
</div>
</div>
</template>
</el-tab-pane>
<el-tab-pane label="函数库" name="function">
<div
class="workflow-dropdown-item cursor flex p-8-12"
@click="clickNodes(functionNode)"
@mousedown="onmousedown(functionNode)"
>
<component
:is="iconComponent(`function-lib-node-icon`)"
class="mr-8 mt-4"
:size="32"
/>
<div class="pre-wrap">
<div class="lighter">{{ functionNode.label }}</div>
<el-text type="info" size="small">{{ functionNode.text }}</el-text>
</div>
</div>
</div>
</template>
<template v-for="(item, index) in functionLibList" :key="index">
<div
class="workflow-dropdown-item cursor flex p-8-12"
@click="clickNodes(functionLibNode, item)"
@mousedown="onmousedown(functionLibNode, item)"
>
<component
:is="iconComponent(`function-lib-node-icon`)"
class="mr-8 mt-4"
:size="32"
/>
<div class="pre-wrap">
<div class="lighter">{{ item.name }}</div>
<el-text type="info" size="small">{{ item.desc }}</el-text>
</div>
</div>
</template>
</el-tab-pane>
</el-tabs>
</div>
</el-collapse-transition>
<!-- 主画布 -->
@ -106,7 +146,7 @@
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import Workflow from '@/workflow/index.vue'
import { menuNodes } from '@/workflow/common/data'
import { menuNodes, functionLibNode, functionNode } from '@/workflow/common/data'
import { iconComponent } from '@/workflow/icons/utils'
import applicationApi from '@/api/application'
import { isAppIcon } from '@/utils/application'
@ -115,6 +155,7 @@ import { datetimeFormat } from '@/utils/time'
import useStore from '@/stores'
import { WorkFlowInstance } from '@/workflow/common/validate'
import { hasPermission } from '@/utils/permission'
import functionLibApi from '@/api/function-lib'
const { user, application } = useStore()
const router = useRouter()
@ -137,6 +178,8 @@ const showPopover = ref(false)
const showDebug = ref(false)
const enlarge = ref(false)
const saveTime = ref<any>('')
const activeName = ref('base')
const functionLibList = ref<any[]>([])
function publicHandle() {
workflowRef.value
@ -199,12 +242,22 @@ function clickoutsideDebug() {
showDebug.value = false
}
function clickNodes(item: any) {
function clickNodes(item: any, data?: any) {
if (data) {
item['properties']['stepName'] = data.name
item['properties']['node_data'] = data
}
workflowRef.value?.addNode(item)
showPopover.value = false
}
function onmousedown(item: any) {
function onmousedown(item: any, data?: any) {
if (data) {
item['properties']['stepName'] = data.name
item['properties']['node_data'] = { ...data, function_lib_id: data.id }
}
workflowRef.value?.onmousedown(item)
showPopover.value = false
}
function getGraphData() {
@ -230,6 +283,12 @@ function saveApplication() {
})
}
function getList() {
functionLibApi.getAllFunctionLib({}, loading).then((res: any) => {
functionLibList.value = res.data
})
}
/**
* 定时保存
*/
@ -250,6 +309,7 @@ const closeInterval = () => {
onMounted(() => {
getDetail()
getList()
//
if (hasPermission(`APPLICATION:MANAGE:${id}`, 'AND')) {
initInterval()
@ -298,6 +358,11 @@ onBeforeUnmount(() => {
}
}
}
.workflow-dropdown-tabs {
.el-tabs__nav-wrap {
padding: 0 16px;
}
}
}
.workflow-debug-container {

View File

@ -0,0 +1,109 @@
<template>
<el-dialog
:title="isEdit ? '编辑变量' : '添加变量'"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
append-to-body
>
<el-form
label-position="top"
ref="fieldFormRef"
:rules="rules"
:model="form"
require-asterisk-position="right"
>
<el-form-item label="变量名" prop="name">
<el-input
v-model="form.name"
placeholder="请输入变量名"
show-word-limit
@blur="form.name = form.name.trim()"
/>
</el-form-item>
<el-form-item label="数据类型">
<el-select v-model="form.type">
<el-option v-for="item in typeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="来源">
<el-select v-model="form.source">
<el-option label="引用变量" value="reference" />
<el-option label="自定义" value="custom" />
</el-select>
</el-form-item>
<el-form-item label="是否必填" @click.prevent>
<el-switch size="small" v-model="form.is_required"></el-switch>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
{{ isEdit ? '保存' : '添加' }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import type { FormInstance } from 'element-plus'
import { cloneDeep } from 'lodash'
const typeOptions = ['string', 'int', 'dict', 'array', 'float']
const emit = defineEmits(['refresh'])
const fieldFormRef = ref()
const loading = ref<boolean>(false)
const isEdit = ref(false)
const form = ref<any>({
name: '',
type: typeOptions[0],
source: 'reference',
is_required: false
})
const rules = reactive({
name: [{ required: true, message: '请输入变量名', trigger: 'blur' }]
})
const dialogVisible = ref<boolean>(false)
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
name: '',
type: typeOptions[0],
source: 'reference',
is_required: false
}
isEdit.value = false
}
})
const open = (row: any) => {
if (row) {
form.value = cloneDeep(row)
isEdit.value = true
}
dialogVisible.value = true
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
emit('refresh', form.value)
dialogVisible.value = false
}
})
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,150 @@
<template>
<el-drawer v-model="dubugVisible" size="60%" :append-to-body="true">
<template #header>
<div class="flex align-center" style="margin-left: -8px">
<el-button class="cursor mr-4" link @click.prevent="dubugVisible = false">
<el-icon :size="20">
<Back />
</el-icon>
</el-button>
<h4>调试</h4>
</div>
</template>
<div>
<div v-if="form.debug_field_list.length > 0" class="mb-16">
<h4 class="title-decoration-1 mb-16">输入变量</h4>
<el-card shadow="never" class="card-never" style="--el-card-padding: 12px">
<el-form
ref="FormRef"
:model="form"
label-position="top"
require-asterisk-position="right"
hide-required-asterisk
v-loading="loading"
>
<template v-for="(item, index) in form.debug_field_list" :key="index">
<el-form-item
:label="item.name"
:prop="'debug_field_list.' + index + '.value'"
:rules="{
required: item.is_required,
message: '请输入变量值',
trigger: 'blur'
}"
>
<template #label>
<div class="flex">
<span
>{{ item.name }} <span class="danger" v-if="item.is_required">*</span></span
>
<el-tag type="info" class="info-tag ml-4">{{ item.type }}</el-tag>
</div>
</template>
<el-input v-model="item.value" placeholder="请输入变量值" />
</el-form-item>
</template>
</el-form>
</el-card>
</div>
<el-button type="primary" @click="submit(FormRef)" :loading="loading"> 运行 </el-button>
<div v-if="showResult" class="mt-8">
<h4 class="title-decoration-1 mb-16 mt-16">运行结果</h4>
<div class="mb-16">
<el-alert v-if="isSuccess" title="运行成功" type="success" show-icon :closable="false" />
<el-alert v-else title="运行失败" type="error" show-icon :closable="false" />
</div>
<p class="lighter mb-8">输出</p>
<el-card class="pre-wrap danger" shadow="never" style="max-height: 350px; overflow: scroll">
{{ result || '-' }}
</el-card>
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import functionLibApi from '@/api/function-lib'
import type { FormInstance } from 'element-plus'
const FormRef = ref()
const loading = ref(false)
const dubugVisible = ref(false)
const showResult = ref(false)
const isSuccess = ref(false)
const result = ref('')
const form = ref<any>({
debug_field_list: [],
code: '',
input_field_list: []
})
watch(dubugVisible, (bool) => {
if (!bool) {
showResult.value = false
isSuccess.value = false
result.value = ''
form.value = {
debug_field_list: [],
code: '',
input_field_list: []
}
}
})
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) {
functionLibApi
.postFunctionLibDebug(form.value, loading)
.then((res) => {
showResult.value = true
isSuccess.value = true
result.value = res.data
})
.catch((res) => {
showResult.value = true
isSuccess.value = false
result.value = res.data
})
} else {
await formEl.validate((valid: any) => {
if (valid) {
functionLibApi.postFunctionLibDebug(form.value, loading).then((res) => {
if (res.code === 500) {
showResult.value = true
isSuccess.value = false
result.value = res.message
} else {
showResult.value = true
isSuccess.value = true
result.value = res.data
}
})
}
})
}
}
const open = (data: any) => {
if (data.input_field_list.length > 0) {
data.input_field_list.forEach((item: any) => {
form.value.debug_field_list.push({
value: '',
...item
})
})
}
form.value.code = data.code
form.value.input_field_list = data.input_field_list
dubugVisible.value = true
}
defineExpose({
open
})
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,224 @@
<template>
<el-drawer v-model="visible" size="60%">
<template #header>
<h4>{{ isEdit ? '编辑函数' : '创建函数' }}</h4>
</template>
<div>
<h4 class="title-decoration-1 mb-16">基础信息</h4>
<el-form
ref="FormRef"
:model="form"
:rules="rules"
label-position="top"
require-asterisk-position="right"
v-loading="loading"
>
<el-form-item label="函数名称" prop="name">
<el-input
v-model="form.name"
placeholder="请输入函数名称"
maxlength="64"
show-word-limit
@blur="form.name = form.name.trim()"
/>
</el-form-item>
<el-form-item label="描述">
<el-input
v-model="form.desc"
type="textarea"
placeholder="请输入函数的描述"
maxlength="128"
show-word-limit
:autosize="{ minRows: 3 }"
@blur="form.desc = form.desc.trim()"
/>
</el-form-item>
</el-form>
<div class="flex-between">
<h4 class="title-decoration-1 mb-16">
输入变量 <el-text type="info" class="color-secondary"> 使用函数时显示 </el-text>
</h4>
<el-button link type="primary" @click="openAddDialog()">
<el-icon class="mr-4"><Plus /></el-icon>
</el-button>
</div>
<el-table :data="form.input_field_list" class="mb-16">
<el-table-column prop="name" label="变量名" />
<el-table-column label="数据类型">
<template #default="{ row }">
<el-tag type="info" class="info-tag">{{ row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column label="必填">
<template #default="{ row }">
<div @click.stop>
<el-switch size="small" v-model="row.is_required" />
</div>
</template>
</el-table-column>
<el-table-column prop="source" label="来源">
<template #default="{ row }">
{{ row.source === 'custom' ? '自定义' : '引用变量' }}
</template>
</el-table-column>
<el-table-column label="操作" align="left" width="80">
<template #default="{ row, $index }">
<span class="mr-4">
<el-tooltip effect="dark" content="修改" placement="top">
<el-button type="primary" text @click.stop="openAddDialog(row, $index)">
<el-icon><EditPen /></el-icon>
</el-button>
</el-tooltip>
</span>
<el-tooltip effect="dark" content="删除" placement="top">
<el-button type="primary" text @click="deleteField($index)">
<el-icon>
<Delete />
</el-icon>
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<h4 class="title-decoration-1 mb-16">
Python 代码 <el-text type="info" class="color-secondary"> 使用函数时不显示 </el-text>
</h4>
<CodemirrorEditor v-model:value="form.code" v-if="showEditor" @change="changeCode" />
<h4 class="title-decoration-1 mb-16 mt-16">
输出变量 <el-text type="info" class="color-secondary"> 使用函数时显示 </el-text>
</h4>
<div class="flex-between border-r-4 p-8-12 mb-8 layout-bg lighter">
<span>结果 {result}</span>
</div>
</div>
<template #footer>
<div>
<el-button :loading="loading" @click="visible = false">取消</el-button>
<el-button :loading="loading" @click="openDebug">调试</el-button>
<el-button type="primary" @click="submit(FormRef)" :loading="loading">
{{ isEdit ? '保存' : '创建' }}</el-button
>
</div>
</template>
<FunctionDebugDrawer ref="FunctionDebugDrawerRef" />
<FieldFormDialog ref="FieldFormDialogRef" @refresh="refreshFieldList" />
</el-drawer>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import FieldFormDialog from './FieldFormDialog.vue'
import FunctionDebugDrawer from './FunctionDebugDrawer.vue'
import type { functionLibData } from '@/api/type/function-lib'
import functionLibApi from '@/api/function-lib'
import type { FormInstance } from 'element-plus'
import { MsgSuccess, MsgError } from '@/utils/message'
import { cloneDeep } from 'lodash'
const emit = defineEmits(['refresh'])
const FieldFormDialogRef = ref()
const FunctionDebugDrawerRef = ref()
const FormRef = ref()
const isEdit = ref(false)
const loading = ref(false)
const visible = ref(false)
const showEditor = ref(false)
const currentIndex = ref<any>(null)
const form = ref<functionLibData>({
name: '',
desc: '',
code: '',
input_field_list: []
})
watch(visible, (bool) => {
if (!bool) {
isEdit.value = false
showEditor.value = true
currentIndex.value = null
form.value = {
name: '',
desc: '',
code: '',
input_field_list: []
}
}
})
const rules = reactive({
name: [{ required: true, message: '请输入函数名称', trigger: 'blur' }]
})
function openDebug() {
FunctionDebugDrawerRef.value.open(form.value)
}
function deleteField(index: any) {
form.value.input_field_list?.splice(index, 1)
}
function openAddDialog(data?: any, index?: any) {
if (typeof index !== 'undefined') {
currentIndex.value = index
}
FieldFormDialogRef.value.open(data)
}
function refreshFieldList(data: any) {
if (currentIndex.value !== null) {
form.value.input_field_list?.splice(currentIndex.value, 1, data)
} else {
form.value.input_field_list?.push(data)
}
currentIndex.value = null
}
function changeCode(value: string) {
form.value.code = value
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid: any) => {
if (valid) {
if (isEdit.value) {
functionLibApi.putFunctionLib(form.value?.id as string, form.value, loading).then((res) => {
MsgSuccess('编辑成功')
emit('refresh', res.data)
visible.value = false
})
} else {
functionLibApi.postFunctionLib(form.value, loading).then((res) => {
MsgSuccess('创建成功')
emit('refresh')
visible.value = false
})
}
}
})
}
const open = (data: any) => {
if (data) {
isEdit.value = true
form.value = cloneDeep(data)
}
visible.value = true
setTimeout(() => {
showEditor.value = true
}, 100)
}
defineExpose({
open
})
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,142 @@
<template>
<div class="function-lib-list-container p-24" style="padding-top: 16px">
<div class="flex-between mb-16">
<h4>函数库</h4>
<el-input
v-model="searchValue"
@change="searchHandle"
placeholder="按函数名称搜索"
prefix-icon="Search"
class="w-240"
clearable
/>
</div>
<div v-loading.fullscreen.lock="paginationConfig.current_page === 1 && loading">
<InfiniteScroll
:size="functionLibList.length"
:total="paginationConfig.total"
:page_size="paginationConfig.page_size"
v-model:current_page="paginationConfig.current_page"
@load="getList"
:loading="loading"
>
<el-row :gutter="15">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb-16">
<CardAdd title="创建函数" @click="openCreateDialog()" />
</el-col>
<el-col
:xs="24"
:sm="12"
:md="8"
:lg="6"
:xl="4"
v-for="(item, index) in functionLibList"
:key="index"
class="mb-16"
>
<CardBox
:title="item.name"
:description="item.desc"
class="function-lib-card cursor"
@click="openCreateDialog(item)"
>
<template #icon>
<AppAvatar class="mr-12 avatar-green" shape="square" :size="32">
<img src="@/assets/icon_function_outlined.svg" style="width: 58%" alt="" />
</AppAvatar>
</template>
<template #footer>
<div class="footer-content">
<el-tooltip effect="dark" content="复制" placement="top">
<el-button text>
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
<el-tooltip effect="dark" content="删除" placement="top">
<el-button text @click.stop="deleteFunctionLib(item)">
<el-icon><Delete /></el-icon>
</el-button>
</el-tooltip>
</div>
</template>
</CardBox>
</el-col>
</el-row>
</InfiniteScroll>
</div>
<FunctionFormDrawer ref="FunctionFormDrawerRef" @refresh="refresh" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import functionLibApi from '@/api/function-lib'
import FunctionFormDrawer from './component/FunctionFormDrawer.vue'
const loading = ref(false)
const FunctionFormDrawerRef = ref()
const functionLibList = ref<any[]>([])
const paginationConfig = reactive({
current_page: 1,
page_size: 20,
total: 0
})
const searchValue = ref('')
function openCreateDialog(data?: any) {
FunctionFormDrawerRef.value.open(data)
}
function searchHandle() {
paginationConfig.total = 0
paginationConfig.current_page = 1
functionLibList.value = []
getList()
}
function deleteFunctionLib(row: any) {
// MsgConfirm(
// // @ts-ignore
// `${t('views.function-lib.function-libList.card.delete.confirmTitle')}${row.name} ?`,
// t('views.function-lib.function-libList.card.delete.confirmMessage'),
// {
// confirmButtonText: t('views.function-lib.function-libList.card.delete.confirmButton'),
// cancelButtonText: t('views.function-lib.function-libList.card.delete.cancelButton'),
// confirmButtonClass: 'danger'
// }
// )
// .then(() => {})
// .catch(() => {})
}
function getList() {
functionLibApi
.getFunctionLib(paginationConfig, searchValue.value && { name: searchValue.value }, loading)
.then((res: any) => {
functionLibList.value = [...functionLibList.value, ...res.data.records]
paginationConfig.total = res.data.total
})
}
function refresh(data: any) {
if (data) {
const index = functionLibList.value.findIndex((v) => v.id === data.id)
functionLibList.value.splice(index, 1, data)
} else {
paginationConfig.total = 0
paginationConfig.current_page = 1
functionLibList.value = []
getList()
}
}
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped></style>

View File

@ -88,7 +88,7 @@
</el-tooltip>
</div>
</template>
<EditModel ref="eidtModelRef" @submit="emit('change')"></EditModel>
<EditModel ref="editModelRef" @submit="emit('change')"></EditModel>
</card-box>
</template>
<script setup lang="ts">
@ -130,7 +130,7 @@ const errMessage = computed(() => {
return ''
})
const emit = defineEmits(['change', 'update:model'])
const eidtModelRef = ref<InstanceType<typeof EditModel>>()
const editModelRef = ref<InstanceType<typeof EditModel>>()
let interval: any
const deleteModel = () => {
MsgConfirm(`删除模型 `, `是否删除模型:${props.model.name} ?`, {
@ -154,7 +154,7 @@ const cancelDownload = () => {
const openEditModel = () => {
const provider = props.provider_list.find((p) => p.provider === props.model.provider)
if (provider) {
eidtModelRef.value?.open(provider, props.model)
editModelRef.value?.open(provider, props.model)
}
}
const icon = computed(() => {

View File

@ -141,6 +141,42 @@ export const replyNode = {
}
export const menuNodes = [aiChatNode, searchDatasetNode, questionNode, conditionNode, replyNode]
/**
*
*/
export const functionNode = {
type: WorkflowType.FunctionLibCustom,
text: '通过执行自定义脚本,实现数据处理',
label: '自定义函数',
properties: {
stepName: '自定义函数',
config: {
fields: [
{
label: '结果',
value: 'result'
}
]
}
}
}
export const functionLibNode = {
type: WorkflowType.FunctionLib,
text: '通过执行自定义脚本,实现数据处理',
label: '自定义函数',
properties: {
stepName: '自定义函数',
config: {
fields: [
{
label: '结果',
value: 'result'
}
]
}
}
}
export const compareList = [
{ value: 'is_null', label: '为空' },
{ value: 'is_not_null', label: '不为空' },
@ -165,7 +201,9 @@ export const nodeDict: any = {
[WorkflowType.Condition]: conditionNode,
[WorkflowType.Base]: baseNode,
[WorkflowType.Start]: startNode,
[WorkflowType.Reply]: replyNode
[WorkflowType.Reply]: replyNode,
[WorkflowType.FunctionLib]: functionLibNode,
[WorkflowType.FunctionLibCustom]: functionNode
}
export function isWorkFlow(type: string | undefined) {
return type === 'WORK_FLOW'

View File

@ -1,6 +1,11 @@
import { WorkflowType } from '@/enums/workflow'
const end_nodes = [WorkflowType.AiChat, WorkflowType.Reply]
const end_nodes = [
WorkflowType.AiChat,
WorkflowType.Reply,
WorkflowType.FunctionLib,
WorkflowType.FunctionLibCustom
]
export class WorkFlowInstance {
nodes
edges

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #34c724">
<img src="@/assets/icon_function_outlined.svg" style="width: 75%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #34c724">
<img src="@/assets/icon_function_outlined.svg" style="width: 75%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -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
}

View File

@ -0,0 +1,121 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<h5 class="title-decoration-1 mb-16">节点设置</h5>
<h5 class="lighter mb-8">输入变量</h5>
<el-form
@submit.prevent
@mousemove.stop
@mousedown.stop
@keydown.stop
@click.stop
ref="FunctionNodeFormRef"
:model="chat_data"
label-position="top"
require-asterisk-position="right"
hide-required-asterisk
>
<el-card shadow="never" class="card-never mb-16" style="--el-card-padding: 12px">
<div v-if="chat_data.input_field_list?.length > 0">
<template v-for="(item, index) in chat_data.input_field_list" :key="index">
<el-form-item
:label="item.name"
:prop="'input_field_list.' + index + '.value'"
:rules="{
required: item.is_required,
message: '请输入变量值',
trigger: 'blur'
}"
>
<template #label>
<div class="flex-between">
<div>
<span
>{{ item.name }} <span class="danger" v-if="item.is_required">*</span></span
>
<el-tag type="info" class="info-tag ml-4">{{ item.type }}</el-tag>
</div>
</div>
</template>
<NodeCascader
v-if="item.source === 'reference'"
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
placeholder="请选择变量"
v-model="item.value"
/>
<el-input v-else v-model="item.value" placeholder="请输入变量值" />
</el-form-item>
</template>
</div>
<el-text type="info" v-else> 暂无数据 </el-text>
</el-card>
<el-form-item label="返回内容" @click.prevent>
<template #label>
<div class="flex align-center">
<div class="mr-4">
<span>返回内容<span class="danger">*</span></span>
</div>
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
<template #content>
关闭后该节点的内容则不输出给用户 如果你想让用户看到该节点的输出内容请打开开关
</template>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<el-switch size="small" v-model="chat_data.is_result" />
</el-form-item>
</el-form>
</NodeContainer>
</template>
<script setup lang="ts">
import { set } from 'lodash'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import type { FormInstance } from 'element-plus'
import { ref, computed, onMounted } from 'vue'
import { isLastNode } from '@/workflow/common/data'
const props = defineProps<{ nodeModel: any }>()
const nodeCascaderRef = ref()
const form = {
input_field_list: [],
is_result: false
}
const chat_data = computed({
get: () => {
if (props.nodeModel.properties.node_data) {
return props.nodeModel.properties.node_data
} else {
set(props.nodeModel.properties, 'node_data', form)
}
return props.nodeModel.properties.node_data
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
}
})
const FunctionNodeFormRef = ref<FormInstance>()
const validate = () => {
return FunctionNodeFormRef.value?.validate().catch((err) => {
return Promise.reject({ node: props.nodeModel, errMessage: err })
})
}
onMounted(() => {
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
if (isLastNode(props.nodeModel)) {
set(props.nodeModel.properties.node_data, 'is_result', true)
}
}
set(props.nodeModel, 'validate', validate)
})
</script>
<style lang="scss" scoped></style>

View File

@ -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
}

View File

@ -0,0 +1,200 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<h5 class="title-decoration-1 mb-16">节点设置</h5>
<div class="flex-between">
<h5 class="lighter mb-8">输入变量</h5>
<el-button link type="primary" @click="openAddDialog()">
<el-icon class="mr-4"><Plus /></el-icon>
</el-button>
</div>
<el-form
@submit.prevent
@mousemove.stop
@mousedown.stop
@keydown.stop
@click.stop
ref="FunctionNodeFormRef"
:model="chat_data"
label-position="top"
require-asterisk-position="right"
hide-required-asterisk
>
<el-card shadow="never" class="card-never mb-16" style="--el-card-padding: 12px">
<div v-if="chat_data.input_field_list?.length > 0">
<template v-for="(item, index) in chat_data.input_field_list" :key="index">
<el-form-item
:label="item.name"
:prop="'input_field_list.' + index + '.value'"
:rules="{
required: item.is_required,
message: '请输入变量值',
trigger: 'blur'
}"
>
<template #label>
<div class="flex-between">
<div>
<span
>{{ item.name }} <span class="danger" v-if="item.is_required">*</span></span
>
<el-tag type="info" class="info-tag ml-4">{{ item.type }}</el-tag>
</div>
<div>
<el-button text @click.stop="openAddDialog(item, index)">
<el-icon><EditPen /></el-icon>
</el-button>
<el-button text @click="deleteField(index)" style="margin-left: 4px !important">
<el-icon>
<Delete />
</el-icon>
</el-button>
</div>
</div>
</template>
<NodeCascader
v-if="item.source === 'reference'"
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
placeholder="请选择变量"
v-model="item.value"
/>
<el-input v-else v-model="item.value" placeholder="请输入变量值" />
</el-form-item>
</template>
</div>
<el-text type="info" v-else> 暂无数据 </el-text>
</el-card>
<h5 class="lighter mb-8">Python 代码</h5>
<CodemirrorEditor
v-model:value="chat_data.code"
@change="changeCode"
@wheel="wheel"
@keydown="isKeyDown = true"
@keyup="isKeyDown = false"
class="mb-8"
v-if="showEditor"
/>
<el-form-item label="返回内容" @click.prevent>
<template #label>
<div class="flex align-center">
<div class="mr-4">
<span>返回内容<span class="danger">*</span></span>
</div>
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
<template #content>
关闭后该节点的内容则不输出给用户 如果你想让用户看到该节点的输出内容请打开开关
</template>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<el-switch size="small" v-model="chat_data.is_result" />
</el-form-item>
</el-form>
<FieldFormDialog ref="FieldFormDialogRef" @refresh="refreshFieldList" />
</NodeContainer>
</template>
<script setup lang="ts">
import { cloneDeep, set } from 'lodash'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import type { FormInstance } from 'element-plus'
import { ref, computed, onMounted } from 'vue'
import FieldFormDialog from '@/views/function-lib/component/FieldFormDialog.vue'
import { isLastNode } from '@/workflow/common/data'
const props = defineProps<{ nodeModel: any }>()
const isKeyDown = ref(false)
const wheel = (e: any) => {
if (isKeyDown.value) {
e.preventDefault()
} else {
e.stopPropagation()
return true
}
}
const FieldFormDialogRef = ref()
const nodeCascaderRef = ref()
const form = {
code: '',
input_field_list: [],
is_result: false
}
const currentIndex = ref<any>(null)
const showEditor = ref(false)
const chat_data = computed({
get: () => {
if (props.nodeModel.properties.node_data) {
return props.nodeModel.properties.node_data
} else {
set(props.nodeModel.properties, 'node_data', form)
}
return props.nodeModel.properties.node_data
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
}
})
const FunctionNodeFormRef = ref<FormInstance>()
const validate = () => {
return FunctionNodeFormRef.value?.validate().catch((err) => {
return Promise.reject({ node: props.nodeModel, errMessage: err })
})
}
function changeCode(value: string) {
set(props.nodeModel.properties.node_data, 'code', value)
}
function openAddDialog(data?: any, index?: any) {
if (typeof index !== 'undefined') {
currentIndex.value = index
}
FieldFormDialogRef.value.open(data)
}
function deleteField(index: any) {
const list = cloneDeep(props.nodeModel.properties.node_data.input_field_list)
list.splice(index, 1)
set(props.nodeModel.properties.node_data, 'input_field_list', list)
}
function refreshFieldList(data: any) {
const list = cloneDeep(props.nodeModel.properties.node_data.input_field_list)
const obj = {
value: '',
...data
}
if (currentIndex.value !== null) {
list.splice(currentIndex.value, 1, obj)
} else {
list.push(obj)
}
set(props.nodeModel.properties.node_data, 'input_field_list', list)
currentIndex.value = null
}
onMounted(() => {
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
if (isLastNode(props.nodeModel)) {
set(props.nodeModel.properties.node_data, 'is_result', true)
}
}
set(props.nodeModel, 'validate', validate)
setTimeout(() => {
showEditor.value = true
}, 100)
})
</script>
<style lang="scss" scoped></style>