Skip to content

Commit 641a3a8

Browse files
authored
feat: Tool workflow (#4915)
1 parent 995c73c commit 641a3a8

119 files changed

Lines changed: 5626 additions & 134 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/application/flow/common.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ class WorkflowMode(Enum):
9999

100100
KNOWLEDGE_LOOP = "knowledge-loop"
101101

102+
TOOL = "tool"
103+
104+
TOOL_LOOP = "tool-loop"
105+
102106

103107
class Workflow:
104108
"""

apps/application/flow/i_step_node.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from application.models import ChatRecord, ChatUserType
2323
from common.field.common import InstanceField
2424
from knowledge.models.knowledge_action import KnowledgeAction, State
25+
from tools.models import ToolRecord
2526

2627
chat_cache = cache
2728

@@ -115,6 +116,40 @@ def handler(self, workflow):
115116
'start_time') is not None else 0)
116117

117118

119+
def get_tool_workflow_state(workflow):
120+
if workflow.is_the_task_interrupted():
121+
return State.REVOKED
122+
details = workflow.get_runtime_details()
123+
node_list = details.values()
124+
all_node = [*node_list, *get_loop_workflow_node(node_list)]
125+
err = any([True for value in all_node if value.get('status') == 500 and not value.get('enableException')])
126+
if err:
127+
return State.FAILURE
128+
return State.SUCCESS
129+
130+
131+
class ToolWorkflowPostHandler(WorkFlowPostHandler):
132+
def __init__(self, chat_info, tool_id):
133+
super().__init__(chat_info)
134+
self.tool_id = tool_id
135+
136+
def handler(self, workflow):
137+
state = get_tool_workflow_state(workflow)
138+
record = ToolRecord(id=self.chat_info.tool_record_id, tool_id=self.tool_id,
139+
workspace_id=self.chat_info.workspace_id,
140+
source_type=self.chat_info.source_type,
141+
source_id=self.chat_info.source_id,
142+
state=state,
143+
meta={
144+
'output': workflow.out_context,
145+
'details': workflow.get_runtime_details(),
146+
'answer_text_list': workflow.get_answer_text_list()
147+
})
148+
self.chat_info.set_record(record)
149+
self.chat_info = None
150+
self.tool_id = None
151+
152+
118153
def get_loop_workflow_node(node_list):
119154
result = []
120155
for item in node_list:
@@ -204,6 +239,11 @@ class KnowledgeFlowParamsSerializer(serializers.Serializer):
204239
knowledge_base = serializers.DictField(required=False, label="知识库设置")
205240

206241

242+
class ToolFlowParamsSerializer(serializers.Serializer):
243+
tool_id = serializers.UUIDField(required=True, label="工具id")
244+
workspace_id = serializers.CharField(required=True, label="工作空间id")
245+
246+
207247
class INode:
208248
view_type = 'many_view'
209249

apps/application/flow/knowledge_loop_workflow_manage.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@
1313
class KnowledgeLoopWorkflowManage(LoopWorkflowManage):
1414
def get_params_serializer_class(self):
1515
return KnowledgeFlowParamsSerializer
16+
17+
def get_source_type(self):
18+
return "KNOWLEDGE"
19+
20+
def get_source_id(self):
21+
return self.params.get('knowledge_id')

apps/application/flow/knowledge_workflow_manage.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,9 @@ def hand_node_result(self, current_node, node_result_future):
122122
current_node.node_chunk.end()
123123
QuerySet(KnowledgeAction).filter(id=self.params.get('knowledge_action_id')).update(
124124
details=self.get_runtime_details())
125+
126+
def get_source_type(self):
127+
return "KNOWLEDGE"
128+
129+
def get_source_id(self):
130+
return self.params.get('knowledge_id')

apps/application/flow/loop_workflow_manage.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,9 @@ def generate_prompt(self, prompt: str):
191191
prompt_template = PromptTemplate.from_template(prompt, template_format='jinja2')
192192
value = prompt_template.format(context=context)
193193
return value
194+
195+
def get_source_type(self):
196+
return "APPLICATION"
197+
198+
def get_source_id(self):
199+
return self.params.get('application_id')

apps/application/flow/step_node/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@
3535
from .text_to_video_step_node.impl.base_text_to_video_node import BaseTextToVideoNode
3636
from .tool_lib_node import *
3737
from .tool_node import *
38+
from .tool_workflow_lib_node import BaseToolWorkflowLibNodeNode
3839
from .variable_aggregation_node.impl.base_variable_aggregation_node import BaseVariableAggregationNode
3940
from .variable_assign_node import BaseVariableAssignNode
4041
from .variable_splitting_node import BaseVariableSplittingNode
4142
from .video_understand_step_node import BaseVideoUnderstandNode
4243
from .document_split_node import BaseDocumentSplitNode
44+
from .tool_start_node import BaseToolStartStepNode
4345

4446
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseSearchDocumentNode, BaseQuestionNode,
4547
BaseConditionNode, BaseReplyNode,
@@ -51,7 +53,8 @@
5153
BaseIntentNode, BaseLoopNode, BaseLoopStartStepNode,
5254
BaseLoopContinueNode,
5355
BaseLoopBreakNode, BaseVariableSplittingNode, BaseParameterExtractionNode, BaseVariableAggregationNode,
54-
BaseDataSourceLocalNode, BaseDataSourceWebNode, BaseKnowledgeWriteNode, BaseDocumentSplitNode]
56+
BaseDataSourceLocalNode, BaseDataSourceWebNode, BaseKnowledgeWriteNode, BaseDocumentSplitNode,
57+
BaseToolStartStepNode, BaseToolWorkflowLibNodeNode]
5558

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

apps/application/flow/step_node/ai_chat_step_node/i_chat_node.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717

1818
class ChatNodeSerializer(serializers.Serializer):
19-
model_id = serializers.CharField(required=True, label=_("Model id"))
19+
model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
20+
model_id_type = serializers.CharField(required=False, default='custom', label=_("Model id type"))
21+
model_id_reference = serializers.ListField(required=False, child=serializers.CharField(), allow_empty=True,
22+
label=_("Reference Field"))
2023
system = serializers.CharField(required=False, allow_blank=True, allow_null=True,
2124
label=_("Role Setting"))
2225
prompt = serializers.CharField(required=True, label=_("Prompt word"))
@@ -41,23 +44,24 @@ class ChatNodeSerializer(serializers.Serializer):
4144
tool_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True,
4245
label=_("Tool IDs"), )
4346
application_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True,
44-
label=_("App IDs"), )
47+
label=_("App IDs"), )
4548
skill_tool_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_empty=True,
46-
label=_("Skill IDs"), )
49+
label=_("Skill IDs"), )
4750
mcp_output_enable = serializers.BooleanField(required=False, default=True, label=_("Whether to enable MCP output"))
4851

4952

5053
class IChatNode(INode):
5154
type = 'ai-chat-node'
5255
support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP,
53-
WorkflowMode.KNOWLEDGE]
56+
WorkflowMode.KNOWLEDGE, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP]
5457

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

5861
def _run(self):
59-
if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__(
60-
self.workflow_manage.flow.workflow_mode):
62+
if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL,
63+
WorkflowMode.TOOL_LOOP].__contains__(
64+
self.workflow_manage.flow.workflow_mode):
6165
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data,
6266
**{'history_chat_record': [], 'stream': True, 'chat_id': None, 'chat_record_id': None})
6367
else:
@@ -66,6 +70,8 @@ def _run(self):
6670
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id,
6771
chat_record_id,
6872
model_params_setting=None,
73+
model_id_type=None,
74+
model_id_reference=None,
6975
dialogue_type=None,
7076
model_setting=None,
7177
mcp_servers=None,

apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ def save_context(self, details, workflow_manage):
151151

152152
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
153153
model_params_setting=None,
154+
model_id_type=None,
155+
model_id_reference=None,
154156
dialogue_type=None,
155157
model_setting=None,
156158
mcp_servers=None,
@@ -165,8 +167,20 @@ def execute(self, model_id, system, prompt, dialogue_number, history_chat_record
165167
if dialogue_type is None:
166168
dialogue_type = 'WORKFLOW'
167169

168-
if model_params_setting is None:
170+
if model_id_type == 'reference' and model_id_reference:
171+
172+
reference_data = self.workflow_manage.get_reference_field(
173+
model_id_reference[0],
174+
model_id_reference[1:],
175+
)
176+
177+
if reference_data and isinstance(reference_data, dict):
178+
model_id = reference_data.get('model_id', model_id)
179+
model_params_setting = reference_data.get('model_params_setting')
180+
181+
if model_params_setting is None and model_id:
169182
model_params_setting = get_default_model_params_setting(model_id)
183+
170184
if model_setting is None:
171185
model_setting = {'reasoning_content_enable': False, 'reasoning_content_end': '</think>',
172186
'reasoning_content_start': '<think>'}

apps/application/flow/step_node/condition_node/i_condition_node.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
3838

3939
type = 'condition-node'
4040

41-
support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP]
41+
support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE,
42+
WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL, WorkflowMode.TOOL_LOOP]

apps/application/flow/step_node/direct_reply_node/i_reply_node.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ def is_valid(self, *, raise_exception=False):
4040
class IReplyNode(INode):
4141
type = 'reply-node'
4242
support = [WorkflowMode.APPLICATION, WorkflowMode.APPLICATION_LOOP, WorkflowMode.KNOWLEDGE_LOOP,
43-
WorkflowMode.KNOWLEDGE]
43+
WorkflowMode.KNOWLEDGE, WorkflowMode.TOOL]
4444

4545
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
4646
return ReplyNodeParamsSerializer
4747

4848
def _run(self):
49-
if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP].__contains__(
49+
if [WorkflowMode.KNOWLEDGE, WorkflowMode.KNOWLEDGE_LOOP, WorkflowMode.TOOL,
50+
WorkflowMode.TOOL_LOOP].__contains__(
5051
self.workflow_manage.flow.workflow_mode):
5152
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data,
5253
**{'stream': True})

0 commit comments

Comments
 (0)