Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
de93d5d
feat: init knowledge workflow
shaohuzhang1 Nov 3, 2025
cd68e89
feat: add knowledge workflow and version models, serializers, and API…
liuruibin Nov 3, 2025
c632a09
feat: knowledge workflow
shaohuzhang1 Nov 4, 2025
45b32bf
feat: knowledge workflow
shaohuzhang1 Nov 4, 2025
3c31fbd
feat: add KnowledgeWorkflowModelSerializer and Operate class for work…
liuruibin Nov 6, 2025
107fc44
fix: route
shaohuzhang1 Nov 10, 2025
b886d8d
feat: knowledge workflow
shaohuzhang1 Nov 11, 2025
b82e12a
feat: Knowledge workflow permission
zhanweizhang7 Nov 11, 2025
8d7734b
feat: knowledge workflow
shaohuzhang1 Nov 11, 2025
856c1ee
feat: knowledge workflow
shaohuzhang1 Nov 11, 2025
4ee7065
feat: knowledge workflow
shaohuzhang1 Nov 11, 2025
8df567f
feat: knowledge workflow
shaohuzhang1 Nov 12, 2025
17c0056
feat: Data source web node
zhanweizhang7 Nov 12, 2025
58446c8
fix: Back route
zhanweizhang7 Nov 12, 2025
6b61dac
feat: knowledge workflow
shaohuzhang1 Nov 12, 2025
399f336
feat: knowledge workflow
shaohuzhang1 Nov 13, 2025
39fd365
feat: Knowledge write node
zhanweizhang7 Nov 17, 2025
18194cc
feat: add Data Source tool functionality and localization
liuruibin Nov 17, 2025
7eddb4b
feat: add Data Source tool functionality and localization
liuruibin Nov 17, 2025
bc1a480
feat: knowledge workflow
shaohuzhang1 Nov 17, 2025
30902f8
feat: knowledge workflow
shaohuzhang1 Nov 18, 2025
3e47117
fix: simplify export tool permission check in ToolListContainer.vue
liuruibin Nov 18, 2025
754cb89
fix: simplify export condition in ToolResourceIndex.vue
liuruibin Nov 18, 2025
8baa8e8
fix: simplify condition for copying tool in ToolListContainer
liuruibin Nov 18, 2025
e758f01
feat: knowledge workflow
shaohuzhang1 Nov 19, 2025
ec72140
fix: Upload local files and add output fields
shaohuzhang1 Nov 20, 2025
9dc3f21
feat: Knowledge write
zhanweizhang7 Nov 20, 2025
b6c6e1b
feat: add Document Split Node functionality and localization
liuruibin Nov 20, 2025
27e5c65
feat: add Document Split Node functionality and localization
liuruibin Nov 20, 2025
1da372e
feat: Knowledge write
zhanweizhang7 Nov 20, 2025
80f14f1
feat: enhance Document Split Node with result processing and problem …
liuruibin Nov 20, 2025
820b680
fix: Allow problem be blank
zhanweizhang7 Nov 21, 2025
5922597
feat: enhance Document Split Node with result processing and problem …
liuruibin Nov 21, 2025
ada109d
feat: tool datasource
shaohuzhang1 Nov 21, 2025
7e7e786
Merge branch 'v2' into knowledge_workflow
shaohuzhang1 Nov 21, 2025
b121d95
fix: Optimization of knowledge base workflow execution logic
shaohuzhang1 Nov 24, 2025
9c42965
refactor: streamline image handling by updating application and knowl…
liuruibin Nov 24, 2025
cd28a7f
refactor: streamline image handling by updating application and knowl…
liuruibin Nov 24, 2025
1d60741
feat: extend support modes in variable aggregation node to include kn…
liuruibin Nov 24, 2025
e5a2c57
feat: Chunks stored
zhanweizhang7 Nov 24, 2025
99477d6
refactor: simplify file handling in document extraction by removing u…
liuruibin Nov 24, 2025
1622d06
refactor: update file ID assignment in document extraction to use pro…
liuruibin Nov 24, 2025
c9eb84f
feat: Workflow menu that distinguishes between applications and knowl…
shaohuzhang1 Nov 24, 2025
a3c0cc1
refactor: update file ID assignment in document extraction to use pro…
liuruibin Nov 24, 2025
10372c8
fix: Add workspace ID as workflow execution parameter
shaohuzhang1 Nov 24, 2025
3745234
feat: add code template for Data Source tool form functionality
liuruibin Nov 24, 2025
18445b5
refactor: remove unused sys import and improve module handling
liuruibin Nov 25, 2025
757cff4
Merge branch 'v2-c' into knowledge_workflow
liuruibin Nov 25, 2025
1f7ae78
feat: Execution details support loading status
shaohuzhang1 Nov 25, 2025
0bb8a62
refactor: update tool type handling and improve category merging logic
liuruibin Nov 25, 2025
b2ad983
feat: Alter fork depth
zhanweizhang7 Nov 25, 2025
c4fd921
fix: ensure filterList is properly initialized and updated in getList…
liuruibin Nov 25, 2025
f1f03e9
refactor: simplify ToolStoreDialog by removing unused toolType logic
liuruibin Nov 25, 2025
45b3dd3
perf: Optimize the style
wangdan-fit2cloud Nov 25, 2025
62b7ca1
style: adjust div width for improved layout in Tree component
liuruibin Nov 26, 2025
76dcc22
refactor: improve polling mechanism for knowledge workflow action
liuruibin Nov 26, 2025
434a9bf
fix: Get workspace_id from workflow params
zhanweizhang7 Nov 26, 2025
831c07d
fix: filter out 'file_bytes' from result in get_details method
liuruibin Nov 26, 2025
daacbc3
feat: add recursive filtering for file_bytes in context data
liuruibin Nov 26, 2025
85597a4
fix: append results to paragraph_list instead of replacing it
liuruibin Nov 26, 2025
9368df5
perf: Optimize translation files
wangdan-fit2cloud Nov 26, 2025
091f771
fix: include document name in bytes_to_uploaded_file call for better …
liuruibin Nov 26, 2025
35b8a34
refactor: optimize buffer retrieval in document processing
liuruibin Nov 26, 2025
691cd8d
Merge branch 'v2-c' into knowledge_workflow
liuruibin Nov 26, 2025
826858d
refactor: remove redundant parameter from bytes_to_uploaded_file call
liuruibin Nov 26, 2025
82274fe
fix: Page style optimization
wangdan-fit2cloud Nov 26, 2025
bb5da65
feat: add slider for setting limit in document rules form
liuruibin Nov 27, 2025
362083a
feat: add workflow knowledge management endpoints and related functio…
liuruibin Nov 27, 2025
a7ed32b
fix: swap file size and file count limits in form inputs
liuruibin Nov 27, 2025
7685246
Merge branch 'v2-c' into knowledge_workflow
liuruibin Nov 27, 2025
aa46fb6
refactor: update tool_config args to use list format for improved rea…
liuruibin Nov 27, 2025
5a029d9
feat: Node supports knowledge base workflow
shaohuzhang1 Nov 27, 2025
bcdea17
Merge branch 'v2-c' into knowledge_workflow
liuruibin Nov 27, 2025
6434ba7
feat: Node supports knowledge base workflow
zhanweizhang7 Nov 27, 2025
83d8ee6
fix: Basic node data cannot be obtained in the workflow
shaohuzhang1 Nov 27, 2025
3a76770
style: Knowledge base workflow debugging page style adjustment
wangdan-fit2cloud Nov 27, 2025
4ea9c55
Merge branch 'v2-c' into knowledge_workflow
liuruibin Nov 28, 2025
3cb4a47
fix: Loop nodes cannot be used in the knowledge base workflow
shaohuzhang1 Nov 28, 2025
334c374
fix: Knowledge base workflow variable assignment node
shaohuzhang1 Nov 28, 2025
1b24d61
feat: add chunk size slider to form for custom split strategy
liuruibin Nov 28, 2025
9a2827b
fix: Workflow style optimization
wangdan-fit2cloud Nov 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions apps/application/flow/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@date:2024/12/11 17:57
@desc:
"""

from enum import Enum
from typing import List, Dict

from django.db.models import QuerySet
Expand Down Expand Up @@ -90,6 +90,16 @@ def __init__(self, edge, node):
self.node = node


class WorkflowMode(Enum):
APPLICATION = "application"

APPLICATION_LOOP = "application-loop"

KNOWLEDGE = "knowledge"

KNOWLEDGE_LOOP = "knowledge-loop"


class Workflow:
"""
节点列表
Expand All @@ -112,7 +122,10 @@ class Workflow:
"""
next_node_map: Dict[str, List[EdgeNode]]

def __init__(self, nodes: List[Node], edges: List[Edge]):
workflow_mode: WorkflowMode

def __init__(self, nodes: List[Node], edges: List[Edge],
workflow_mode: WorkflowMode = WorkflowMode.APPLICATION.value):
self.nodes = nodes
self.edges = edges
self.node_map = {node.id: node for node in nodes}
Expand All @@ -125,6 +138,7 @@ def __init__(self, nodes: List[Node], edges: List[Edge]):
self.next_node_map = {key: [EdgeNode(edge, self.node_map.get(edge.targetNodeId)) for edge in edges] for
key, edges in
group_by(edges, key=lambda edge: edge.sourceNodeId).items()}
self.workflow_mode = workflow_mode

def get_node(self, node_id):
"""
Expand Down Expand Up @@ -167,13 +181,13 @@ def get_next_nodes(self, node_id) -> List[Node]:
return [en.node for en in self.next_node_map.get(node_id, [])]

@staticmethod
def new_instance(flow_obj: Dict):
def new_instance(flow_obj: Dict, workflow_mode: WorkflowMode = WorkflowMode.APPLICATION):
nodes = flow_obj.get('nodes')
edges = flow_obj.get('edges')
nodes = [Node(node.get('id'), node.get('type'), **node)
for node in nodes]
edges = [Edge(edge.get('id'), edge.get('type'), **edge) for edge in edges]
return Workflow(nodes, edges)
return Workflow(nodes, edges, workflow_mode)

def get_start_node(self):
return self.get_node('start-node')
Expand All @@ -190,10 +204,9 @@ def is_valid(self):
self.is_valid_base_node()
self.is_valid_work_flow()

@staticmethod
def is_valid_node_params(node: Node):
def is_valid_node_params(self, node: Node):
from application.flow.step_node import get_node
get_node(node.type)(node, None, None)
get_node(node.type, self.workflow_mode)(node, None, None)

def is_valid_node(self, node: Node):
self.is_valid_node_params(node)
Expand Down
30 changes: 26 additions & 4 deletions apps/application/flow/i_step_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from application.models import ApplicationChatUserStats
from application.models import ChatRecord, ChatUserType
from common.field.common import InstanceField
from knowledge.models.knowledge_action import KnowledgeAction, State

chat_cache = cache

Expand Down Expand Up @@ -78,7 +79,8 @@ def handler(self, workflow):
message_tokens=message_tokens,
answer_tokens=answer_tokens,
answer_text_list=answer_text_list,
run_time=time.time() - workflow.context['start_time'],
run_time=time.time() - workflow.context.get('start_time') if workflow.context.get(
'start_time') is not None else 0,
index=0)

self.chat_info.append_chat_record(chat_record)
Expand All @@ -97,6 +99,16 @@ def handler(self, workflow):
self.chat_info = None


class KnowledgeWorkflowPostHandler(WorkFlowPostHandler):
def __init__(self, chat_info, knowledge_action_id):
super().__init__(chat_info)
self.knowledge_action_id = knowledge_action_id

def handler(self, workflow):
QuerySet(KnowledgeAction).filter(id=self.knowledge_action_id).update(
state=State.SUCCESS)


class NodeResult:
def __init__(self, node_variable: Dict, workflow_variable: Dict,
_write_context=write_context, _is_interrupt=is_interrupt):
Expand Down Expand Up @@ -153,6 +165,14 @@ class FlowParamsSerializer(serializers.Serializer):
debug = serializers.BooleanField(required=True, label="是否debug")


class KnowledgeFlowParamsSerializer(serializers.Serializer):
knowledge_id = serializers.UUIDField(required=True, label="知识库id")
workspace_id = serializers.CharField(required=True, label="工作空间id")
knowledge_action_id = serializers.UUIDField(required=True, label="知识库任务执行器id")
data_source = serializers.DictField(required=True, label="数据源")
knowledge_base = serializers.DictField(required=False, label="知识库设置")


class INode:
view_type = 'many_view'

Expand All @@ -165,7 +185,8 @@ def get_answer_list(self) -> List[Answer] | None:
return None
reasoning_content_enable = self.context.get('model_setting', {}).get('reasoning_content_enable', False)
return [
Answer(self.answer_text, self.view_type, self.runtime_node_id, self.workflow_params['chat_record_id'], {},
Answer(self.answer_text, self.view_type, self.runtime_node_id, self.workflow_params.get('chat_record_id'),
{},
self.runtime_node_id, self.context.get('reasoning_content', '') if reasoning_content_enable else '')]

def __init__(self, node, workflow_params, workflow_manage, up_node_id_list=None,
Expand Down Expand Up @@ -222,13 +243,14 @@ def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
pass

def get_flow_params_serializer_class(self) -> Type[serializers.Serializer]:
return FlowParamsSerializer
return self.workflow_manage.get_params_serializer_class()

def get_write_error_context(self, e):
self.status = 500
self.answer_text = str(e)
self.err_message = str(e)
self.context['run_time'] = time.time() - self.context['start_time']
current_time = time.time()
self.context['run_time'] = current_time - (self.context.get('start_time') or current_time)

def write_error_context(answer, status=200):
pass
Expand Down
15 changes: 15 additions & 0 deletions apps/application/flow/knowledge_loop_workflow_manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# coding=utf-8
"""
@project: maxkb
@Author:虎
@file: workflow_manage.py
@date:2024/1/9 17:40
@desc:
"""
from application.flow.i_step_node import KnowledgeFlowParamsSerializer
from application.flow.loop_workflow_manage import LoopWorkflowManage


class KnowledgeLoopWorkflowManage(LoopWorkflowManage):
def get_params_serializer_class(self):
return KnowledgeFlowParamsSerializer
102 changes: 102 additions & 0 deletions apps/application/flow/knowledge_workflow_manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# coding=utf-8
"""
@project: MaxKB
@Author:虎虎
@file: Knowledge_workflow_manage.py
@date:2025/11/13 19:02
@desc:
"""
import traceback
from concurrent.futures import ThreadPoolExecutor

from django.db.models import QuerySet
from django.utils.translation import get_language

from application.flow.common import Workflow
from application.flow.i_step_node import WorkFlowPostHandler, KnowledgeFlowParamsSerializer
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
from knowledge.models.knowledge_action import KnowledgeAction, State

executor = ThreadPoolExecutor(max_workers=200)


class KnowledgeWorkflowManage(WorkflowManage):

def __init__(self, flow: Workflow,
params,
work_flow_post_handler: WorkFlowPostHandler,
base_to_response: BaseToResponse = SystemToResponse(),
start_node_id=None,
start_node_data=None, chat_record=None, child_node=None):
super().__init__(flow, params, work_flow_post_handler, base_to_response, None, None, None,
None,
None, None, start_node_id, start_node_data, chat_record, child_node)

def get_params_serializer_class(self):
return KnowledgeFlowParamsSerializer

def get_start_node(self):
start_node_list = [node for node in self.flow.nodes if
self.params.get('data_source', {}).get('node_id') == node.id]
return start_node_list[0]

def run(self):
executor.submit(self._run)

def _run(self):
QuerySet(KnowledgeAction).filter(id=self.params.get('knowledge_action_id')).update(
state=State.STARTED)
language = get_language()
self.run_chain_async(self.start_node, None, language)
while self.is_run():
pass
self.work_flow_post_handler.handler(self)

@staticmethod
def get_node_details(current_node, node, index):
if current_node == node:
return {
'name': node.node.properties.get('stepName'),
"index": index,
'run_time': 0,
'type': node.type,
'status': 202,
'err_message': ""
}

return node.get_details(index)

def run_chain(self, current_node, node_result_future=None):
QuerySet(KnowledgeAction).filter(id=self.params.get('knowledge_action_id')).update(
details=self.get_runtime_details(lambda node, index: self.get_node_details(current_node, node, index)))
if node_result_future is None:
node_result_future = self.run_node_future(current_node)
try:
result = self.hand_node_result(current_node, node_result_future)
return result
except Exception as e:
traceback.print_exc()
return None

def hand_node_result(self, current_node, node_result_future):
try:
current_result = node_result_future.result()
result = current_result.write_context(current_node, self)
if result is not None:
# 阻塞获取结果
list(result)
return current_result
except Exception as e:
traceback.print_exc()
self.status = 500
current_node.get_write_error_context(e)
self.answer += str(e)
QuerySet(KnowledgeAction).filter(id=self.params.get('knowledge_action_id')).update(
details=self.get_runtime_details(),
state=State.FAILURE)
finally:
current_node.node_chunk.end()
QuerySet(KnowledgeAction).filter(id=self.params.get('knowledge_action_id')).update(
details=self.get_runtime_details())
10 changes: 5 additions & 5 deletions apps/application/flow/loop_workflow_manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,18 @@ def get_node_cls_by_id(self, node_id, up_node_id_list=None,
get_node_params=lambda node: node.properties.get('node_data')):
for node in self.flow.nodes:
if node.id == node_id:
node_instance = get_node(node.type)(node,
self.params, self, up_node_id_list,
get_node_params,
salt=self.get_index())
node_instance = get_node(node.type, self.flow.workflow_mode)(node,
self.params, self, up_node_id_list,
get_node_params,
salt=self.get_index())
return node_instance
return None

def stream(self):
close_old_connections()
language = get_language()
self.run_chain_async(self.start_node, None, language)
return self.await_result()
return self.await_result(is_cleanup=False)

def get_index(self):
return self.loop_params.get('index')
Expand Down
16 changes: 10 additions & 6 deletions apps/application/flow/step_node/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
from .ai_chat_step_node import *
from .application_node import BaseApplicationNode
from .condition_node import *
from .data_source_local_node.impl.base_data_source_local_node import BaseDataSourceLocalNode
from .data_source_web_node.impl.base_data_source_web_node import BaseDataSourceWebNode
from .direct_reply_node import *
from .document_extract_node import *
from .form_node import *
from .image_generate_step_node import *
from .image_to_video_step_node import BaseImageToVideoNode
from .image_understand_step_node import *
from .intent_node import *
from .knowledge_write_node.impl.base_knowledge_write_node import BaseKnowledgeWriteNode
from .loop_break_node import BaseLoopBreakNode
from .loop_continue_node import BaseLoopContinueNode
from .loop_node import *
Expand All @@ -36,6 +39,7 @@
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

node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseSearchDocumentNode, BaseQuestionNode,
BaseConditionNode, BaseReplyNode,
Expand All @@ -46,11 +50,11 @@
BaseVideoUnderstandNode,
BaseIntentNode, BaseLoopNode, BaseLoopStartStepNode,
BaseLoopContinueNode,
BaseLoopBreakNode, BaseVariableSplittingNode, BaseParameterExtractionNode, BaseVariableAggregationNode]
BaseLoopBreakNode, BaseVariableSplittingNode, BaseParameterExtractionNode, BaseVariableAggregationNode,
BaseDataSourceLocalNode, BaseDataSourceWebNode, BaseKnowledgeWriteNode, BaseDocumentSplitNode]

node_map = {n.type: {w: n for w in n.support} for n in node_list}

def get_node(node_type):
find_list = [node for node in node_list if node.type == node_type]
if len(find_list) > 0:
return find_list[0]
return None

def get_node(node_type, workflow_model):
return node_map.get(node_type).get(workflow_model)
14 changes: 12 additions & 2 deletions apps/application/flow/step_node/ai_chat_step_node/i_chat_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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


Expand All @@ -34,22 +35,31 @@ class ChatNodeSerializer(serializers.Serializer):
mcp_enable = serializers.BooleanField(required=False, label=_("Whether to enable MCP"))
mcp_servers = serializers.JSONField(required=False, label=_("MCP Server"))
mcp_tool_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("MCP Tool ID"))
mcp_tool_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True, label=_("MCP Tool IDs"), )
mcp_tool_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True,
label=_("MCP Tool IDs"), )
mcp_source = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("MCP Source"))

tool_enable = serializers.BooleanField(required=False, default=False, label=_("Whether to enable tools"))
tool_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True,
label=_("Tool 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]

def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return ChatNodeSerializer

def _run(self):
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_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:
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)

def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id,
chat_record_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from rest_framework import serializers

from application.flow.common import WorkflowMode
from application.flow.i_step_node import INode, NodeResult

from django.utils.translation import gettext_lazy as _
Expand All @@ -25,6 +26,7 @@ class ApplicationNodeSerializer(serializers.Serializer):

class IApplicationNode(INode):
type = 'application-node'
support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP]

def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return ApplicationNodeSerializer
Expand Down
Loading
Loading