diff --git a/apps/application/flow/common.py b/apps/application/flow/common.py index 675cc4a6387..abb439a7faf 100644 --- a/apps/application/flow/common.py +++ b/apps/application/flow/common.py @@ -99,6 +99,10 @@ class WorkflowMode(Enum): KNOWLEDGE_LOOP = "knowledge-loop" + TOOL = "tool" + + TOOL_LOOP = "tool-loop" + class Workflow: """ diff --git a/apps/application/flow/i_step_node.py b/apps/application/flow/i_step_node.py index 63806b4884d..c2daacfdadb 100644 --- a/apps/application/flow/i_step_node.py +++ b/apps/application/flow/i_step_node.py @@ -22,6 +22,7 @@ from application.models import ChatRecord, ChatUserType from common.field.common import InstanceField from knowledge.models.knowledge_action import KnowledgeAction, State +from tools.models import ToolRecord chat_cache = cache @@ -115,6 +116,40 @@ def handler(self, workflow): 'start_time') is not None else 0) +def get_tool_workflow_state(workflow): + if workflow.is_the_task_interrupted(): + return State.REVOKED + details = workflow.get_runtime_details() + node_list = details.values() + all_node = [*node_list, *get_loop_workflow_node(node_list)] + err = any([True for value in all_node if value.get('status') == 500 and not value.get('enableException')]) + if err: + return State.FAILURE + return State.SUCCESS + + +class ToolWorkflowPostHandler(WorkFlowPostHandler): + def __init__(self, chat_info, tool_id): + super().__init__(chat_info) + self.tool_id = tool_id + + def handler(self, workflow): + state = get_tool_workflow_state(workflow) + record = ToolRecord(id=self.chat_info.tool_record_id, tool_id=self.tool_id, + workspace_id=self.chat_info.workspace_id, + source_type=self.chat_info.source_type, + source_id=self.chat_info.source_id, + state=state, + meta={ + 'output': workflow.out_context, + 'details': workflow.get_runtime_details(), + 'answer_text_list': workflow.get_answer_text_list() + }) + self.chat_info.set_record(record) + self.chat_info = None + self.tool_id = None + + def get_loop_workflow_node(node_list): result = [] for item in node_list: @@ -204,6 +239,11 @@ class KnowledgeFlowParamsSerializer(serializers.Serializer): knowledge_base = serializers.DictField(required=False, label="知识库设置") +class ToolFlowParamsSerializer(serializers.Serializer): + tool_id = serializers.UUIDField(required=True, label="工具id") + workspace_id = serializers.CharField(required=True, label="工作空间id") + + class INode: view_type = 'many_view' diff --git a/apps/application/flow/knowledge_loop_workflow_manage.py b/apps/application/flow/knowledge_loop_workflow_manage.py index fbda319c1ad..31d3ab4df25 100644 --- a/apps/application/flow/knowledge_loop_workflow_manage.py +++ b/apps/application/flow/knowledge_loop_workflow_manage.py @@ -13,3 +13,9 @@ class KnowledgeLoopWorkflowManage(LoopWorkflowManage): def get_params_serializer_class(self): return KnowledgeFlowParamsSerializer + + def get_source_type(self): + return "KNOWLEDGE" + + def get_source_id(self): + return self.params.get('knowledge_id') diff --git a/apps/application/flow/knowledge_workflow_manage.py b/apps/application/flow/knowledge_workflow_manage.py index 4a19803a367..98212c9ee5a 100644 --- a/apps/application/flow/knowledge_workflow_manage.py +++ b/apps/application/flow/knowledge_workflow_manage.py @@ -122,3 +122,9 @@ def hand_node_result(self, current_node, node_result_future): current_node.node_chunk.end() QuerySet(KnowledgeAction).filter(id=self.params.get('knowledge_action_id')).update( details=self.get_runtime_details()) + + def get_source_type(self): + return "KNOWLEDGE" + + def get_source_id(self): + return self.params.get('knowledge_id') diff --git a/apps/application/flow/loop_workflow_manage.py b/apps/application/flow/loop_workflow_manage.py index 27c84f4dc66..c236b15dcc5 100644 --- a/apps/application/flow/loop_workflow_manage.py +++ b/apps/application/flow/loop_workflow_manage.py @@ -191,3 +191,9 @@ def generate_prompt(self, prompt: str): prompt_template = PromptTemplate.from_template(prompt, template_format='jinja2') value = prompt_template.format(context=context) return value + + def get_source_type(self): + return "APPLICATION" + + def get_source_id(self): + return self.params.get('application_id') diff --git a/apps/application/flow/step_node/__init__.py b/apps/application/flow/step_node/__init__.py index 52379486872..4c38020771e 100644 --- a/apps/application/flow/step_node/__init__.py +++ b/apps/application/flow/step_node/__init__.py @@ -35,11 +35,13 @@ from .text_to_video_step_node.impl.base_text_to_video_node import BaseTextToVideoNode from .tool_lib_node import * from .tool_node import * +from .tool_workflow_lib_node import BaseToolWorkflowLibNodeNode from .variable_aggregation_node.impl.base_variable_aggregation_node import BaseVariableAggregationNode from .variable_assign_node import BaseVariableAssignNode from .variable_splitting_node import BaseVariableSplittingNode from .video_understand_step_node import BaseVideoUnderstandNode from .document_split_node import BaseDocumentSplitNode +from .tool_start_node import BaseToolStartStepNode node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseSearchDocumentNode, BaseQuestionNode, BaseConditionNode, BaseReplyNode, @@ -51,7 +53,8 @@ BaseIntentNode, BaseLoopNode, BaseLoopStartStepNode, BaseLoopContinueNode, BaseLoopBreakNode, BaseVariableSplittingNode, BaseParameterExtractionNode, BaseVariableAggregationNode, - BaseDataSourceLocalNode, BaseDataSourceWebNode, BaseKnowledgeWriteNode, BaseDocumentSplitNode] + BaseDataSourceLocalNode, BaseDataSourceWebNode, BaseKnowledgeWriteNode, BaseDocumentSplitNode, + BaseToolStartStepNode, BaseToolWorkflowLibNodeNode] node_map = {n.type: {w: n for w in n.support} for n in node_list} diff --git a/apps/application/flow/step_node/ai_chat_step_node/i_chat_node.py b/apps/application/flow/step_node/ai_chat_step_node/i_chat_node.py index 77e53cd4a44..994fda75df6 100644 --- a/apps/application/flow/step_node/ai_chat_step_node/i_chat_node.py +++ b/apps/application/flow/step_node/ai_chat_step_node/i_chat_node.py @@ -16,7 +16,10 @@ class ChatNodeSerializer(serializers.Serializer): - model_id = serializers.CharField(required=True, label=_("Model id")) + model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id")) + model_id_type = serializers.CharField(required=False, default='custom', label=_("Model id type")) + model_id_reference = serializers.ListField(required=False, child=serializers.CharField(), allow_empty=True, + label=_("Reference Field")) system = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Role Setting")) prompt = serializers.CharField(required=True, label=_("Prompt word")) @@ -41,23 +44,24 @@ class ChatNodeSerializer(serializers.Serializer): tool_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True, label=_("Tool IDs"), ) application_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True, - label=_("App IDs"), ) + label=_("App IDs"), ) skill_tool_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True, - label=_("Skill IDs"), ) + label=_("Skill IDs"), ) mcp_output_enable = serializers.BooleanField(required=False, default=True, label=_("Whether to enable MCP output")) class IChatNode(INode): type = 'ai-chat-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP, - WorkflowMode.KNOWLEDGE] + WorkflowMode.KNOWLEDGE, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return ChatNodeSerializer def _run(self): - if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__( - self.workflow_manage.flow.workflow_mode): + if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, + WorkflowMode.TOOL_LOOP].__contains__( + self.workflow_manage.flow.workflow_mode): return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data, **{'history_chat_record': [], 'stream': True, 'chat_id': None, 'chat_record_id': None}) else: @@ -66,6 +70,8 @@ def _run(self): def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id, model_params_setting=None, + model_id_type=None, + model_id_reference=None, dialogue_type=None, model_setting=None, mcp_servers=None, diff --git a/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py b/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py index 8e083d77036..2cac27df950 100644 --- a/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py +++ b/apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py @@ -151,6 +151,8 @@ def save_context(self, details, workflow_manage): def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id, model_params_setting=None, + model_id_type=None, + model_id_reference=None, dialogue_type=None, model_setting=None, mcp_servers=None, @@ -165,8 +167,20 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record if dialogue_type is None: dialogue_type = 'WORKFLOW' - if model_params_setting is None: + if model_id_type == 'reference' and model_id_reference: + + reference_data = self.workflow_manage.get_reference_field( + model_id_reference[0], + model_id_reference[1:], + ) + + if reference_data and isinstance(reference_data, dict): + model_id = reference_data.get('model_id', model_id) + model_params_setting = reference_data.get('model_params_setting') + + if model_params_setting is None and model_id: model_params_setting = get_default_model_params_setting(model_id) + if model_setting is None: model_setting = {'reasoning_content_enable': False, 'reasoning_content_end': '', 'reasoning_content_start': ''} diff --git a/apps/application/flow/step_node/condition_node/i_condition_node.py b/apps/application/flow/step_node/condition_node/i_condition_node.py index 9dec6b0c649..664ee91baff 100644 --- a/apps/application/flow/step_node/condition_node/i_condition_node.py +++ b/apps/application/flow/step_node/condition_node/i_condition_node.py @@ -38,4 +38,5 @@ def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: type = 'condition-node' - support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP] + support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] diff --git a/apps/application/flow/step_node/direct_reply_node/i_reply_node.py b/apps/application/flow/step_node/direct_reply_node/i_reply_node.py index c1646a05494..9526d3c046a 100644 --- a/apps/application/flow/step_node/direct_reply_node/i_reply_node.py +++ b/apps/application/flow/step_node/direct_reply_node/i_reply_node.py @@ -40,13 +40,14 @@ def is_valid(self, *, raise_exception=False): class IReplyNode(INode): type = 'reply-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP, - WorkflowMode.KNOWLEDGE] + WorkflowMode.KNOWLEDGE, WorkflowMode.TOOL] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return ReplyNodeParamsSerializer def _run(self): - if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__( + if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, + WorkflowMode.TOOL_LOOP].__contains__( self.workflow_manage.flow.workflow_mode): return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data, **{'stream': True}) diff --git a/apps/application/flow/step_node/document_extract_node/i_document_extract_node.py b/apps/application/flow/step_node/document_extract_node/i_document_extract_node.py index eca4856ddb9..d2cf43e0238 100644 --- a/apps/application/flow/step_node/document_extract_node/i_document_extract_node.py +++ b/apps/application/flow/step_node/document_extract_node/i_document_extract_node.py @@ -16,7 +16,8 @@ class DocumentExtractNodeSerializer(serializers.Serializer): class IDocumentExtractNode(INode): type = 'document-extract-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP, - WorkflowMode.KNOWLEDGE] + WorkflowMode.KNOWLEDGE, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] + def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return DocumentExtractNodeSerializer diff --git a/apps/application/flow/step_node/document_split_node/i_document_split_node.py b/apps/application/flow/step_node/document_split_node/i_document_split_node.py index 6834238d3d0..e4a1c809fb1 100644 --- a/apps/application/flow/step_node/document_split_node/i_document_split_node.py +++ b/apps/application/flow/step_node/document_split_node/i_document_split_node.py @@ -72,7 +72,8 @@ class DocumentSplitNodeSerializer(serializers.Serializer): class IDocumentSplitNode(INode): type = 'document-split-node' support = [ - WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.KNOWLEDGE + WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.KNOWLEDGE, + WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP ] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: diff --git a/apps/application/flow/step_node/form_node/i_form_node.py b/apps/application/flow/step_node/form_node/i_form_node.py index a47a75ddcac..9be117f857f 100644 --- a/apps/application/flow/step_node/form_node/i_form_node.py +++ b/apps/application/flow/step_node/form_node/i_form_node.py @@ -25,7 +25,7 @@ class FormNodeParamsSerializer(serializers.Serializer): class IFormNode(INode): type = 'form-node' view_type = 'single_view' - support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP] + support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return FormNodeParamsSerializer diff --git a/apps/application/flow/step_node/form_node/impl/base_form_node.py b/apps/application/flow/step_node/form_node/impl/base_form_node.py index 930125124fa..30dfa97229b 100644 --- a/apps/application/flow/step_node/form_node/impl/base_form_node.py +++ b/apps/application/flow/step_node/form_node/impl/base_form_node.py @@ -144,8 +144,9 @@ def get_answer_list(self) -> List[Answer] | None: value = prompt_template.format(form=form, context=context, runtime_node_id=self.runtime_node_id, chat_record_id=self.flow_params_serializer.data.get("chat_record_id"), form_field_list=form_field_list) - return [Answer(value, self.view_type, self.runtime_node_id, self.workflow_params['chat_record_id'], None, - self.runtime_node_id, '')] + return [ + Answer(value, self.view_type, self.runtime_node_id, self.workflow_params.get('chat_record_id') or '', None, + self.runtime_node_id, '')] def get_details(self, index: int, **kwargs): form_content_format = self.context.get('form_content_format') diff --git a/apps/application/flow/step_node/image_generate_step_node/i_image_generate_node.py b/apps/application/flow/step_node/image_generate_step_node/i_image_generate_node.py index a3528d5fb13..13d227bc87f 100644 --- a/apps/application/flow/step_node/image_generate_step_node/i_image_generate_node.py +++ b/apps/application/flow/step_node/image_generate_step_node/i_image_generate_node.py @@ -33,14 +33,15 @@ class ImageGenerateNodeSerializer(serializers.Serializer): class IImageGenerateNode(INode): type = 'image-generate-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return ImageGenerateNodeSerializer def _run(self): - if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__( - self.workflow_manage.flow.workflow_mode): + if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, + WorkflowMode.TOOL_LOOP].__contains__( + self.workflow_manage.flow.workflow_mode): return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data, **{'history_chat_record': [], 'stream': True, 'chat_id': None, 'chat_record_id': None}) else: diff --git a/apps/application/flow/step_node/image_to_video_step_node/i_image_to_video_node.py b/apps/application/flow/step_node/image_to_video_step_node/i_image_to_video_node.py index 8e4aa9f0d33..adc6731ee0e 100644 --- a/apps/application/flow/step_node/image_to_video_step_node/i_image_to_video_node.py +++ b/apps/application/flow/step_node/image_to_video_step_node/i_image_to_video_node.py @@ -36,7 +36,8 @@ class ImageToVideoNodeSerializer(serializers.Serializer): class IImageToVideoNode(INode): type = 'image-to-video-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] + def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return ImageToVideoNodeSerializer @@ -57,11 +58,12 @@ def _run(self): if k not in ['first_frame_url', 'last_frame_url']} if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__( self.workflow_manage.flow.workflow_mode): - return self.execute(first_frame_url=first_frame_url, last_frame_url=last_frame_url, **node_params_data, **self.flow_params_serializer.data, + return self.execute(first_frame_url=first_frame_url, last_frame_url=last_frame_url, **node_params_data, + **self.flow_params_serializer.data, **{'history_chat_record': [], 'stream': True, 'chat_id': None, 'chat_record_id': None}) else: return self.execute(first_frame_url=first_frame_url, last_frame_url=last_frame_url, - **node_params_data, **self.flow_params_serializer.data) + **node_params_data, **self.flow_params_serializer.data) def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_type, history_chat_record, model_params_setting, diff --git a/apps/application/flow/step_node/image_understand_step_node/i_image_understand_node.py b/apps/application/flow/step_node/image_understand_step_node/i_image_understand_node.py index 325ef935fe8..aa2932689a1 100644 --- a/apps/application/flow/step_node/image_understand_step_node/i_image_understand_node.py +++ b/apps/application/flow/step_node/image_understand_step_node/i_image_understand_node.py @@ -32,7 +32,7 @@ class ImageUnderstandNodeSerializer(serializers.Serializer): class IImageUnderstandNode(INode): type = 'image-understand-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return ImageUnderstandNodeSerializer @@ -40,10 +40,11 @@ def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: def _run(self): res = self.workflow_manage.get_reference_field(self.node_params_serializer.data.get('image_list')[0], self.node_params_serializer.data.get('image_list')[1:]) - if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__( - self.workflow_manage.flow.workflow_mode): + if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, + WorkflowMode.TOOL_LOOP].__contains__( + self.workflow_manage.flow.workflow_mode): return self.execute(image=res, **self.node_params_serializer.data, **self.flow_params_serializer.data, - **{'history_chat_record': [], 'stream': True, 'chat_record_id': None}) + **{'history_chat_record': [], 'stream': True, 'chat_record_id': None}) else: return self.execute(image=res, **self.node_params_serializer.data, **self.flow_params_serializer.data) diff --git a/apps/application/flow/step_node/intent_node/i_intent_node.py b/apps/application/flow/step_node/intent_node/i_intent_node.py index d609359d471..de09826b392 100644 --- a/apps/application/flow/step_node/intent_node/i_intent_node.py +++ b/apps/application/flow/step_node/intent_node/i_intent_node.py @@ -28,7 +28,7 @@ class IntentNodeSerializer(serializers.Serializer): class IIntentNode(INode): type = 'intent-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def save_context(self, details, workflow_manage): pass @@ -41,8 +41,9 @@ def _run(self): self.node_params_serializer.data.get('content_list')[0], self.node_params_serializer.data.get('content_list')[1:], ) - if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__( - self.workflow_manage.flow.workflow_mode): + if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, + WorkflowMode.TOOL_LOOP].__contains__( + self.workflow_manage.flow.workflow_mode): return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data, **{'history_chat_record': [], 'stream': True, 'chat_id': None, 'chat_record_id': None, 'user_input': str(question)}) diff --git a/apps/application/flow/step_node/loop_break_node/i_loop_break_node.py b/apps/application/flow/step_node/loop_break_node/i_loop_break_node.py index bc3bf739a35..07edf227b53 100644 --- a/apps/application/flow/step_node/loop_break_node/i_loop_break_node.py +++ b/apps/application/flow/step_node/loop_break_node/i_loop_break_node.py @@ -29,7 +29,7 @@ class LoopBreakNodeSerializer(serializers.Serializer): class ILoopBreakNode(INode): type = 'loop-break-node' - support = [WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP] + support = [WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return LoopBreakNodeSerializer diff --git a/apps/application/flow/step_node/loop_continue_node/i_loop_continue_node.py b/apps/application/flow/step_node/loop_continue_node/i_loop_continue_node.py index efb21437d46..00b6aa04c39 100644 --- a/apps/application/flow/step_node/loop_continue_node/i_loop_continue_node.py +++ b/apps/application/flow/step_node/loop_continue_node/i_loop_continue_node.py @@ -28,7 +28,7 @@ class LoopContinueNodeSerializer(serializers.Serializer): class ILoopContinueNode(INode): type = 'loop-continue-node' - support = [WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP] + support = [WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return LoopContinueNodeSerializer diff --git a/apps/application/flow/step_node/loop_node/i_loop_node.py b/apps/application/flow/step_node/loop_node/i_loop_node.py index fb0fd914778..e16dbebc059 100644 --- a/apps/application/flow/step_node/loop_node/i_loop_node.py +++ b/apps/application/flow/step_node/loop_node/i_loop_node.py @@ -41,7 +41,7 @@ def is_valid(self, *, raise_exception=False): class ILoopNode(INode): type = 'loop-node' - support = [WorkflowMode.APPLICATION, WorkflowMode.KNOWLEDGE] + support = [WorkflowMode.APPLICATION, WorkflowMode.KNOWLEDGE, WorkflowMode.TOOL] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return ILoopNodeSerializer diff --git a/apps/application/flow/step_node/loop_node/impl/base_loop_node.py b/apps/application/flow/step_node/loop_node/impl/base_loop_node.py index 7ffbaf26d58..6b03d628664 100644 --- a/apps/application/flow/step_node/loop_node/impl/base_loop_node.py +++ b/apps/application/flow/step_node/loop_node/impl/base_loop_node.py @@ -268,10 +268,16 @@ def get_loop_context(self): def execute(self, loop_type, array, number, loop_body, **kwargs) -> NodeResult: from application.flow.loop_workflow_manage import LoopWorkflowManage, Workflow from application.flow.knowledge_loop_workflow_manage import KnowledgeLoopWorkflowManage + from application.flow.tool_loop_workflow_manage import ToolLoopWorkflowManage def workflow_manage_new_instance(loop_data, global_data, start_node_id=None, start_node_data=None, chat_record=None, child_node=None): - workflow_mode = WorkflowMode.KNOWLEDGE_LOOP if WorkflowMode.KNOWLEDGE == self.workflow_manage.flow.workflow_mode else WorkflowMode.APPLICATION_LOOP - c = KnowledgeLoopWorkflowManage if workflow_mode == WorkflowMode.KNOWLEDGE_LOOP else LoopWorkflowManage + workflow_mode = {WorkflowMode.APPLICATION: WorkflowMode.APPLICATION_LOOP, + WorkflowMode.KNOWLEDGE: WorkflowMode.KNOWLEDGE_LOOP, + WorkflowMode.TOOL: WorkflowMode.TOOL_LOOP}.get( + self.workflow_manage.flow.workflow_mode) or WorkflowMode.APPLICATION + c = {WorkflowMode.APPLICATION_LOOP: LoopWorkflowManage, + WorkflowMode.KNOWLEDGE_LOOP: KnowledgeLoopWorkflowManage, + WorkflowMode.TOOL_LOOP: ToolLoopWorkflowManage}.get(workflow_mode) or LoopWorkflowManage workflow_manage = c(Workflow.new_instance(loop_body, workflow_mode), self.workflow_manage.params, LoopWorkFlowPostHandler( diff --git a/apps/application/flow/step_node/loop_start_node/i_loop_start_node.py b/apps/application/flow/step_node/loop_start_node/i_loop_start_node.py index 3c50b33b2df..7c3ffa31413 100644 --- a/apps/application/flow/step_node/loop_start_node/i_loop_start_node.py +++ b/apps/application/flow/step_node/loop_start_node/i_loop_start_node.py @@ -12,7 +12,7 @@ class ILoopStarNode(INode): type = 'loop-start-node' - support = [WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP] + support = [WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL_LOOP] def _run(self): return self.execute(**self.flow_params_serializer.data) diff --git a/apps/application/flow/step_node/mcp_node/i_mcp_node.py b/apps/application/flow/step_node/mcp_node/i_mcp_node.py index d0a7be13322..6dd3827d640 100644 --- a/apps/application/flow/step_node/mcp_node/i_mcp_node.py +++ b/apps/application/flow/step_node/mcp_node/i_mcp_node.py @@ -21,7 +21,7 @@ class McpNodeSerializer(serializers.Serializer): class IMcpNode(INode): type = 'mcp-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return McpNodeSerializer diff --git a/apps/application/flow/step_node/parameter_extraction_node/i_parameter_extraction_node.py b/apps/application/flow/step_node/parameter_extraction_node/i_parameter_extraction_node.py index a0a9bc5cb59..f36cd4c26ce 100644 --- a/apps/application/flow/step_node/parameter_extraction_node/i_parameter_extraction_node.py +++ b/apps/application/flow/step_node/parameter_extraction_node/i_parameter_extraction_node.py @@ -25,7 +25,7 @@ class VariableSplittingNodeParamsSerializer(serializers.Serializer): class IParameterExtractionNode(INode): type = 'parameter-extraction-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return VariableSplittingNodeParamsSerializer diff --git a/apps/application/flow/step_node/question_node/i_question_node.py b/apps/application/flow/step_node/question_node/i_question_node.py index 19399976054..63d5454a4bd 100644 --- a/apps/application/flow/step_node/question_node/i_question_node.py +++ b/apps/application/flow/step_node/question_node/i_question_node.py @@ -33,7 +33,7 @@ class QuestionNodeSerializer(serializers.Serializer): class IQuestionNode(INode): type = 'question-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return QuestionNodeSerializer diff --git a/apps/application/flow/step_node/reranker_node/i_reranker_node.py b/apps/application/flow/step_node/reranker_node/i_reranker_node.py index 0421c55b00a..2829eb50e5f 100644 --- a/apps/application/flow/step_node/reranker_node/i_reranker_node.py +++ b/apps/application/flow/step_node/reranker_node/i_reranker_node.py @@ -42,7 +42,7 @@ def is_valid(self, *, raise_exception=False): class IRerankerNode(INode): type = 'reranker-node' - support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP] + support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return RerankerStepNodeSerializer diff --git a/apps/application/flow/step_node/search_document_node/i_search_document_node.py b/apps/application/flow/step_node/search_document_node/i_search_document_node.py index 2d55ef64bae..0a2c99a1e71 100644 --- a/apps/application/flow/step_node/search_document_node/i_search_document_node.py +++ b/apps/application/flow/step_node/search_document_node/i_search_document_node.py @@ -43,7 +43,7 @@ def is_valid(self, *, raise_exception=False): class ISearchDocumentStepNode(INode): type = 'search-document-node' - support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP] + support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return SearchDocumentStepNodeSerializer diff --git a/apps/application/flow/step_node/search_knowledge_node/i_search_knowledge_node.py b/apps/application/flow/step_node/search_knowledge_node/i_search_knowledge_node.py index 17da82a4a27..0cf23cb5e5d 100644 --- a/apps/application/flow/step_node/search_knowledge_node/i_search_knowledge_node.py +++ b/apps/application/flow/step_node/search_knowledge_node/i_search_knowledge_node.py @@ -68,7 +68,7 @@ def get_paragraph_list(chat_record, node_id): class ISearchKnowledgeStepNode(INode): type = 'search-knowledge-node' - support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP] + support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return SearchDatasetStepNodeSerializer diff --git a/apps/application/flow/step_node/speech_to_text_step_node/i_speech_to_text_node.py b/apps/application/flow/step_node/speech_to_text_step_node/i_speech_to_text_node.py index a071b46d1cb..acb82affa90 100644 --- a/apps/application/flow/step_node/speech_to_text_step_node/i_speech_to_text_node.py +++ b/apps/application/flow/step_node/speech_to_text_step_node/i_speech_to_text_node.py @@ -24,7 +24,7 @@ class SpeechToTextNodeSerializer(serializers.Serializer): class ISpeechToTextNode(INode): type = 'speech-to-text-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP,WorkflowMode.TOOL,WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return SpeechToTextNodeSerializer diff --git a/apps/application/flow/step_node/text_to_speech_step_node/i_text_to_speech_node.py b/apps/application/flow/step_node/text_to_speech_step_node/i_text_to_speech_node.py index 975a6e98166..a1047dc71c1 100644 --- a/apps/application/flow/step_node/text_to_speech_step_node/i_text_to_speech_node.py +++ b/apps/application/flow/step_node/text_to_speech_step_node/i_text_to_speech_node.py @@ -24,7 +24,7 @@ class TextToSpeechNodeSerializer(serializers.Serializer): class ITextToSpeechNode(INode): type = 'text-to-speech-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return TextToSpeechNodeSerializer diff --git a/apps/application/flow/step_node/text_to_video_step_node/i_text_to_video_node.py b/apps/application/flow/step_node/text_to_video_step_node/i_text_to_video_node.py index e596f00d1cb..66d57e74c78 100644 --- a/apps/application/flow/step_node/text_to_video_step_node/i_text_to_video_node.py +++ b/apps/application/flow/step_node/text_to_video_step_node/i_text_to_video_node.py @@ -33,14 +33,15 @@ class TextToVideoNodeSerializer(serializers.Serializer): class ITextToVideoNode(INode): type = 'text-to-video-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return TextToVideoNodeSerializer def _run(self): - if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__( - self.workflow_manage.flow.workflow_mode): + if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, + WorkflowMode.TOOL_LOOP].__contains__( + self.workflow_manage.flow.workflow_mode): return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data, **{'history_chat_record': [], 'stream': True, 'chat_id': None, 'chat_record_id': None}) else: diff --git a/apps/application/flow/step_node/tool_lib_node/i_tool_lib_node.py b/apps/application/flow/step_node/tool_lib_node/i_tool_lib_node.py index e1d9335ce3d..08f3e3a845d 100644 --- a/apps/application/flow/step_node/tool_lib_node/i_tool_lib_node.py +++ b/apps/application/flow/step_node/tool_lib_node/i_tool_lib_node.py @@ -42,7 +42,7 @@ def is_valid(self, *, raise_exception=False): class IToolLibNode(INode): type = 'tool-lib-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return FunctionLibNodeParamsSerializer diff --git a/apps/application/flow/step_node/tool_node/i_tool_node.py b/apps/application/flow/step_node/tool_node/i_tool_node.py index 1b34e96fee9..4f8343a67db 100644 --- a/apps/application/flow/step_node/tool_node/i_tool_node.py +++ b/apps/application/flow/step_node/tool_node/i_tool_node.py @@ -54,7 +54,7 @@ def is_valid(self, *, raise_exception=False): class IToolNode(INode): type = 'tool-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return FunctionNodeParamsSerializer diff --git a/apps/application/flow/step_node/tool_start_node/__init__.py b/apps/application/flow/step_node/tool_start_node/__init__.py new file mode 100644 index 00000000000..98a1afcd904 --- /dev/null +++ b/apps/application/flow/step_node/tool_start_node/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py + @date:2024/6/11 15:30 + @desc: +""" +from .impl import * diff --git a/apps/application/flow/step_node/tool_start_node/i_tool_start_node.py b/apps/application/flow/step_node/tool_start_node/i_tool_start_node.py new file mode 100644 index 00000000000..ca313277376 --- /dev/null +++ b/apps/application/flow/step_node/tool_start_node/i_tool_start_node.py @@ -0,0 +1,21 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: i_start_node.py + @date:2024/6/3 16:54 + @desc: +""" +from application.flow.common import WorkflowMode +from application.flow.i_step_node import INode, NodeResult + + +class IToolStartNode(INode): + type = 'tool-start-node' + support = [WorkflowMode.TOOL] + + def _run(self): + return self.execute(**self.flow_params_serializer.data) + + def execute(self, **kwargs) -> NodeResult: + pass diff --git a/apps/application/flow/step_node/tool_start_node/impl/__init__.py b/apps/application/flow/step_node/tool_start_node/impl/__init__.py new file mode 100644 index 00000000000..6fcd243dc5c --- /dev/null +++ b/apps/application/flow/step_node/tool_start_node/impl/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py + @date:2024/6/11 15:36 + @desc: +""" +from .base_tool_start_node import BaseToolStartStepNode diff --git a/apps/application/flow/step_node/tool_start_node/impl/base_tool_start_node.py b/apps/application/flow/step_node/tool_start_node/impl/base_tool_start_node.py new file mode 100644 index 00000000000..05a2675eb68 --- /dev/null +++ b/apps/application/flow/step_node/tool_start_node/impl/base_tool_start_node.py @@ -0,0 +1,66 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: base_start_node.py + @date:2024/6/3 17:17 + @desc: +""" +from typing import Type + +from rest_framework import serializers + +from application.flow.i_step_node import NodeResult +from application.flow.step_node.tool_start_node.i_tool_start_node import IToolStartNode + + +class BaseToolStartStepNode(IToolStartNode): + def save_context(self, details, workflow_manage): + base_node = self.workflow_manage.get_base_node() + workflow_variable = {} + self.context['exception_message'] = details.get('err_message') + self.status = details.get('status') + self.err_message = details.get('err_message') + for key, value in workflow_variable.items(): + workflow_manage.context[key] = value + for item in details.get('global_fields', []): + workflow_manage.context[item.get('key')] = item.get('value') + + def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: + pass + + def execute(self, **kwargs) -> NodeResult: + base_node = self.workflow_manage.get_base_node() + global_value = {} + params = self.workflow_manage.get_body() + for item in base_node.properties.get('user_input_field_list', []): + global_value[item.get('field')] = params[item.get('field')] + + self.workflow_manage.out_context = { + item.get('field'): None + for item in base_node.properties.get('user_output_field_list', []) + if item.get('default_value', None) is not None + } + return NodeResult({}, global_value) + + def get_details(self, index: int, **kwargs): + global_fields = [] + for field in self.node.properties.get('config')['globalFields']: + key = field['value'] + global_fields.append({ + 'label': field['label'], + 'key': key, + 'value': self.workflow_manage.context[key] if key in self.workflow_manage.context else '' + }) + return { + 'name': self.node.properties.get('stepName'), + "index": index, + "question": self.context.get('question'), + 'run_time': self.context.get('run_time'), + 'type': self.node.type, + 'status': self.status, + 'err_message': self.err_message, + 'global_fields': global_fields, + '': '', + 'enableException': self.node.properties.get('enableException'), + } diff --git a/apps/application/flow/step_node/tool_workflow_lib_node/__init__.py b/apps/application/flow/step_node/tool_workflow_lib_node/__init__.py new file mode 100644 index 00000000000..d417d531251 --- /dev/null +++ b/apps/application/flow/step_node/tool_workflow_lib_node/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2026/3/16 13:53 + @desc: +""" +from .impl import * \ No newline at end of file diff --git a/apps/application/flow/step_node/tool_workflow_lib_node/i_tool_workflow_lib_node.py b/apps/application/flow/step_node/tool_workflow_lib_node/i_tool_workflow_lib_node.py new file mode 100644 index 00000000000..82b73d0904b --- /dev/null +++ b/apps/application/flow/step_node/tool_workflow_lib_node/i_tool_workflow_lib_node.py @@ -0,0 +1,57 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: i_function_lib_node.py + @date:2024/8/8 16:21 + @desc: +""" +from typing import Type + +from django.db import connection +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from application.flow.common import WorkflowMode +from application.flow.i_step_node import INode, NodeResult +from common.field.common import ObjectField +from tools.models.tool import Tool, ToolType + + +class InputField(serializers.Serializer): + field = serializers.CharField(required=True, label=_('Variable Name')) + label = serializers.CharField(required=True, label=_('Variable Label')) + source = serializers.CharField(required=True, label=_('Variable Source')) + type = serializers.CharField(required=True, label=_('Variable Type')) + value = ObjectField(required=True, label=_("Variable Value"), model_type_list=[str, list, bool, dict, int, float]) + + +class FunctionLibNodeParamsSerializer(serializers.Serializer): + tool_lib_id = serializers.UUIDField(required=True, label=_('Library ID')) + input_field_list = InputField(required=True, many=True) + is_result = serializers.BooleanField(required=False, + label=_('Whether to return content')) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + f_lib = QuerySet(Tool).filter(id=self.data.get('tool_lib_id'), tool_type=ToolType.WORKFLOW).first() + # 归还链接到连接池 + connection.close() + if f_lib is None: + raise Exception(_('The function has been deleted')) + + +class IToolWorkflowLibNode(INode): + type = 'tool-workflow-lib-node' + support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] + + 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, tool_lib_id, input_field_list, **kwargs) -> NodeResult: + pass diff --git a/apps/application/flow/step_node/tool_workflow_lib_node/impl/__init__.py b/apps/application/flow/step_node/tool_workflow_lib_node/impl/__init__.py new file mode 100644 index 00000000000..0b593554784 --- /dev/null +++ b/apps/application/flow/step_node/tool_workflow_lib_node/impl/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2026/3/16 13:53 + @desc: +""" +from .base_tool_workflow_lib_node import * diff --git a/apps/application/flow/step_node/tool_workflow_lib_node/impl/base_tool_workflow_lib_node.py b/apps/application/flow/step_node/tool_workflow_lib_node/impl/base_tool_workflow_lib_node.py new file mode 100644 index 00000000000..89541df2966 --- /dev/null +++ b/apps/application/flow/step_node/tool_workflow_lib_node/impl/base_tool_workflow_lib_node.py @@ -0,0 +1,217 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: base_tool_workflow_lib_node.py.py + @date:2026/3/16 13:55 + @desc: +""" + +import time +from typing import Dict + +import uuid_utils.compat as uuid +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ + +from application.flow.common import WorkflowMode, Workflow +from application.flow.i_step_node import NodeResult, ToolWorkflowPostHandler, INode +from application.flow.step_node.tool_workflow_lib_node.i_tool_workflow_lib_node import IToolWorkflowLibNode +from application.models import ChatRecord +from application.serializers.common import ToolExecute +from common.exception.app_exception import ChatException +from common.handle.impl.response.loop_to_response import LoopToResponse +from tools.models import ToolWorkflowVersion + + +def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str, + reasoning_content: str): + result = node_variable.get('result') + node.context['application_node_dict'] = node_variable.get('application_node_dict') + node.context['node_dict'] = node_variable.get('node_dict', {}) + node.context['is_interrupt_exec'] = node_variable.get('is_interrupt_exec') + node.context['message_tokens'] = result.get('usage', {}).get('prompt_tokens', 0) + node.context['answer_tokens'] = result.get('usage', {}).get('completion_tokens', 0) + node.context['answer'] = answer + node.context['result'] = answer + node.context['reasoning_content'] = reasoning_content + node.context['run_time'] = time.time() - node.context['start_time'] + if workflow.is_result(node, NodeResult(node_variable, workflow_variable)): + node.answer_text = answer + + +def get_answer_list(instance, child_node_node_dict, runtime_node_id): + answer_list = instance.get_record_answer_list() + for a in answer_list: + _v = child_node_node_dict.get(a.get('runtime_node_id')) + if _v: + a['runtime_node_id'] = runtime_node_id + a['child_node'] = _v + return answer_list + + +def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INode, workflow): + """ + 写入上下文数据 (流式) + @param node_variable: 节点数据 + @param workflow_variable: 全局数据 + @param node: 节点 + @param workflow: 工作流管理器 + """ + workflow_manage_new_instance = node_variable.get('workflow_manage_new_instance') + node_params = node.node_params + start_node_id = node_params.get('child_node', {}).get('runtime_node_id') + child_node_data = node.context.get('child_node_data') or [] + start_node_data = None + chat_record = None + child_node = None + if start_node_id: + chat_record_id = node_params.get('child_node', {}).get('chat_record_id') + child_node = node_params.get('child_node', {}).get('child_node') + start_node_data = node_params.get('node_data') + chat_record = ChatRecord(id=chat_record_id, answer_text_list=[], answer_text='', + details=child_node_data) + instance = workflow_manage_new_instance(start_node_id, + start_node_data, chat_record, child_node) + answer = '' + reasoning_content = '' + usage = {} + node_child_node = {} + is_interrupt_exec = False + response = instance.stream() + child_node_node_dict = {} + for chunk in response: + response_content = chunk + content = (response_content.get('content', '') or '') + runtime_node_id = response_content.get('runtime_node_id', '') + chat_record_id = response_content.get('chat_record_id', '') + child_node = response_content.get('child_node') + node_type = response_content.get('node_type') + _reasoning_content = (response_content.get('reasoning_content', '') or '') + if node_type == 'form-node': + is_interrupt_exec = True + answer += content + reasoning_content += _reasoning_content + node_child_node = {'runtime_node_id': runtime_node_id, 'chat_record_id': chat_record_id, + 'child_node': child_node} + + child_node = chunk.get('child_node') + runtime_node_id = chunk.get('runtime_node_id', '') + chat_record_id = chunk.get('chat_record_id', '') + child_node_node_dict[runtime_node_id] = { + 'runtime_node_id': runtime_node_id, + 'chat_record_id': chat_record_id, + 'child_node': child_node} + content_chunk = (chunk.get('content', '') or '') + reasoning_content_chunk = (chunk.get('reasoning_content', '') or '') + reasoning_content += reasoning_content_chunk + answer += content_chunk + yield chunk + if chunk.get('node_status', "SUCCESS") == 'ERROR': + is_interrupt_exec = True + node.status = 500 + node.err_message = chunk.get('content') + usage = response_content.get('usage', {}) + child_answer_data = get_answer_list(instance, child_node_node_dict, node.runtime_node_id) + node.context['usage'] = {'usage': usage} + node.context['child_node'] = node_child_node + node.context['child_node_data'] = instance.get_runtime_details() + node.context['is_interrupt_exec'] = is_interrupt_exec + node.context['child_node_data'] = instance.get_runtime_details() + node.context['child_answer_data'] = child_answer_data + node.context['run_time'] = time.time() - node.context.get("start_time") + for key, value in instance.out_context.items(): + node.context[key] = value + + +def _is_interrupt_exec(node, node_variable: Dict, workflow_variable: Dict): + return node.context.get('is_interrupt_exec', False) + + +class BaseToolWorkflowLibNodeNode(IToolWorkflowLibNode): + def get_parameters(self, input_field_list): + result = {} + for input in input_field_list: + source = input.get('source') + value = input.get('value') + if source == 'reference': + value = self.workflow_manage.get_reference_field( + value[0], + value[1:]) + result[input.get('field')] = value + + return result + + def save_context(self, details, workflow_manage): + self.context['child_answer_data'] = details.get('child_answer_data') + self.context['child_node_data'] = details.get('child_node_data') + self.context['result'] = details.get('result') + self.context['exception_message'] = details.get('err_message') + if self.node_params.get('is_result'): + self.answer_text = str(details.get('result')) + + @staticmethod + def to_chat_record(record): + if record is None: + return None + return ChatRecord( + answer_text_list=record.meta.get('answer_text_list'), + details=record.meta.get('details'), + answer_text='', + ) + + def execute(self, tool_lib_id, input_field_list, **kwargs) -> NodeResult: + from application.flow.tool_workflow_manage import ToolWorkflowManage + workspace_id = self.workflow_manage.get_body().get('workspace_id') + tool_workflow_version = QuerySet(ToolWorkflowVersion).filter(tool_id=tool_lib_id).order_by( + '-create_time')[0:1].first() + if tool_workflow_version is None: + raise ChatException(500, _("The tool has not been published. Please use it after publishing.")) + parameters = self.get_parameters(input_field_list) + tool_record_id = (self.node_params.get('child_node') or {}).get('chat_record_id') or str(uuid.uuid7()) + took_execute = ToolExecute(tool_lib_id, tool_record_id, + workspace_id, + self.workflow_manage.get_source_type(), + self.workflow_manage.get_source_id(), + False) + + def workflow_manage_new_instance(start_node_id=None, + start_node_data=None, chat_record=None, child_node=None): + work_flow_manage = ToolWorkflowManage( + Workflow.new_instance(tool_workflow_version.work_flow, WorkflowMode.TOOL), + { + 'chat_record_id': tool_record_id, + 'tool_id': tool_lib_id, + 'stream': True, + 'workspace_id': workspace_id, + **parameters}, + ToolWorkflowPostHandler(took_execute, tool_lib_id), + base_to_response=LoopToResponse(), + start_node_id=start_node_id, + start_node_data=start_node_data, + child_node=child_node, + chat_record=self.to_chat_record(took_execute.get_record()), + is_the_task_interrupted=lambda: False) + + return work_flow_manage + + return NodeResult({'workflow_manage_new_instance': workflow_manage_new_instance}, + {}, _write_context=write_context_stream, + _is_interrupt=_is_interrupt_exec) + + def get_details(self, index: int, **kwargs): + result = self.context.get('result') + + return { + 'name': self.node.properties.get('stepName'), + "index": index, + "result": result, + "params": self.context.get('params'), + 'run_time': self.context.get('run_time'), + 'type': self.node.type, + 'status': self.status, + 'child_node_data': self.context.get("child_node_data"), + 'child_answer_data': self.context.get("child_answer_data"), + 'err_message': self.err_message, + 'enableException': self.node.properties.get('enableException'), + } diff --git a/apps/application/flow/step_node/variable_aggregation_node/i_variable_aggregation_node.py b/apps/application/flow/step_node/variable_aggregation_node/i_variable_aggregation_node.py index 4878f0c4c0c..0e000e3d5ec 100644 --- a/apps/application/flow/step_node/variable_aggregation_node/i_variable_aggregation_node.py +++ b/apps/application/flow/step_node/variable_aggregation_node/i_variable_aggregation_node.py @@ -28,7 +28,8 @@ class VariableAggregationNodeSerializer(serializers.Serializer): class IVariableAggregation(INode): type = 'variable-aggregation-node' - support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP] + support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return VariableAggregationNodeSerializer diff --git a/apps/application/flow/step_node/variable_assign_node/i_variable_assign_node.py b/apps/application/flow/step_node/variable_assign_node/i_variable_assign_node.py index b65d8c81274..6652cbe9e9a 100644 --- a/apps/application/flow/step_node/variable_assign_node/i_variable_assign_node.py +++ b/apps/application/flow/step_node/variable_assign_node/i_variable_assign_node.py @@ -17,7 +17,7 @@ class VariableAssignNodeParamsSerializer(serializers.Serializer): class IVariableAssignNode(INode): type = 'variable-assign-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return VariableAssignNodeParamsSerializer diff --git a/apps/application/flow/step_node/variable_assign_node/impl/base_variable_assign_node.py b/apps/application/flow/step_node/variable_assign_node/impl/base_variable_assign_node.py index 3ff1982f573..f1711009a47 100644 --- a/apps/application/flow/step_node/variable_assign_node/impl/base_variable_assign_node.py +++ b/apps/application/flow/step_node/variable_assign_node/impl/base_variable_assign_node.py @@ -31,6 +31,13 @@ def chat_evaluation(self, variable, value): else: self.workflow_manage.chat_context[variable['fields'][1]] = value + def out_evaluation(self, variable, value): + from application.flow.loop_workflow_manage import LoopWorkflowManage + if isinstance(self.workflow_manage, LoopWorkflowManage): + self.workflow_manage.parentWorkflowManage.out_context[variable['fields'][1]] = value + else: + self.workflow_manage.out_context[variable['fields'][1]] = value + def handle(self, variable, evaluation): result = { 'name': variable['name'], @@ -76,7 +83,9 @@ def execute(self, variable_list, **kwargs) -> NodeResult: if 'loop' == variable['fields'][0]: result = self.handle(variable, self.loop_evaluation) result_list.append(result) - + if 'output' == variable['fields'][0]: + result = self.handle(variable, self.out_evaluation) + result_list.append(result) if is_chat: from application.flow.loop_workflow_manage import LoopWorkflowManage if isinstance(self.workflow_manage, LoopWorkflowManage): diff --git a/apps/application/flow/step_node/variable_splitting_node/i_variable_splitting_node.py b/apps/application/flow/step_node/variable_splitting_node/i_variable_splitting_node.py index 78983ca0a70..39c48f817be 100644 --- a/apps/application/flow/step_node/variable_splitting_node/i_variable_splitting_node.py +++ b/apps/application/flow/step_node/variable_splitting_node/i_variable_splitting_node.py @@ -20,7 +20,7 @@ class VariableSplittingNodeParamsSerializer(serializers.Serializer): class IVariableSplittingNode(INode): type = 'variable-splitting-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return VariableSplittingNodeParamsSerializer diff --git a/apps/application/flow/step_node/video_understand_step_node/i_video_understand_node.py b/apps/application/flow/step_node/video_understand_step_node/i_video_understand_node.py index 0b362415d36..25d2971959c 100644 --- a/apps/application/flow/step_node/video_understand_step_node/i_video_understand_node.py +++ b/apps/application/flow/step_node/video_understand_step_node/i_video_understand_node.py @@ -31,7 +31,7 @@ class VideoUnderstandNodeSerializer(serializers.Serializer): class IVideoUnderstandNode(INode): type = 'video-understand-node' support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, - WorkflowMode.KNOWLEDGE_LOOP] + WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP] def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: return VideoUnderstandNodeSerializer @@ -40,10 +40,11 @@ def _run(self): res = self.workflow_manage.get_reference_field(self.node_params_serializer.data.get('video_list')[0], self.node_params_serializer.data.get('video_list')[1:]) - if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__( + if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, + WorkflowMode.TOOL_LOOP].__contains__( self.workflow_manage.flow.workflow_mode): return self.execute(video=res, **self.node_params_serializer.data, **self.flow_params_serializer.data, - **{'history_chat_record': [], 'stream': True, 'chat_id': None, 'chat_record_id': None}) + **{'history_chat_record': [], 'stream': True, 'chat_id': None, 'chat_record_id': None}) else: return self.execute(video=res, **self.node_params_serializer.data, **self.flow_params_serializer.data) diff --git a/apps/application/flow/tool_loop_workflow_manage.py b/apps/application/flow/tool_loop_workflow_manage.py new file mode 100644 index 00000000000..9fc2425f014 --- /dev/null +++ b/apps/application/flow/tool_loop_workflow_manage.py @@ -0,0 +1,21 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: workflow_manage.py + @date:2024/1/9 17:40 + @desc: +""" +from application.flow.i_step_node import ToolFlowParamsSerializer +from application.flow.loop_workflow_manage import LoopWorkflowManage + + +class ToolLoopWorkflowManage(LoopWorkflowManage): + def get_params_serializer_class(self): + return ToolFlowParamsSerializer + + def get_source_type(self): + return "TOOL" + + def get_source_id(self): + return self.params.get('tool_id') diff --git a/apps/application/flow/tool_workflow_manage.py b/apps/application/flow/tool_workflow_manage.py new file mode 100644 index 00000000000..4b42983e592 --- /dev/null +++ b/apps/application/flow/tool_workflow_manage.py @@ -0,0 +1,55 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: tool_workflow_manage.py + @date:2026/3/12 15:17 + @desc: +""" +from concurrent.futures import ThreadPoolExecutor + +from django.db import close_old_connections +from django.utils.translation import get_language + +from application.flow.common import Workflow +from application.flow.i_step_node import WorkFlowPostHandler, ToolFlowParamsSerializer +from application.flow.workflow_manage import WorkflowManage +from common.handle.base_to_response import BaseToResponse +from common.handle.impl.response.system_to_response import SystemToResponse + +executor = ThreadPoolExecutor(max_workers=200) + + +class ToolWorkflowManage(WorkflowManage): + def __init__(self, flow: Workflow, params, work_flow_post_handler: WorkFlowPostHandler, + base_to_response: BaseToResponse = SystemToResponse(), form_data=None, + start_node_id=None, + start_node_data=None, chat_record=None, child_node=None, is_the_task_interrupted=lambda: False): + super().__init__(flow, params, work_flow_post_handler, base_to_response, form_data, None, None, None, + None, None, start_node_id, start_node_data, chat_record, child_node, is_the_task_interrupted) + self.out_context = {} + + def get_params_serializer_class(self): + return ToolFlowParamsSerializer + + def stream(self): + close_old_connections() + language = get_language() + self.run_chain_async(self.start_node, None, language) + return self.await_result(is_cleanup=False) + + def get_start_node(self): + return self.flow.get_node('tool-start-node') + + def get_base_node(self): + """ + 获取基础节点 + @return: + """ + return self.flow.get_node('tool-base-node') + + def get_source_type(self): + return "TOOL" + + def get_source_id(self): + return self.params.get('tool_id') diff --git a/apps/application/flow/workflow_manage.py b/apps/application/flow/workflow_manage.py index 0bffd0c5b3d..3d5ebdd7ec6 100644 --- a/apps/application/flow/workflow_manage.py +++ b/apps/application/flow/workflow_manage.py @@ -192,9 +192,7 @@ def load_node(self, chat_record, start_node_id, start_node_data): if node_details.get('runtime_node_id') == start_node_id: def get_node_params(n): is_result = False - if n.type == 'application-node': - is_result = True - if n.type == 'loop-node': + if ['application-node', 'loop-node', 'tool-workflow-lib-node'].__contains__(n.type): is_result = True return {**n.properties.get('node_data'), 'form_data': start_node_data, 'node_data': start_node_data, 'child_node': self.child_node, 'is_result': is_result} @@ -798,3 +796,9 @@ def get_node_reference(self, reference_address: Dict): def get_params_serializer_class(self): return FlowParamsSerializer + + def get_source_type(self): + return "APPLICATION" + + def get_source_id(self): + return self.params.get('application_id') diff --git a/apps/application/serializers/common.py b/apps/application/serializers/common.py index 68f1a9e7de0..a4d193cdacc 100644 --- a/apps/application/serializers/common.py +++ b/apps/application/serializers/common.py @@ -22,6 +22,75 @@ from models_provider.models import Model from models_provider.tools import get_model_credential from system_manage.models.resource_mapping import ResourceMapping +from tools.models import ToolRecord + + +class ToolExecute: + def __init__(self, tool_id: str, + tool_record_id: str, + workspace_id: str, + source_type, + source_id, + debug=False): + self.tool_id = tool_id + self.workspace_id = workspace_id + self.source_type = source_type + self.source_id = source_id + self.tool_record_id = tool_record_id + self.debug = debug + + def get_record(self): + if self.tool_record_id: + if self.debug: + return self.to_record(cache.get(Cache_Version.TOOL_WORKFLOW_EXECUTE.get_key(key=self.tool_record_id), + version=Cache_Version.TOOL_WORKFLOW_EXECUTE.get_version())) + else: + return QuerySet(ToolRecord).filter(tool_id=self.tool_id, id=self.tool_record_id).first() + return None + + def to_record(self, tool_record_dict): + if tool_record_dict is None: + return None + return ToolRecord(id=tool_record_dict.get('id'), + tool_id=tool_record_dict.get('tool_id'), + workspace_id=tool_record_dict.get('workspace_id'), + source_type=tool_record_dict.get('source_type'), + source_id=tool_record_dict.get('source_id'), + meta=tool_record_dict.get('meta'), + state=tool_record_dict.get('state'), + run_time=tool_record_dict.get('run_time')) + + def to_dict(self, tool_record): + return {'id': tool_record.id, + 'tool_id': tool_record.tool_id, + 'workspace_id': tool_record.workspace_id, + 'source_type': tool_record.source_type, + 'source_id': tool_record.source_id, + 'meta': tool_record.meta, + 'state': tool_record.state, + 'run_time': tool_record.run_time} + + def set_record(self, tool_record): + cache.set(Cache_Version.TOOL_WORKFLOW_EXECUTE.get_key(key=self.tool_record_id), self.to_dict(tool_record), + version=Cache_Version.TOOL_WORKFLOW_EXECUTE.get_version(), + timeout=60 * 30) + if not self.debug: + QuerySet(ToolRecord).update_or_create(id=tool_record.id, + create_defaults={'id': tool_record.id, + 'tool_id': tool_record.tool_id, + 'workspace_id': tool_record.workspace_id, + "source_type": tool_record.source_type, + 'source_id': tool_record.source_id, + 'meta': tool_record.meta, + 'run_time': tool_record.run_time}, + defaults={ + 'workspace_id': tool_record.workspace_id, + 'tool_id': tool_record.tool_id, + "source_type": tool_record.source_type, + 'source_id': tool_record.source_id, + 'meta': tool_record.meta, + 'run_time': tool_record.run_time + }) class ChatInfo: diff --git a/apps/common/constants/cache_version.py b/apps/common/constants/cache_version.py index 1b202dc8e07..0aed4715e2e 100644 --- a/apps/common/constants/cache_version.py +++ b/apps/common/constants/cache_version.py @@ -39,6 +39,8 @@ class Cache_Version(Enum): CHAT_USER_TOKEN = "CHAT_USER_TOKEN", lambda token: token + TOOL_WORKFLOW_EXECUTE = "TOOL_WORKFLOW_EXECUTE", lambda key: key + def get_version(self): return self.value[0] diff --git a/apps/tools/api/tool_workflow.py b/apps/tools/api/tool_workflow.py new file mode 100644 index 00000000000..a9cf179377d --- /dev/null +++ b/apps/tools/api/tool_workflow.py @@ -0,0 +1,54 @@ +# coding=utf-8 +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter + +from common.mixins.api_mixin import APIMixin +from common.result import DefaultResultSerializer +from tools.serializers.tool_workflow import ToolWorkflowImportRequest + + +class ToolWorkflowApi(APIMixin): + pass + + +class ToolWorkflowVersionApi(APIMixin): + pass + + +class ToolWorkflowExportApi(APIMixin): + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + description="工作空间id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + OpenApiParameter( + name="tool_id", + description="工具id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + ] + + @staticmethod + def get_response(): + return DefaultResultSerializer + + +class ToolWorkflowImportApi(APIMixin): + @staticmethod + def get_parameters(): + return ToolWorkflowExportApi.get_parameters() + + @staticmethod + def get_request(): + return ToolWorkflowImportRequest + + @staticmethod + def get_response(): + return DefaultResultSerializer diff --git a/apps/tools/migrations/0007_alter_tool_tool_type_toolworkflow_and_more.py b/apps/tools/migrations/0007_alter_tool_tool_type_toolworkflow_and_more.py new file mode 100644 index 00000000000..9e0b7fa3507 --- /dev/null +++ b/apps/tools/migrations/0007_alter_tool_tool_type_toolworkflow_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.11 on 2026-03-10 10:46 + +import django.db.models.deletion +import uuid_utils.compat +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tools', '0006_alter_tool_tool_type'), + ] + + operations = [ + migrations.AlterField( + model_name='tool', + name='tool_type', + field=models.CharField(choices=[('INTERNAL', '内置'), ('CUSTOM', '自定义'), ('SKILL', '技能'), ('MCP', 'MCP工具'), ('DATA_SOURCE', '数据源'), ('WORKFLOW', 'Workflow')], db_index=True, default='CUSTOM', max_length=20, verbose_name='工具类型'), + ), + migrations.CreateModel( + name='ToolWorkflow', + fields=[ + ('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')), + ('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), + ('workspace_id', models.CharField(db_index=True, default='default', max_length=64, verbose_name='工作空间id')), + ('work_flow', models.JSONField(default=dict, verbose_name='工作流数据')), + ('is_publish', models.BooleanField(db_index=True, default=False, verbose_name='是否发布')), + ('publish_time', models.DateTimeField(blank=True, null=True, verbose_name='发布时间')), + ('tool', models.OneToOneField(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='workflow', to='tools.tool', verbose_name='工具')), + ], + options={ + 'db_table': 'tool_workflow', + }, + ), + migrations.CreateModel( + name='ToolWorkflowVersion', + fields=[ + ('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')), + ('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), + ('workspace_id', models.CharField(db_index=True, default='default', max_length=64, verbose_name='工作空间id')), + ('name', models.CharField(default='', max_length=128, verbose_name='版本名称')), + ('work_flow', models.JSONField(default=dict, verbose_name='工作流数据')), + ('publish_user_id', models.UUIDField(default=None, null=True, verbose_name='发布者id')), + ('publish_user_name', models.CharField(default='', max_length=128, verbose_name='发布者名称')), + ('tool', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='tools.tool', verbose_name='工具')), + ], + options={ + 'db_table': 'tool_workflow_version', + }, + ), + ] diff --git a/apps/tools/models/__init__.py b/apps/tools/models/__init__.py index 4d360990db7..0e5f75ce59d 100644 --- a/apps/tools/models/__init__.py +++ b/apps/tools/models/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from .tool import * +from .tool_workflow import * \ No newline at end of file diff --git a/apps/tools/models/tool.py b/apps/tools/models/tool.py index 0e27ff6e946..5f4d5bac334 100644 --- a/apps/tools/models/tool.py +++ b/apps/tools/models/tool.py @@ -36,11 +36,13 @@ class ToolType(models.TextChoices): SKILL = "SKILL", "技能" MCP = "MCP", "MCP工具" DATA_SOURCE = "DATA_SOURCE", "数据源" + WORKFLOW = "WORKFLOW" class ToolTaskTypeChoices(models.TextChoices): APPLICATION = 'APPLICATION' KNOWLEDGE = 'KNOWLEDGE' + TOOL = 'TOOL' TRIGGER = 'TRIGGER' @@ -69,7 +71,6 @@ class Meta: db_table = "tool" - class ToolRecord(AppModelMixin): id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id") tool = models.ForeignKey(Tool, on_delete=models.SET_NULL, null=True) diff --git a/apps/tools/models/tool_workflow.py b/apps/tools/models/tool_workflow.py new file mode 100644 index 00000000000..4f070cebd6c --- /dev/null +++ b/apps/tools/models/tool_workflow.py @@ -0,0 +1,46 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: tool_workflow.py + @date:2026/3/3 13:59 + @desc: +""" +from django.db import models + +from common.mixins.app_model_mixin import AppModelMixin +import uuid_utils.compat as uuid + +from tools.models import Tool + + +class ToolWorkflow(AppModelMixin): + """ + 知识库工作流表 + """ + id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id") + tool = models.OneToOneField(Tool, on_delete=models.CASCADE, verbose_name="工具", + db_constraint=False, related_name='workflow') + workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True) + work_flow = models.JSONField(verbose_name="工作流数据", default=dict) + is_publish = models.BooleanField(verbose_name="是否发布", default=False, db_index=True) + publish_time = models.DateTimeField(verbose_name="发布时间", null=True, blank=True) + + class Meta: + db_table = "tool_workflow" + + +class ToolWorkflowVersion(AppModelMixin): + """ + 知识库工作流版本表 - 记录工作流历史版本 + """ + id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id") + tool = models.ForeignKey(Tool, on_delete=models.CASCADE, verbose_name="工具", db_constraint=False) + workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True) + name = models.CharField(verbose_name="版本名称", max_length=128, default="") + work_flow = models.JSONField(verbose_name="工作流数据", default=dict) + publish_user_id = models.UUIDField(verbose_name="发布者id", max_length=128, default=None, null=True) + publish_user_name = models.CharField(verbose_name="发布者名称", max_length=128, default="") + + class Meta: + db_table = "tool_workflow_version" diff --git a/apps/tools/serializers/tool.py b/apps/tools/serializers/tool.py index f0bdb41a18c..97878edddce 100644 --- a/apps/tools/serializers/tool.py +++ b/apps/tools/serializers/tool.py @@ -9,7 +9,7 @@ import tempfile import zipfile from typing import Dict - +from django.core.cache import cache import requests import uuid_utils.compat as uuid from django.core import validators @@ -24,6 +24,7 @@ from rest_framework import serializers, status from application.models import Application +from common.constants.cache_version import Cache_Version from common.database_model_manage.database_model_manage import DatabaseModelManage from common.db.search import page_search, native_page_search, native_search from common.exception.app_exception import AppApiException @@ -40,6 +41,7 @@ from system_manage.serializers.resource_mapping_serializers import ResourceMappingSerializer from system_manage.serializers.user_resource_permission import UserResourcePermissionSerializer from tools.models import Tool, ToolScope, ToolFolder, ToolType, ToolRecord +from tools.models.tool_workflow import ToolWorkflow from trigger.models import TriggerTask, Trigger from users.serializers.user import is_workspace_manage @@ -391,7 +393,8 @@ def insert(self, instance, with_valid=True): 'user_id': self.data.get('user_id'), 'auth_target_type': AuthTargetType.TOOL.value }).auth_resource(str(tool_id)) - + if instance.get('tool_type') == ToolType.WORKFLOW: + ToolWorkflow(id=uuid.uuid7(), tool_id=tool_id, work_flow=instance.get('work_flow', {})).save() # 如果是SKILL类型的工具,修改file表中对应的记录 if instance.get('tool_type') == ToolType.SKILL: file_id = instance.get('code') @@ -609,11 +612,18 @@ def one(self): 'name': skill_file.file_name, 'size': skill_file.file_size, } if skill_file else None + work_flow = {} + if tool.tool_type == 'WORKFLOW': + tool_workflow = QuerySet(ToolWorkflow).filter(tool_id=tool.id).first() + if tool_workflow: + work_flow = tool_workflow.work_flow + return { **ToolModelSerializer(tool).data, 'init_params': tool.init_params if tool.init_params else {}, 'nick_name': nick_name, - 'fileList': [skill_file_dict] if tool.tool_type == 'SKILL' else [] + 'fileList': [skill_file_dict] if tool.tool_type == 'SKILL' else [], + 'work_flow': work_flow } def export(self): @@ -989,6 +999,30 @@ class ToolRecord(serializers.Serializer): source_type = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('source type')) state = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('state')) + class Operate(serializers.Serializer): + id = serializers.UUIDField(required=False, allow_null=True, label=_('record id')) + tool_id = serializers.UUIDField(required=True, label=_('tool id')) + workspace_id = serializers.CharField(required=False, allow_null=True, label=_('workspace id')) + + def one(self): + self.is_valid(raise_exception=True) + tool_record = cache.get(Cache_Version.TOOL_WORKFLOW_EXECUTE.get_key(key=self.data.get('id')), + version=Cache_Version.TOOL_WORKFLOW_EXECUTE.get_version()) + if tool_record: + return tool_record + tool_record = QuerySet(ToolRecord).filter(id=self.data.get('id'), tool_id=self.data.get('tool_id'), + workspace_id=self.data.get('workspace_id')).first() + if tool_record: + return {'id': tool_record.id, + 'tool_id': tool_record.tool_id, + 'workspace_id': tool_record.workspace_id, + 'source_type': tool_record.source_type, + 'source_id': tool_record.source_id, + 'meta': tool_record.meta, + 'state': tool_record.state, + 'run_time': tool_record.run_time} + raise AppApiException(500, _('Tool record does not exist')) + def one(self): self.is_valid(raise_exception=True) if self.data.get('record_id'): @@ -1081,6 +1115,8 @@ class Query(serializers.Serializer): user_id = serializers.UUIDField(required=False, allow_null=True, label=_('user id')) scope = serializers.CharField(required=True, label=_('scope')) tool_type = serializers.CharField(required=False, label=_('tool type'), allow_null=True, allow_blank=True) + tool_type_list = serializers.ListField(child=serializers.CharField(), required=False, label=_('tool type list'), + allow_null=True, allow_empty=True) create_user = serializers.UUIDField(required=False, label=_('create user'), allow_null=True) def page_tool(self, current_page: int, page_size: int): @@ -1142,7 +1178,11 @@ def get_query_set(self, workspace_manage, is_x_pack_ee): if scope is not None: tool_query_set = tool_query_set.filter(scope=scope) - if tool_type: + + tool_type_list = self.data.get('tool_type_list') + if tool_type_list: + tool_query_set = tool_query_set.filter(tool_type__in=tool_type_list) + elif tool_type: tool_query_set = tool_query_set.filter(tool_type=tool_type) query_set_dict = { diff --git a/apps/tools/serializers/tool_version.py b/apps/tools/serializers/tool_version.py new file mode 100644 index 00000000000..9eba191375d --- /dev/null +++ b/apps/tools/serializers/tool_version.py @@ -0,0 +1,107 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: KnowledgeVersionSerializer.py + @date:2025/11/28 18:00 + @desc: +""" +from typing import Dict + +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.db.search import page_search +from common.exception.app_exception import AppApiException +from tools.models import ToolWorkflowVersion, Tool + + +class ToolWorkflowVersionEditSerializer(serializers.Serializer): + name = serializers.CharField(required=False, max_length=128, allow_null=True, allow_blank=True, + label=_("Version Name")) + + +class ToolVersionModelSerializer(serializers.ModelSerializer): + class Meta: + model = ToolWorkflowVersion + fields = ['id', 'name', 'workspace_id', 'tool_id', 'work_flow', 'publish_user_id', 'publish_user_name', + 'create_time', + 'update_time'] + + +class ToolWorkflowVersionQuerySerializer(serializers.Serializer): + tool_id = serializers.UUIDField(required=True, label=_("Tool ID")) + name = serializers.CharField(required=False, allow_null=True, allow_blank=True, + label=_("summary")) + + +class ToolWorkflowVersionSerializer(serializers.Serializer): + workspace_id = serializers.CharField(required=False, label=_("Workspace ID")) + + class Query(serializers.Serializer): + workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID")) + + def get_query_set(self, query): + query_set = QuerySet(ToolWorkflowVersion).filter(tool_id=query.get('tool_id')) + if 'name' in query and query.get('name') is not None: + query_set = query_set.filter(name__contains=query.get('name')) + if 'workspace_id' in self.data and self.data.get('workspace_id') is not None: + query_set = query_set.filter(workspace_id=self.data.get('workspace_id')) + return query_set.order_by("-create_time") + + def list(self, query, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + ToolWorkflowVersionQuerySerializer(data=query).is_valid(raise_exception=True) + query_set = self.get_query_set(query) + return [ToolVersionModelSerializer(v).data for v in query_set] + + def page(self, query, current_page, page_size, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + return page_search(current_page, page_size, + self.get_query_set(query), + post_records_handler=lambda v: ToolVersionModelSerializer(v).data) + + class Operate(serializers.Serializer): + workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID")) + tool_id = serializers.UUIDField(required=True, label=_("Tool ID")) + tool_version_id = serializers.UUIDField(required=True, + label=_("Tool version ID")) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + workspace_id = self.data.get('workspace_id') + query_set = QuerySet(Tool).filter(id=self.data.get('tool_id')) + if workspace_id: + query_set = query_set.filter(workspace_id=workspace_id) + if not query_set.exists(): + raise AppApiException(500, _('Tool id does not exist')) + + def one(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + tool_version = QuerySet(ToolWorkflowVersion).filter(tool_id=self.data.get('tool_id'), + id=self.data.get( + 'tool_version_id')).first() + if tool_version is not None: + return ToolVersionModelSerializer(tool_version).data + else: + raise AppApiException(500, _('Workflow version does not exist')) + + def edit(self, instance: Dict, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + ToolWorkflowVersionEditSerializer(data=instance).is_valid(raise_exception=True) + tool_version = QuerySet(ToolWorkflowVersion).filter(tool_id=self.data.get('tool_id'), + id=self.data.get( + 'knowledge_version_id')).first() + if tool_version is not None: + name = instance.get('name', None) + if name is not None and len(name) > 0: + tool_version.name = name + tool_version.save() + return ToolVersionModelSerializer(tool_version).data + else: + raise AppApiException(500, _('Workflow version does not exist')) diff --git a/apps/tools/serializers/tool_workflow.py b/apps/tools/serializers/tool_workflow.py new file mode 100644 index 00000000000..cffe8c2a6db --- /dev/null +++ b/apps/tools/serializers/tool_workflow.py @@ -0,0 +1,319 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: tool_workflow.py + @date:2026/3/6 13:59 + @desc: +""" +# coding=utf-8 +import pickle +from functools import reduce +from typing import Dict, List + +import requests +import uuid_utils.compat as uuid +from django.db import transaction +from django.db.models import QuerySet +from django.http import HttpResponse +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers, status + +from application.flow.common import Workflow, WorkflowMode +from application.flow.i_step_node import ToolWorkflowPostHandler +from application.flow.tool_workflow_manage import ToolWorkflowManage +from application.models import ChatRecord +from application.serializers.common import ToolExecute +from common.exception.app_exception import AppApiException +from common.field.common import UploadedFileField +from common.result import result +from common.utils.common import bytes_to_uploaded_file +from common.utils.common import restricted_loads, generate_uuid +from common.utils.logger import maxkb_logger +from common.utils.tool_code import ToolExecutor +from knowledge.models import KnowledgeWorkflow +from system_manage.models import AuthTargetType +from system_manage.serializers.user_resource_permission import UserResourcePermissionSerializer +from tools.models import Tool, ToolScope, ToolWorkflow, ToolWorkflowVersion +from tools.serializers.tool import ToolExportModelSerializer +from users.models import User + +tool_executor = ToolExecutor() + + +def hand_node(node, update_tool_map): + if node.get('type') == 'tool-lib-node': + tool_lib_id = (node.get('properties', {}).get('node_data', {}).get('tool_lib_id') or '') + node.get('properties', {}).get('node_data', {})['tool_lib_id'] = update_tool_map.get(tool_lib_id, tool_lib_id) + + if node.get('type') == 'search-knowledge-node': + node.get('properties', {}).get('node_data', {})['knowledge_id_list'] = [] + if node.get('type') == 'ai-chat-node': + node_data = node.get('properties', {}).get('node_data', {}) + mcp_tool_ids = node_data.get('mcp_tool_ids') or [] + node_data['mcp_tool_ids'] = [update_tool_map.get(tool_id, + tool_id) for tool_id in mcp_tool_ids] + tool_ids = node_data.get('tool_ids') or [] + node_data['tool_ids'] = [update_tool_map.get(tool_id, + tool_id) for tool_id in tool_ids] + if node.get('type') == 'mcp-node': + mcp_tool_id = (node.get('properties', {}).get('node_data', {}).get('mcp_tool_id') or '') + node.get('properties', {}).get('node_data', {})['mcp_tool_id'] = update_tool_map.get(mcp_tool_id, + mcp_tool_id) + + +class ToolWorkflowModelSerializer(serializers.ModelSerializer): + class Meta: + model = ToolWorkflow + fields = '__all__' + + +class ToolWorkflowImportRequest(serializers.Serializer): + file = UploadedFileField(required=True, label=_("file")) + + +class ToolWorkflowActionListQuerySerializer(serializers.Serializer): + user_name = serializers.CharField(required=False, label=_('Name'), allow_blank=True, allow_null=True) + state = serializers.CharField(required=False, label=_("State"), allow_blank=True, allow_null=True) + + +class ToolWorkflowInstance: + + def __init__(self, knowledge_workflow: dict, version: str, tool_list: List[dict]): + self.knowledge_workflow = knowledge_workflow + self.version = version + self.tool_list = tool_list + + def get_tool_list(self): + return self.tool_list or [] + + +class ToolWorkflowSerializer(serializers.Serializer): + class Import(serializers.Serializer): + user_id = serializers.UUIDField(required=True, label=_('user id')) + workspace_id = serializers.CharField(required=False, label=_('workspace id')) + knowledge_id = serializers.UUIDField(required=True, label=_('knowledge id')) + + @transaction.atomic + def import_(self, instance: dict, is_import_tool, with_valid=True): + if with_valid: + self.is_valid() + ToolWorkflowSerializer(data=instance).is_valid(raise_exception=True) + user_id = self.data.get('user_id') + workspace_id = self.data.get('workspace_id') + tool_id = self.data.get('tool_id') + tool_instance_bytes = instance.get('file').read() + try: + tool_instance = restricted_loads(tool_instance_bytes) + except Exception as e: + raise AppApiException(1001, _("Unsupported file format")) + tool_workflow = tool_instance.work_flow + tool_list = tool_instance.get_tool_list() + update_tool_map = {} + if len(tool_list) > 0: + tool_id_list = reduce(lambda x, y: [*x, *y], + [[tool.get('id'), generate_uuid((tool.get('id') + workspace_id or ''))] + for tool + in + tool_list], []) + # 存在的工具列表 + exits_tool_id_list = [str(tool.id) for tool in + QuerySet(Tool).filter(id__in=tool_id_list, workspace_id=workspace_id)] + # 需要更新的工具集合 + update_tool_map = {tool.get('id'): generate_uuid((tool.get('id') + workspace_id or '')) for tool + in + tool_list if + not exits_tool_id_list.__contains__( + tool.get('id'))} + + tool_list = [{**tool, 'id': update_tool_map.get(tool.get('id'))} for tool in tool_list if + not exits_tool_id_list.__contains__( + tool.get('id')) and not exits_tool_id_list.__contains__( + generate_uuid((tool.get('id') + workspace_id or '')))] + + work_flow = self.to_tool_workflow( + tool_workflow, + update_tool_map, + ) + tool_model_list = [self.to_tool(tool, workspace_id, user_id) for tool in tool_list] + QuerySet(ToolWorkflow).filter(workspace_id=workspace_id, tool_id=tool_id).update_or_create( + tool_id=tool_id, + workspace_id=workspace_id, + defaults={'work_flow': work_flow} + ) + + if is_import_tool: + if len(tool_model_list) > 0: + QuerySet(Tool).bulk_create(tool_model_list) + UserResourcePermissionSerializer(data={ + 'workspace_id': self.data.get('workspace_id'), + 'user_id': self.data.get('user_id'), + 'auth_target_type': AuthTargetType.TOOL.value + }).auth_resource_batch([t.id for t in tool_model_list]) + + @staticmethod + def to_tool_workflow(knowledge_workflow, update_tool_map): + work_flow = knowledge_workflow.get("work_flow") + for node in work_flow.get('nodes', []): + hand_node(node, update_tool_map) + if node.get('type') == 'loop_node': + for n in node.get('properties', {}).get('node_data', {}).get('loop_body', {}).get('nodes', []): + hand_node(n, update_tool_map) + return work_flow + + @staticmethod + def to_tool(tool, workspace_id, user_id): + return Tool(id=tool.get('id'), + user_id=user_id, + name=tool.get('name'), + code=tool.get('code'), + template_id=tool.get('template_id'), + input_field_list=tool.get('input_field_list'), + init_field_list=tool.get('init_field_list'), + is_active=False if len((tool.get('init_field_list') or [])) > 0 else tool.get('is_active'), + tool_type=tool.get('tool_type', 'CUSTOM') or 'CUSTOM', + scope=ToolScope.SHARED if workspace_id == 'None' else ToolScope.WORKSPACE, + folder_id='default' if workspace_id == 'None' else workspace_id, + workspace_id=workspace_id) + + class Export(serializers.Serializer): + user_id = serializers.UUIDField(required=True, label=_('user id')) + workspace_id = serializers.CharField(required=False, label=_('workspace id')) + tool_id = serializers.UUIDField(required=True, label=_('knowledge id')) + + def export(self, with_valid=True): + try: + if with_valid: + self.is_valid() + tool_id = self.data.get('tool_id') + tool_workflow = QuerySet(ToolWorkflow).filter(tool_id=tool_id).first() + tool = QuerySet(Tool).filter(id=tool_id).first() + from application.flow.tools import get_tool_id_list + tool_id_list = get_tool_id_list(tool_workflow.work_flow) + tool_list = [] + if len(tool_id_list) > 0: + tool_list = QuerySet(Tool).filter(id__in=tool_id_list).exclude(scope=ToolScope.SHARED) + tool_workflow_dict = {'id': tool.id, + 'work_flow': tool_workflow.work_flow, + 'workspace_id': tool.workspace_id, + 'name': tool.name, + 'desc': tool.desc, + 'tool_type': tool.tool_type} + + tool_workflow_instance = ToolWorkflowInstance( + tool_workflow_dict, + 'v2', + [ToolExportModelSerializer(tool).data for tool in tool_list] + ) + tool_workflow_pickle = pickle.dumps(tool_workflow_instance) + response = HttpResponse(content_type='text/plain', content=tool_workflow_pickle) + response['Content-Disposition'] = f'attachment; filename="{tool.name}.tool"' + return response + except Exception as e: + return result.error(str(e), response_status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + class Operate(serializers.Serializer): + user_id = serializers.UUIDField(required=True, label=_('user id')) + workspace_id = serializers.CharField(required=True, label=_('workspace id')) + tool_id = serializers.UUIDField(required=True, label=_('tool id')) + + def debug(self, instance: Dict, user, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + tool_workflow = QuerySet(ToolWorkflow).filter(tool_id=self.data.get("tool_id")).first() + tool_record_id = instance.get('chat_record_id') or str(uuid.uuid7()) + took_execute = ToolExecute(self.data.get("tool_id"), tool_record_id, + self.data.get("workspace_id"), + None, + None, + True) + record = took_execute.get_record() + work_flow_manage = ToolWorkflowManage( + Workflow.new_instance(tool_workflow.work_flow, WorkflowMode.TOOL), + { + 'chat_record_id': tool_record_id, + 'tool_id': self.data.get("tool_id"), + 'stream': True, + 'workspace_id': self.data.get("workspace_id"), + **instance}, + + ToolWorkflowPostHandler(took_execute, self.data.get("tool_id")), + is_the_task_interrupted=lambda: False, + child_node=instance.get('child_node'), + start_node_id=instance.get('runtime_node_id'), + start_node_data=instance.get('node_data'), + chat_record=self.to_chat_record(record) + ) + + r = work_flow_manage.run() + return r + + @staticmethod + def to_chat_record(record): + if record is None: + return None + return ChatRecord( + answer_text_list=record.meta.get('answer_text_list'), + details=record.meta.get('details'), + answer_text='', + ) + + def publish(self, with_valid=True): + if with_valid: + self.is_valid() + user_id = self.data.get('user_id') + workspace_id = self.data.get("workspace_id") + user = QuerySet(User).filter(id=user_id).first() + tool_workflow = QuerySet(ToolWorkflow).filter(tool_id=self.data.get("tool_id"), + workspace_id=workspace_id).first() + work_flow_version = ToolWorkflowVersion(work_flow=tool_workflow.work_flow, + tool_id=self.data.get("tool_id"), + name=timezone.localtime(timezone.now()).strftime( + '%Y-%m-%d %H:%M:%S'), + publish_user_id=user_id, + publish_user_name=user.username, + workspace_id=workspace_id) + work_flow_version.save() + QuerySet(ToolWorkflow).filter( + tool_id=self.data.get("tool_id") + ).update(is_publish=True, publish_time=timezone.now()) + return True + + def edit(self, instance: Dict): + self.is_valid(raise_exception=True) + if instance.get("work_flow"): + QuerySet(ToolWorkflow).update_or_create(tool_id=self.data.get("tool_id"), + create_defaults={'id': uuid.uuid7(), + 'tool_id': self.data.get( + "tool_id"), + "workspace_id": self.data.get( + 'workspace_id'), + 'work_flow': instance.get('work_flow', + {}), }, + defaults={ + 'work_flow': instance.get('work_flow') + }) + return self.one() + if instance.get("work_flow_template"): + template_instance = instance.get('work_flow_template') + download_url = template_instance.get('downloadUrl') + # 查找匹配的版本名称 + res = requests.get(download_url, timeout=5) + ToolWorkflowSerializer.Import(data={ + 'user_id': self.data.get('user_id'), + 'workspace_id': self.data.get('workspace_id'), + 'tool_id': str(self.data.get('tool_id')), + }).import_({'file': bytes_to_uploaded_file(res.content, 'file.tool')}, is_import_tool=False) + + try: + requests.get(template_instance.get('downloadCallbackUrl'), timeout=5) + except Exception as e: + maxkb_logger.error(f"callback appstore tool download error: {e}") + + return self.one() + + def one(self): + self.is_valid(raise_exception=True) + workflow = QuerySet(KnowledgeWorkflow).filter(knowledge_id=self.data.get('knowledge_id')).first() + return {**ToolWorkflowModelSerializer(workflow).data} diff --git a/apps/tools/urls.py b/apps/tools/urls.py index dd505bdb5e8..68445f0a8cf 100644 --- a/apps/tools/urls.py +++ b/apps/tools/urls.py @@ -8,6 +8,7 @@ path('workspace/internal/tool', views.ToolView.InternalTool.as_view()), path('workspace/store/tool', views.ToolView.StoreTool.as_view()), path('workspace//tool', views.ToolView.as_view()), + path('workspace//tool/workflow', views.ToolWorkflowView.as_view()), path('workspace//tool/import', views.ToolView.Import.as_view()), path('workspace//tool/pylint', views.ToolView.Pylint.as_view()), path('workspace//tool/debug', views.ToolView.Debug.as_view()), @@ -15,6 +16,10 @@ path('workspace//tool/test_connection', views.ToolView.TestConnection.as_view()), path('workspace//tool/upload_skill_file', views.ToolView.UploadSkillFile.as_view()), path('workspace//tool/', views.ToolView.Operate.as_view()), + path('workspace//tool//publish', views.ToolWorkflowView.Publish.as_view()), + path('workspace//tool//debug', views.ToolWorkflowDebugView.as_view()), + path('workspace//tool//workflow', views.ToolWorkflowView.Operate.as_view()), + path('workspace//tool//workflow/export', views.ToolWorkflowView.Export.as_view()), path('workspace//tool//edit_icon', views.ToolView.EditIcon.as_view()), path('workspace//tool//export', views.ToolView.Export.as_view()), path('workspace//tool//add_internal_tool', views.ToolView.AddInternalTool.as_view()), @@ -23,4 +28,8 @@ path('workspace//tool//tool_record/', views.ToolView.ToolRecord.as_view()), path('workspace//tool//tool_record//', views.ToolView.PageToolRecord.as_view()), path('workspace//tool//', views.ToolView.Page.as_view()), + path('workspace//tool//tool_version', views.ToolWorkflowVersionView.as_view()), + path('workspace//tool//tool_version//', views.ToolWorkflowVersionView.Page.as_view()), + path('workspace//tool//tool_version/', views.ToolWorkflowVersionView.Operate.as_view()), + ] diff --git a/apps/tools/views/__init__.py b/apps/tools/views/__init__.py index d3bf330deeb..94c01cadef1 100644 --- a/apps/tools/views/__init__.py +++ b/apps/tools/views/__init__.py @@ -1 +1,3 @@ -from .tool import * \ No newline at end of file +from .tool import * +from .tool_workflow import * +from .tool_workflow_version import * diff --git a/apps/tools/views/tool.py b/apps/tools/views/tool.py index c7f693bb6e1..f6ebabc5f78 100644 --- a/apps/tools/views/tool.py +++ b/apps/tools/views/tool.py @@ -74,6 +74,7 @@ def get(self, request: Request, workspace_id: str): 'name': request.query_params.get('name'), 'scope': request.query_params.get('scope', ToolScope.WORKSPACE), 'tool_type': request.query_params.get('tool_type'), + 'tool_type_list': request.query_params.getlist('tool_type_list[]'), 'user_id': request.user.id, 'create_user': request.query_params.get('create_user'), } @@ -564,10 +565,10 @@ class ToolRecord(APIView): CompareConstants.AND), ) def get(self, request: Request, tool_id: str, workspace_id: str, record_id: str): - return result.success(ToolSerializer.ToolRecord(data={ + return result.success(ToolSerializer.ToolRecord.Operate(data={ 'tool_id': tool_id, 'workspace_id': workspace_id, - 'record_id': record_id, + 'id': record_id, }).one()) class UploadSkillFile(APIView): @@ -595,4 +596,3 @@ def put(self, request: Request, workspace_id: str): 'user_id': request.user.id, 'file': request.FILES.get('file'), }).upload()) - diff --git a/apps/tools/views/tool_workflow.py b/apps/tools/views/tool_workflow.py new file mode 100644 index 00000000000..3d2a08b8604 --- /dev/null +++ b/apps/tools/views/tool_workflow.py @@ -0,0 +1,229 @@ +# coding=utf-8 + +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework.request import Request +from rest_framework.views import APIView + +from common.auth import TokenAuth +from common.auth.authentication import has_permissions, get_is_permissions +from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants +from common.log.log import log +from common.result import result, DefaultResultSerializer +from knowledge.api.knowledge_workflow import KnowledgeWorkflowApi +from knowledge.serializers.knowledge_workflow import KnowledgeWorkflowSerializer +from tools.api.tool_workflow import ToolWorkflowApi, ToolWorkflowExportApi, ToolWorkflowImportApi +from tools.serializers.tool_workflow import ToolWorkflowSerializer +from tools.views import get_tool_operation_object + + +class ToolWorkflowView(APIView): + authentication_classes = [TokenAuth] + + class Publish(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['PUT'], + description=_("Publishing an tool"), + summary=_("Publishing an tool"), + operation_id=_("Publishing an tool"), # type: ignore + parameters=ToolWorkflowApi.get_parameters(), + request=None, + responses=DefaultResultSerializer, + tags=[_('Tool')] # type: ignore + ) + @has_permissions(PermissionConstants.TOOL_EDIT.get_workspace_tool_permission(), + PermissionConstants.TOOL_EDIT.get_workspace_permission_workspace_manage_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.TOOL.get_workspace_knowledge_permission()], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) + @log(menu='Tool', operate='Publishing an tool', + get_operation_object=lambda r, k: get_tool_operation_object(k.get('tool_id'))) + def put(self, request: Request, workspace_id: str, tool_id: str): + return result.success( + ToolWorkflowSerializer.Operate( + data={'tool_id': tool_id, 'user_id': request.user.id, + 'workspace_id': workspace_id, }).publish()) + + class Export(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_('Export tool workflow'), + summary=_('Export tool workflow'), + operation_id=_('Export tool workflow'), # type: ignore + parameters=ToolWorkflowExportApi.get_parameters(), + request=None, + responses=ToolWorkflowExportApi.get_response(), + tags=[_('Tool')] # type: ignore + ) + @has_permissions( + PermissionConstants.TOOL_EXPORT.get_workspace_tool_permission(), + PermissionConstants.TOOL_EXPORT.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.KNOWLEDGE.get_workspace_tool_permission()], + CompareConstants.AND + ) + ) + @log(menu='Tool', operate="Export tool workflow", + get_operation_object=lambda r, k: get_tool_operation_object(k.get('tool_id')), + ) + def get(self, request: Request, workspace_id: str, tool_id: str): + return ToolWorkflowSerializer.Export( + data={'tool_id': tool_id, 'user_id': request.user.id, 'workspace_id': workspace_id} + ).export() + + class Import(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['POST'], + description=_('Import tool workflow'), + summary=_('Import tool workflow'), + operation_id=_('Import tool workflow'), # type: ignore + parameters=ToolWorkflowImportApi.get_parameters(), + request=ToolWorkflowImportApi.get_request(), + responses=ToolWorkflowImportApi.get_response(), + tags=[_('Tool')] # type: ignore + ) + @has_permissions( + PermissionConstants.TOOL_EXPORT.get_workspace_tool_permission(), + PermissionConstants.TOOL_EXPORT.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.KNOWLEDGE.get_workspace_tool_permission()], + CompareConstants.AND + ) + ) + @log(menu='Tool', operate="Import tool workflow", + get_operation_object=lambda r, k: get_tool_operation_object(k.get('tool')), + ) + def post(self, request: Request, workspace_id: str, tool_id: str): + is_import_tool = get_is_permissions(request, workspace_id=workspace_id)( + PermissionConstants.TOOL_IMPORT.get_workspace_permission(), + PermissionConstants.TOOL_IMPORT.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), RoleConstants.USER.get_workspace_role() + ) + return result.success(ToolWorkflowSerializer.Import(data={ + 'tool_id': tool_id, 'user_id': request.user.id, 'workspace_id': workspace_id + }).import_({'file': request.FILES.get('file')}, is_import_tool)) + + class Operate(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['PUT'], + description=_('Edit tool workflow'), + summary=_('Edit tool workflow'), + operation_id=_('Edit tool workflow'), # type: ignore + parameters=ToolWorkflowApi.get_parameters(), + request=ToolWorkflowApi.get_request(), + responses=ToolWorkflowApi.get_response(), + tags=[_('Tool')] # type: ignore + ) + @has_permissions( + PermissionConstants.TOOL_EDIT.get_workspace_tool_permission(), + PermissionConstants.TOOL_EDIT.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.TOOL.get_workspace_tool_permission()], + CompareConstants.AND + ) + ) + @log( + menu='Tool', operate="Modify tool workflow", + get_operation_object=lambda r, k: get_tool_operation_object(k.get('tool_id')), + ) + def put(self, request: Request, workspace_id: str, tool_id: str): + return result.success(ToolWorkflowSerializer.Operate( + data={'user_id': request.user.id, 'workspace_id': workspace_id, 'tool_id': tool_id} + ).edit(request.data)) + + @extend_schema( + methods=['GET'], + description=_('Get tool workflow'), + summary=_('Get tool workflow'), + operation_id=_('Get tool workflow'), # type: ignore + parameters=KnowledgeWorkflowApi.get_parameters(), + responses=KnowledgeWorkflowApi.get_response(), + tags=[_('Tool')] # type: ignore + ) + @has_permissions( + PermissionConstants.TOOL_READ.get_workspace_tool_permission(), + PermissionConstants.TOOL_READ.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.TOOL.get_workspace_tool_permission()], + CompareConstants.AND + ), + ) + def get(self, request: Request, workspace_id: str, tool_id: str): + return result.success(ToolWorkflowSerializer.Operate( + data={'user_id': request.user.id, 'workspace_id': workspace_id, 'tool_id': tool_id} + ).one()) + + +class KnowledgeWorkflowVersionView(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_('Get tool workflow version list'), + summary=_('Get tool workflow version list'), + operation_id=_('Get tool workflow version list'), # type: ignore + parameters=ToolWorkflowApi.get_parameters(), + responses=ToolWorkflowApi.get_response(), + tags=[_('Tool')] # type: ignore + ) + @has_permissions( + PermissionConstants.TOOL_READ.get_workspace_tool_permission(), + PermissionConstants.TOOL_READ.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.TOOL.get_workspace_tool_permission()], + CompareConstants.AND + ), + ) + def get(self, request: Request, workspace_id: str, tool_id: str): + return result.success(KnowledgeWorkflowSerializer.Operate( + data={'user_id': request.user.id, 'workspace_id': workspace_id, 'tool_id': tool_id} + ).one()) + + +class ToolWorkflowDebugView(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['POST'], + description=_('tool workflow debug'), + summary=_('tool workflow debug'), + operation_id=_('tool workflow debug'), # type: ignore + parameters=ToolWorkflowApi.get_parameters(), + responses=ToolWorkflowApi.get_response(), + tags=[_('Tool')] # type: ignore + ) + @has_permissions( + PermissionConstants.TOOL_EDIT.get_workspace_tool_permission(), + PermissionConstants.TOOL_EDIT.get_workspace_permission_workspace_manage_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), + ViewPermission( + [RoleConstants.USER.get_workspace_role()], + [PermissionConstants.TOOL.get_workspace_tool_permission()], + CompareConstants.AND + ), + ) + def post(self, request: Request, workspace_id: str, tool_id: str): + return ToolWorkflowSerializer.Operate( + data={'workspace_id': workspace_id, 'tool_id': tool_id, 'user_id': request.user.id}).debug( + request.data, + request.user, + True) diff --git a/apps/tools/views/tool_workflow_version.py b/apps/tools/views/tool_workflow_version.py new file mode 100644 index 00000000000..7eea50a9077 --- /dev/null +++ b/apps/tools/views/tool_workflow_version.py @@ -0,0 +1,134 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: application_version.py.py + @date:2025/6/3 15:46 + @desc: +""" +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework.request import Request +from rest_framework.views import APIView + +from common import result +from common.auth import TokenAuth +from common.auth.authentication import has_permissions +from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants +from common.log.log import log +from knowledge.api.knowledge_version import KnowledgeVersionListAPI, KnowledgeVersionPageAPI, \ + KnowledgeVersionOperateAPI +from knowledge.models import Knowledge +from tools.serializers.tool_version import ToolWorkflowVersionSerializer +from tools.views import get_tool_operation_object + + +def get_knowledge_operation_object(knowledge_id): + knowledge_model = QuerySet(model=Knowledge).filter(id=knowledge_id).first() + if knowledge_model is not None: + return { + 'name': knowledge_model.name + } + return {} + + +class ToolWorkflowVersionView(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_("Get the tool version list"), + summary=_("Get the tool version list"), + operation_id=_("Get the tool version list"), # type: ignore + parameters=KnowledgeVersionListAPI.get_parameters(), + responses=KnowledgeVersionListAPI.get_response(), + tags=[_('Tool/Version')] # type: ignore + ) + @has_permissions(PermissionConstants.TOOL_READ.get_workspace_knowledge_permission(), + PermissionConstants.TOOL_READ.get_workspace_permission_workspace_manage_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.TOOL.get_workspace_knowledge_permission()], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) + def get(self, request: Request, workspace_id, tool_id: str): + return result.success( + ToolWorkflowVersionSerializer.Query( + data={'workspace_id': workspace_id}).list( + {'name': request.query_params.get("name"), 'tool_id': tool_id})) + + class Page(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_("Get the list of tool versions by page"), + summary=_("Get the list of tool versions by page"), + operation_id=_("Get the list of tool versions by page"), # type: ignore + parameters=KnowledgeVersionPageAPI.get_parameters(), + responses=KnowledgeVersionPageAPI.get_response(), + tags=[_('Tool/Version')] # type: ignore + ) + @has_permissions(PermissionConstants.TOOL_READ.get_workspace_knowledge_permission(), + PermissionConstants.TOOL_READ.get_workspace_permission_workspace_manage_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.TOOL.get_workspace_knowledge_permission()], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) + def get(self, request: Request, workspace_id: str, tool_id: str, current_page: int, page_size: int): + return result.success( + ToolWorkflowVersionSerializer.Query( + data={'workspace_id': workspace_id}).page( + {'name': request.query_params.get("name"), 'tool_id': tool_id}, + current_page, page_size)) + + class Operate(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['GET'], + description=_("Get tool version details"), + summary=_("Get tool version details"), + operation_id=_("Get tool version details"), # type: ignore + parameters=KnowledgeVersionOperateAPI.get_parameters(), + responses=KnowledgeVersionOperateAPI.get_response(), + tags=[_('Tool/Version')] # type: ignore + ) + @has_permissions(PermissionConstants.TOOL_READ.get_workspace_knowledge_permission(), + PermissionConstants.TOOL_READ.get_workspace_permission_workspace_manage_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.TOOL.get_workspace_knowledge_permission()], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) + def get(self, request: Request, workspace_id: str, tool_id: str, tool_version_id: str): + return result.success( + ToolWorkflowVersionSerializer.Operate( + data={'user_id': request.user, 'workspace_id': workspace_id, + 'tool_id': tool_id, 'tool_version_id': tool_version_id}).one()) + + @extend_schema( + methods=['PUT'], + description=_("Modify tool version information"), + summary=_("Modify tool version information"), + operation_id=_("Modify tool version information"), # type: ignore + parameters=KnowledgeVersionOperateAPI.get_parameters(), + request=None, + responses=KnowledgeVersionOperateAPI.get_response(), + tags=[_('Tool/Version')] # type: ignore + ) + @has_permissions(PermissionConstants.TOOL_EDIT.get_workspace_knowledge_permission(), + PermissionConstants.TOOL_EDIT.get_workspace_permission_workspace_manage_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.TOOL.get_workspace_knowledge_permission()], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) + @log(menu='Tool', operate="Modify tool version information", + get_operation_object=lambda r, k: get_tool_operation_object(k.get('tool_id')), + ) + def put(self, request: Request, workspace_id: str, tool_id: str, tool_version_id: str): + return result.success( + ToolWorkflowVersionSerializer.Operate( + data={'tool_id': tool_id, 'workspace_id': workspace_id, + 'tool_version_id': tool_version_id, + 'user_id': request.user.id}).edit( + request.data)) diff --git a/ui/src/api/tool/tool.ts b/ui/src/api/tool/tool.ts index 9506dfeb175..177561f09c6 100644 --- a/ui/src/api/tool/tool.ts +++ b/ui/src/api/tool/tool.ts @@ -1,5 +1,5 @@ import { Result } from '@/request/Result' -import { get, post, del, put, exportFile } from '@/request/index' +import { get, post, del, put, exportFile, postStream } from '@/request/index' import { type Ref } from 'vue' import type { pageRequest } from '@/api/type/common' import type { AddInternalToolParam, toolData } from '@/api/type/tool' @@ -85,7 +85,6 @@ const postToolTestConnection: (data: toolData, loading?: Ref) => Promis return post(`${prefix.value}/test_connection`, data, undefined, loading) } - /** * 获取工具详情 * @param tool_id 工具id @@ -159,7 +158,6 @@ const addInternalTool: ( return post(`${prefix.value}/${tool_id}/add_internal_tool`, param, undefined, loading) } - /** * 工具商店-添加 */ @@ -179,12 +177,7 @@ const updateStoreTool: ( return post(`${prefix.value}/${tool_id}/update_store_tool`, param, undefined, loading) } -const pageToolRecord = ( - tool_id: string, - page: pageRequest, - param: any, - loading?: Ref, -) => { +const pageToolRecord = (tool_id: string, page: pageRequest, param: any, loading?: Ref) => { return get( `${prefix.value}/${tool_id}/tool_record/${page.current_page}/${page.page_size}`, param, @@ -192,10 +185,7 @@ const pageToolRecord = ( ) } -const getToolRecordDetail = ( - tool_id: string, - record_id: string -) => { +const getToolRecordDetail = (tool_id: string, record_id: string) => { return get(`${prefix.value}/${tool_id}/tool_record/${record_id}`) } @@ -205,8 +195,110 @@ const uploadSkillFile: (data: toolData, loading?: Ref) => Promise { return put(`${prefix.value}/upload_skill_file`, data, undefined, loading) } +/** + * 保存工具工作流 + * @param tool_id + * @param data + * @param loading + * @returns + */ +const putToolWorkflow: ( + tool_id: string, + data: any, + loading?: Ref, +) => Promise> = (tool_id, data, loading) => { + return put(`${prefix.value}/${tool_id}/workflow`, data, undefined, loading) +} +/** + * 导出知识库工作流 + * @param knowledge_id + * @param knowledge_name + * @param loading + * @returns + */ +const exportKnowledgeWorkflow = ( + knowledge_id: string, + knowledge_name: string, + loading?: Ref, +) => { + return exportFile( + knowledge_name + '.kbwf', + `${prefix.value}/${knowledge_id}/workflow/export`, + undefined, + loading, + ) +} +/** + * 导出知识库工作流 + * @param knowledge_id + * @param knowledge_name + * @param loading + * @returns + */ +const exportToolWorkflow = (tool_id: string, tool_name: string, loading?: Ref) => { + return exportFile( + tool_name + '.tool', + `${prefix.value}/${tool_id}/workflow/export`, + undefined, + loading, + ) +} +/** + * 导入工具工作流 + */ +const importToolWorkflow: ( + tool_id: string, + data: any, + loading?: Ref, +) => Promise> = (tool_id, data, loading) => { + return post(`${prefix.value}/${tool_id}/workflow/import`, data, undefined, loading) +} +/** + * 获取工具工作流版本列表 + * @param tool_id + * @param loading + * @returns + */ +const listToolWorkflowVersion: (tool_id: string, loading?: Ref) => Promise> = ( + tool_id: string, + loading, +) => { + return get(`${prefix.value}/${tool_id}/tool_version`, {}, loading) +} +/** + * + * @param tool_id 工具id + * @param tool_version_id 工具版本id + * @param data 数据 + * @param loading + * @returns + */ +const updateToolWorkflowVersion: ( + tool_id: string, + tool_version_id: string, + data: any, + loading?: Ref, +) => Promise> = (tool_id: string, tool_version_id, data, loading) => { + return put(`${prefix.value}/${tool_id}/tool_version/${tool_version_id}`, data, {}, loading) +} +const publish: (tool_id: string, loading?: Ref) => Promise> = ( + tool_id: string, + loading, +) => { + return put(`${prefix.value}/${tool_id}/publish`, {}, {}, loading) +} +/** + * 调试工作流 + * @param 参数 + * chat_id: string + * data + */ +const debugToolWorkflow: (tool_id: string, data: any) => Promise = (tool_id, data) => { + const p = (window.MaxKB?.prefix ? window.MaxKB?.prefix : '/admin') + '/api' + return postStream(`${p}${prefix.value}/${tool_id}/debug`, data) +} export default { getToolList, getAllToolList, @@ -227,4 +319,11 @@ export default { pageToolRecord, getToolRecordDetail, uploadSkillFile, + putToolWorkflow, + importToolWorkflow, + listToolWorkflowVersion, + updateToolWorkflowVersion, + publish, + exportToolWorkflow, + debugToolWorkflow, } diff --git a/ui/src/assets/tool/icon_tool_workflow.svg b/ui/src/assets/tool/icon_tool_workflow.svg new file mode 100644 index 00000000000..1e53f7b475e --- /dev/null +++ b/ui/src/assets/tool/icon_tool_workflow.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/ui/src/components/ai-chat/index.vue b/ui/src/components/ai-chat/index.vue index 02524b91e14..2e3b1ed98a8 100644 --- a/ui/src/components/ai-chat/index.vue +++ b/ui/src/components/ai-chat/index.vue @@ -194,12 +194,21 @@ import { t } from '@/locales' import bus from '@/bus' import { throttle } from 'lodash-es' import { copyClick } from '@/utils/clipboard' +import { loadSharedApi } from '@/utils/dynamics-api/shared-api' provide('upload', (file: any, loading?: Ref) => { return props.type === 'debug-ai-chat' ? applicationApi.postUploadFile(file, 'TEMPORARY_120_MINUTE', 'TEMPORARY_120_MINUTE', loading) : chatAPI.postUploadFile(file, chartOpenId.value, 'CHAT', loading) }) +provide('getSelectModelList', (params: any) => { + if (route.path.includes('resource-management')) { + return loadSharedApi({ type: 'model', systemType: 'systemManage' }).getSelectModelList(params) + } else { + return loadSharedApi({ type: 'model', systemType: 'workspace' }).getSelectModelList(params) + } +}) + const transcribing = ref(false) defineOptions({ name: 'AiChat' }) const route = useRoute() diff --git a/ui/src/components/dynamics-form/constructor/data.ts b/ui/src/components/dynamics-form/constructor/data.ts index 751b06b360d..59da1f1ebb7 100644 --- a/ui/src/components/dynamics-form/constructor/data.ts +++ b/ui/src/components/dynamics-form/constructor/data.ts @@ -54,5 +54,10 @@ const input_type_list = [ label: t('dynamicsForm.input_type_list.UploadInput'), value: 'UploadInput', }, + + { + label: t('dynamicsForm.input_type_list.Model'), + value: 'Model', + }, ] export { input_type_list } diff --git a/ui/src/components/dynamics-form/constructor/items/ModelConstructor.vue b/ui/src/components/dynamics-form/constructor/items/ModelConstructor.vue new file mode 100644 index 00000000000..7e118c068f8 --- /dev/null +++ b/ui/src/components/dynamics-form/constructor/items/ModelConstructor.vue @@ -0,0 +1,311 @@ + + + diff --git a/ui/src/components/dynamics-form/index.vue b/ui/src/components/dynamics-form/index.vue index 8a41683ea9b..473f4c08de8 100644 --- a/ui/src/components/dynamics-form/index.vue +++ b/ui/src/components/dynamics-form/index.vue @@ -215,6 +215,7 @@ const render = ( | (() => Promise>>), data?: Dict, ) => { + console.log(data, '-----') formFieldList.value = [] nextTick(() => { if (typeof render_data == 'string') { @@ -246,6 +247,7 @@ const render = ( } const getFormDefaultValue = (fieldList: Array, form_data?: any) => { form_data = form_data ? form_data : {} + console.log(form_data) const value = fieldList .map((item) => { if (form_data[item.field] !== undefined) { @@ -274,6 +276,7 @@ const getFormDefaultValue = (fieldList: Array, form_data?: any) => { return {} }) .reduce((x, y) => ({ ...x, ...y }), {}) + console.log(value) return value } /** diff --git a/ui/src/components/dynamics-form/items/model/Model.vue b/ui/src/components/dynamics-form/items/model/Model.vue new file mode 100644 index 00000000000..f87010f12f5 --- /dev/null +++ b/ui/src/components/dynamics-form/items/model/Model.vue @@ -0,0 +1,128 @@ + + + diff --git a/ui/src/components/dynamics-form/items/model/provider-data.ts b/ui/src/components/dynamics-form/items/model/provider-data.ts new file mode 100644 index 00000000000..398d07fa0b8 --- /dev/null +++ b/ui/src/components/dynamics-form/items/model/provider-data.ts @@ -0,0 +1,107 @@ +export const providerList = [ + { + "provider": "model_azure_provider", + "name": "Azure OpenAI", + "icon": "" + }, + { + "provider": "model_wenxin_provider", + "name": "千帆大模型", + "icon": "\n\n\n\n" + }, + { + "provider": "model_ollama_provider", + "name": "Ollama", + "icon": " \n\n" + }, + { + "provider": "model_openai_provider", + "name": "OpenAI", + "icon": "" + }, + { + "provider": "model_docker_ai_provider", + "name": "Docker AI", + "icon": "\n\n\n" + }, + { + "provider": "model_kimi_provider", + "name": "Kimi", + "icon": "" + }, + { + "provider": "model_zhipu_provider", + "name": "智谱 AI", + "icon": "" + }, + { + "provider": "model_xf_provider", + "name": "讯飞星火", + "icon": "" + }, + { + "provider": "model_deepseek_provider", + "name": "DeepSeek", + "icon": "\n\t\n" + }, + { + "provider": "model_gemini_provider", + "name": "Gemini", + "icon": "" + }, + { + "provider": "model_volcanic_engine_provider", + "name": "火山引擎", + "icon": "\n\n \n\n" + }, + { + "provider": "model_tencent_provider", + "name": "腾讯混元", + "icon": "\n\n \n\n" + }, + { + "provider": "model_tencent_cloud_provider", + "name": "腾讯云", + "icon": "\n\n\n \n \n \n" + }, + { + "provider": "model_aws_bedrock_provider", + "name": "Amazon Bedrock", + "icon": "" + }, + { + "provider": "model_local_provider", + "name": "本地模型", + "icon": "\n\n\n" + }, + { + "provider": "model_xinference_provider", + "name": "Xorbits Inference", + "icon": "\n\n \n\n" + }, + { + "provider": "model_vllm_provider", + "name": "vLLM", + "icon": "\n\n \n\n" + }, + { + "provider": "aliyun_bai_lian_model_provider", + "name": "阿里云百炼", + "icon": "【icon】阿里百炼大模型" + }, + { + "provider": "model_anthropic_provider", + "name": "Anthropic", + "icon": "" + }, + { + "provider": "model_siliconCloud_provider", + "name": "SILICONFLOW", + "icon": "\n\n\n\n\n" + }, + { + "provider": "model_regolo_provider", + "name": "Regolo", + "icon": "\n\n \n \n \n .cls-1 {\n fill: #303030;\n }\n\n .cls-2 {\n fill: #59e389;\n }\n \n \n \n \n \n \n \n \n \n\n" + } +] \ No newline at end of file diff --git a/ui/src/components/model-select/index.vue b/ui/src/components/model-select/index.vue index 70b79a65199..91120eed1f6 100644 --- a/ui/src/components/model-select/index.vue +++ b/ui/src/components/model-select/index.vue @@ -1,6 +1,12 @@ + diff --git a/ui/src/views/tool-workflow/component/debug-drawer/index.vue b/ui/src/views/tool-workflow/component/debug-drawer/index.vue new file mode 100644 index 00000000000..37ff2e59ffc --- /dev/null +++ b/ui/src/views/tool-workflow/component/debug-drawer/index.vue @@ -0,0 +1,80 @@ + + + diff --git a/ui/src/views/tool-workflow/component/debug/index.vue b/ui/src/views/tool-workflow/component/debug/index.vue new file mode 100644 index 00000000000..74a61d48618 --- /dev/null +++ b/ui/src/views/tool-workflow/component/debug/index.vue @@ -0,0 +1,5 @@ + + + diff --git a/ui/src/views/tool-workflow/component/debug/parameters/index.vue b/ui/src/views/tool-workflow/component/debug/parameters/index.vue new file mode 100644 index 00000000000..292f8331537 --- /dev/null +++ b/ui/src/views/tool-workflow/component/debug/parameters/index.vue @@ -0,0 +1,82 @@ + + + diff --git a/ui/src/views/tool-workflow/component/debug/result/index.vue b/ui/src/views/tool-workflow/component/debug/result/index.vue new file mode 100644 index 00000000000..452e685eb53 --- /dev/null +++ b/ui/src/views/tool-workflow/component/debug/result/index.vue @@ -0,0 +1,278 @@ + + + diff --git a/ui/src/views/tool-workflow/index.vue b/ui/src/views/tool-workflow/index.vue new file mode 100644 index 00000000000..86a80451115 --- /dev/null +++ b/ui/src/views/tool-workflow/index.vue @@ -0,0 +1,621 @@ + + + diff --git a/ui/src/views/tool/WorkflowFormDialog.vue b/ui/src/views/tool/WorkflowFormDialog.vue new file mode 100644 index 00000000000..e21332ce5f4 --- /dev/null +++ b/ui/src/views/tool/WorkflowFormDialog.vue @@ -0,0 +1,166 @@ + + + diff --git a/ui/src/views/tool/component/ToolListContainer.vue b/ui/src/views/tool/component/ToolListContainer.vue index d696772d453..144d9e27229 100644 --- a/ui/src/views/tool/component/ToolListContainer.vue +++ b/ui/src/views/tool/component/ToolListContainer.vue @@ -82,6 +82,16 @@ + +
+ + + +
+
创建工作流
+
+
+
@@ -403,12 +413,13 @@ :source="SourceTypeEnum.TOOL" > + diff --git a/ui/src/workflow/icons/tool-base-node-icon.vue b/ui/src/workflow/icons/tool-base-node-icon.vue new file mode 100644 index 00000000000..cf9ad11e91c --- /dev/null +++ b/ui/src/workflow/icons/tool-base-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/tool-start-node-icon.vue b/ui/src/workflow/icons/tool-start-node-icon.vue new file mode 100644 index 00000000000..da762ada8df --- /dev/null +++ b/ui/src/workflow/icons/tool-start-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/tool-workflow-lib-node-icon.vue b/ui/src/workflow/icons/tool-workflow-lib-node-icon.vue new file mode 100644 index 00000000000..7b639f180f7 --- /dev/null +++ b/ui/src/workflow/icons/tool-workflow-lib-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/nodes/ai-chat-node/index.vue b/ui/src/workflow/nodes/ai-chat-node/index.vue index 27f2d4e384a..32347b2d07e 100644 --- a/ui/src/workflow/nodes/ai-chat-node/index.vue +++ b/ui/src/workflow/nodes/ai-chat-node/index.vue @@ -13,10 +13,13 @@ > @@ -28,7 +31,33 @@ }}*
- + + + + + + +
+
+ +
+
- - +
+ @@ -71,7 +97,7 @@ type="primary" link @click="openGeneratePromptDialog(chat_data.model_id)" - :disabled="!chat_data.model_id" + :disabled="chat_data.model_id_type === 'reference' || !chat_data.model_id" > @@ -458,6 +484,7 @@ + diff --git a/ui/src/workflow/nodes/tool-base-node/component/input/InputFieldTable.vue b/ui/src/workflow/nodes/tool-base-node/component/input/InputFieldTable.vue new file mode 100644 index 00000000000..ee57d5fcd14 --- /dev/null +++ b/ui/src/workflow/nodes/tool-base-node/component/input/InputFieldTable.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/ui/src/workflow/nodes/tool-base-node/component/input/InputTitleDialog.vue b/ui/src/workflow/nodes/tool-base-node/component/input/InputTitleDialog.vue new file mode 100644 index 00000000000..08c2f48c32f --- /dev/null +++ b/ui/src/workflow/nodes/tool-base-node/component/input/InputTitleDialog.vue @@ -0,0 +1,83 @@ + + + diff --git a/ui/src/workflow/nodes/tool-base-node/component/output/OutputFieldFormDialog.vue b/ui/src/workflow/nodes/tool-base-node/component/output/OutputFieldFormDialog.vue new file mode 100644 index 00000000000..ee48511e0c1 --- /dev/null +++ b/ui/src/workflow/nodes/tool-base-node/component/output/OutputFieldFormDialog.vue @@ -0,0 +1,114 @@ + + + diff --git a/ui/src/workflow/nodes/tool-base-node/component/output/OutputFieldTable.vue b/ui/src/workflow/nodes/tool-base-node/component/output/OutputFieldTable.vue new file mode 100644 index 00000000000..16da75aa779 --- /dev/null +++ b/ui/src/workflow/nodes/tool-base-node/component/output/OutputFieldTable.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/ui/src/workflow/nodes/tool-base-node/component/output/OutputTitleDialog.vue b/ui/src/workflow/nodes/tool-base-node/component/output/OutputTitleDialog.vue new file mode 100644 index 00000000000..1768f9fa940 --- /dev/null +++ b/ui/src/workflow/nodes/tool-base-node/component/output/OutputTitleDialog.vue @@ -0,0 +1,83 @@ + + + diff --git a/ui/src/workflow/nodes/tool-base-node/index.ts b/ui/src/workflow/nodes/tool-base-node/index.ts new file mode 100644 index 00000000000..93b5edffd6c --- /dev/null +++ b/ui/src/workflow/nodes/tool-base-node/index.ts @@ -0,0 +1,12 @@ +import ToolBaseNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class ToolBaseNode extends AppNode { + constructor(props: any) { + super(props, ToolBaseNodeVue) + } +} +export default { + type: 'tool-base-node', + model: AppNodeModel, + view: ToolBaseNode, +} diff --git a/ui/src/workflow/nodes/tool-base-node/index.vue b/ui/src/workflow/nodes/tool-base-node/index.vue new file mode 100644 index 00000000000..2ec970bd37e --- /dev/null +++ b/ui/src/workflow/nodes/tool-base-node/index.vue @@ -0,0 +1,14 @@ + + + diff --git a/ui/src/workflow/nodes/tool-start-node/index.ts b/ui/src/workflow/nodes/tool-start-node/index.ts new file mode 100644 index 00000000000..5191bf4ec34 --- /dev/null +++ b/ui/src/workflow/nodes/tool-start-node/index.ts @@ -0,0 +1,36 @@ +import ToolBaseNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +import { t } from '@/locales' +class ToolBaseNode extends AppNode { + constructor(props: any) { + super(props, ToolBaseNodeVue) + } + get_node_field_list() { + const result = [] + result.push({ + value: 'global', + label: t('workflow.variable.global'), + type: 'global', + children: this.props.model.properties?.config?.globalFields || [], + }) + const tbn = this.props.graphModel.getNodeModelById('tool-base-node') + console.log(tbn) + const output = tbn.properties?.user_output_field_list?.map((i: any) => { + return { label: i.label || i.name, value: i.field } + }) + + result.push({ + value: 'output', + label: '参数输出', + type: 'output', + children: output || [], + }) + console.log(result) + return result + } +} +export default { + type: 'tool-start-node', + model: AppNodeModel, + view: ToolBaseNode, +} diff --git a/ui/src/workflow/nodes/tool-start-node/index.vue b/ui/src/workflow/nodes/tool-start-node/index.vue new file mode 100644 index 00000000000..b966ba9f865 --- /dev/null +++ b/ui/src/workflow/nodes/tool-start-node/index.vue @@ -0,0 +1,44 @@ + + + diff --git a/ui/src/workflow/nodes/tool-workflow-lib-node/index.ts b/ui/src/workflow/nodes/tool-workflow-lib-node/index.ts new file mode 100644 index 00000000000..bb896f90846 --- /dev/null +++ b/ui/src/workflow/nodes/tool-workflow-lib-node/index.ts @@ -0,0 +1,15 @@ +import ToolWorkflowLibNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class ToolWorkflowLibNode extends AppNode { + constructor(props: any) { + super(props, ToolWorkflowLibNodeVue) + } + getConfig(props: any) { + return props.model.properties.config + } +} +export default { + type: 'tool-workflow-lib-node', + model: AppNodeModel, + view: ToolWorkflowLibNode, +} diff --git a/ui/src/workflow/nodes/tool-workflow-lib-node/index.vue b/ui/src/workflow/nodes/tool-workflow-lib-node/index.vue new file mode 100644 index 00000000000..ad579d50461 --- /dev/null +++ b/ui/src/workflow/nodes/tool-workflow-lib-node/index.vue @@ -0,0 +1,210 @@ + + +