|
@@ -1,5 +1,8 @@
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
+import pytest
|
|
|
+from flask import Flask
|
|
|
+
|
|
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
|
|
from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult
|
|
|
from core.workflow.entities.variable_pool import VariablePool
|
|
@@ -17,12 +20,20 @@ from core.workflow.graph_engine.entities.event import (
|
|
|
from core.workflow.graph_engine.entities.graph import Graph
|
|
|
from core.workflow.graph_engine.entities.runtime_route_state import RouteNodeState
|
|
|
from core.workflow.graph_engine.graph_engine import GraphEngine
|
|
|
+from core.workflow.nodes.code.code_node import CodeNode
|
|
|
from core.workflow.nodes.event import RunCompletedEvent, RunStreamChunkEvent
|
|
|
from core.workflow.nodes.llm.node import LLMNode
|
|
|
+from core.workflow.nodes.question_classifier.question_classifier_node import QuestionClassifierNode
|
|
|
from models.enums import UserFrom
|
|
|
from models.workflow import WorkflowNodeExecutionStatus, WorkflowType
|
|
|
|
|
|
|
|
|
+@pytest.fixture
|
|
|
+def app():
|
|
|
+ app = Flask(__name__)
|
|
|
+ return app
|
|
|
+
|
|
|
+
|
|
|
@patch("extensions.ext_database.db.session.remove")
|
|
|
@patch("extensions.ext_database.db.session.close")
|
|
|
def test_run_parallel_in_workflow(mock_close, mock_remove):
|
|
@@ -502,3 +513,361 @@ def test_run_branch(mock_close, mock_remove):
|
|
|
assert isinstance(items[9], GraphRunSucceededEvent)
|
|
|
|
|
|
# print(graph_engine.graph_runtime_state.model_dump_json(indent=2))
|
|
|
+
|
|
|
+
|
|
|
+@patch("extensions.ext_database.db.session.remove")
|
|
|
+@patch("extensions.ext_database.db.session.close")
|
|
|
+def test_condition_parallel_correct_output(mock_close, mock_remove, app):
|
|
|
+ """issue #16238, workflow got unexpected additional output"""
|
|
|
+
|
|
|
+ graph_config = {
|
|
|
+ "edges": [
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "isInIteration": False,
|
|
|
+ "isInLoop": False,
|
|
|
+ "sourceType": "question-classifier",
|
|
|
+ "targetType": "question-classifier",
|
|
|
+ },
|
|
|
+ "id": "1742382406742-1-1742382480077-target",
|
|
|
+ "source": "1742382406742",
|
|
|
+ "sourceHandle": "1",
|
|
|
+ "target": "1742382480077",
|
|
|
+ "targetHandle": "target",
|
|
|
+ "type": "custom",
|
|
|
+ "zIndex": 0,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "isInIteration": False,
|
|
|
+ "isInLoop": False,
|
|
|
+ "sourceType": "question-classifier",
|
|
|
+ "targetType": "answer",
|
|
|
+ },
|
|
|
+ "id": "1742382480077-1-1742382531085-target",
|
|
|
+ "source": "1742382480077",
|
|
|
+ "sourceHandle": "1",
|
|
|
+ "target": "1742382531085",
|
|
|
+ "targetHandle": "target",
|
|
|
+ "type": "custom",
|
|
|
+ "zIndex": 0,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "isInIteration": False,
|
|
|
+ "isInLoop": False,
|
|
|
+ "sourceType": "question-classifier",
|
|
|
+ "targetType": "answer",
|
|
|
+ },
|
|
|
+ "id": "1742382480077-2-1742382534798-target",
|
|
|
+ "source": "1742382480077",
|
|
|
+ "sourceHandle": "2",
|
|
|
+ "target": "1742382534798",
|
|
|
+ "targetHandle": "target",
|
|
|
+ "type": "custom",
|
|
|
+ "zIndex": 0,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "isInIteration": False,
|
|
|
+ "isInLoop": False,
|
|
|
+ "sourceType": "question-classifier",
|
|
|
+ "targetType": "answer",
|
|
|
+ },
|
|
|
+ "id": "1742382480077-1742382525856-1742382538517-target",
|
|
|
+ "source": "1742382480077",
|
|
|
+ "sourceHandle": "1742382525856",
|
|
|
+ "target": "1742382538517",
|
|
|
+ "targetHandle": "target",
|
|
|
+ "type": "custom",
|
|
|
+ "zIndex": 0,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {"isInLoop": False, "sourceType": "start", "targetType": "question-classifier"},
|
|
|
+ "id": "1742382361944-source-1742382406742-target",
|
|
|
+ "source": "1742382361944",
|
|
|
+ "sourceHandle": "source",
|
|
|
+ "target": "1742382406742",
|
|
|
+ "targetHandle": "target",
|
|
|
+ "type": "custom",
|
|
|
+ "zIndex": 0,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "isInIteration": False,
|
|
|
+ "isInLoop": False,
|
|
|
+ "sourceType": "question-classifier",
|
|
|
+ "targetType": "code",
|
|
|
+ },
|
|
|
+ "id": "1742382406742-1-1742451801533-target",
|
|
|
+ "source": "1742382406742",
|
|
|
+ "sourceHandle": "1",
|
|
|
+ "target": "1742451801533",
|
|
|
+ "targetHandle": "target",
|
|
|
+ "type": "custom",
|
|
|
+ "zIndex": 0,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {"isInLoop": False, "sourceType": "code", "targetType": "answer"},
|
|
|
+ "id": "1742451801533-source-1742434464898-target",
|
|
|
+ "source": "1742451801533",
|
|
|
+ "sourceHandle": "source",
|
|
|
+ "target": "1742434464898",
|
|
|
+ "targetHandle": "target",
|
|
|
+ "type": "custom",
|
|
|
+ "zIndex": 0,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ "nodes": [
|
|
|
+ {
|
|
|
+ "data": {"desc": "", "selected": False, "title": "开始", "type": "start", "variables": []},
|
|
|
+ "height": 54,
|
|
|
+ "id": "1742382361944",
|
|
|
+ "position": {"x": 30, "y": 286},
|
|
|
+ "positionAbsolute": {"x": 30, "y": 286},
|
|
|
+ "sourcePosition": "right",
|
|
|
+ "targetPosition": "left",
|
|
|
+ "type": "custom",
|
|
|
+ "width": 244,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "classes": [{"id": "1", "name": "financial"}, {"id": "2", "name": "other"}],
|
|
|
+ "desc": "",
|
|
|
+ "instruction": "",
|
|
|
+ "instructions": "",
|
|
|
+ "model": {
|
|
|
+ "completion_params": {"temperature": 0.7},
|
|
|
+ "mode": "chat",
|
|
|
+ "name": "qwen-max-latest",
|
|
|
+ "provider": "langgenius/tongyi/tongyi",
|
|
|
+ },
|
|
|
+ "query_variable_selector": ["1742382361944", "sys.query"],
|
|
|
+ "selected": False,
|
|
|
+ "title": "qc",
|
|
|
+ "topics": [],
|
|
|
+ "type": "question-classifier",
|
|
|
+ "vision": {"enabled": False},
|
|
|
+ },
|
|
|
+ "height": 172,
|
|
|
+ "id": "1742382406742",
|
|
|
+ "position": {"x": 334, "y": 286},
|
|
|
+ "positionAbsolute": {"x": 334, "y": 286},
|
|
|
+ "selected": False,
|
|
|
+ "sourcePosition": "right",
|
|
|
+ "targetPosition": "left",
|
|
|
+ "type": "custom",
|
|
|
+ "width": 244,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "classes": [
|
|
|
+ {"id": "1", "name": "VAT"},
|
|
|
+ {"id": "2", "name": "Stamp Duty"},
|
|
|
+ {"id": "1742382525856", "name": "other"},
|
|
|
+ ],
|
|
|
+ "desc": "",
|
|
|
+ "instruction": "",
|
|
|
+ "instructions": "",
|
|
|
+ "model": {
|
|
|
+ "completion_params": {"temperature": 0.7},
|
|
|
+ "mode": "chat",
|
|
|
+ "name": "qwen-max-latest",
|
|
|
+ "provider": "langgenius/tongyi/tongyi",
|
|
|
+ },
|
|
|
+ "query_variable_selector": ["1742382361944", "sys.query"],
|
|
|
+ "selected": False,
|
|
|
+ "title": "qc 2",
|
|
|
+ "topics": [],
|
|
|
+ "type": "question-classifier",
|
|
|
+ "vision": {"enabled": False},
|
|
|
+ },
|
|
|
+ "height": 210,
|
|
|
+ "id": "1742382480077",
|
|
|
+ "position": {"x": 638, "y": 452},
|
|
|
+ "positionAbsolute": {"x": 638, "y": 452},
|
|
|
+ "selected": False,
|
|
|
+ "sourcePosition": "right",
|
|
|
+ "targetPosition": "left",
|
|
|
+ "type": "custom",
|
|
|
+ "width": 244,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "answer": "VAT:{{#sys.query#}}\n",
|
|
|
+ "desc": "",
|
|
|
+ "selected": False,
|
|
|
+ "title": "answer 2",
|
|
|
+ "type": "answer",
|
|
|
+ "variables": [],
|
|
|
+ },
|
|
|
+ "height": 105,
|
|
|
+ "id": "1742382531085",
|
|
|
+ "position": {"x": 942, "y": 486.5},
|
|
|
+ "positionAbsolute": {"x": 942, "y": 486.5},
|
|
|
+ "selected": False,
|
|
|
+ "sourcePosition": "right",
|
|
|
+ "targetPosition": "left",
|
|
|
+ "type": "custom",
|
|
|
+ "width": 244,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "answer": "Stamp Duty:{{#sys.query#}}\n",
|
|
|
+ "desc": "",
|
|
|
+ "selected": False,
|
|
|
+ "title": "answer 3",
|
|
|
+ "type": "answer",
|
|
|
+ "variables": [],
|
|
|
+ },
|
|
|
+ "height": 105,
|
|
|
+ "id": "1742382534798",
|
|
|
+ "position": {"x": 942, "y": 631.5},
|
|
|
+ "positionAbsolute": {"x": 942, "y": 631.5},
|
|
|
+ "selected": False,
|
|
|
+ "sourcePosition": "right",
|
|
|
+ "targetPosition": "left",
|
|
|
+ "type": "custom",
|
|
|
+ "width": 244,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "answer": "other:{{#sys.query#}}\n",
|
|
|
+ "desc": "",
|
|
|
+ "selected": False,
|
|
|
+ "title": "answer 4",
|
|
|
+ "type": "answer",
|
|
|
+ "variables": [],
|
|
|
+ },
|
|
|
+ "height": 105,
|
|
|
+ "id": "1742382538517",
|
|
|
+ "position": {"x": 942, "y": 776.5},
|
|
|
+ "positionAbsolute": {"x": 942, "y": 776.5},
|
|
|
+ "selected": False,
|
|
|
+ "sourcePosition": "right",
|
|
|
+ "targetPosition": "left",
|
|
|
+ "type": "custom",
|
|
|
+ "width": 244,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "answer": "{{#1742451801533.result#}}",
|
|
|
+ "desc": "",
|
|
|
+ "selected": False,
|
|
|
+ "title": "Answer 5",
|
|
|
+ "type": "answer",
|
|
|
+ "variables": [],
|
|
|
+ },
|
|
|
+ "height": 105,
|
|
|
+ "id": "1742434464898",
|
|
|
+ "position": {"x": 942, "y": 274.70425695336615},
|
|
|
+ "positionAbsolute": {"x": 942, "y": 274.70425695336615},
|
|
|
+ "selected": True,
|
|
|
+ "sourcePosition": "right",
|
|
|
+ "targetPosition": "left",
|
|
|
+ "type": "custom",
|
|
|
+ "width": 244,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "code": '\ndef main(arg1: str, arg2: str) -> dict:\n return {\n "result": arg1 + arg2,\n }\n', # noqa: E501
|
|
|
+ "code_language": "python3",
|
|
|
+ "desc": "",
|
|
|
+ "outputs": {"result": {"children": None, "type": "string"}},
|
|
|
+ "selected": False,
|
|
|
+ "title": "Code",
|
|
|
+ "type": "code",
|
|
|
+ "variables": [
|
|
|
+ {"value_selector": ["sys", "query"], "variable": "arg1"},
|
|
|
+ {"value_selector": ["sys", "query"], "variable": "arg2"},
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ "height": 54,
|
|
|
+ "id": "1742451801533",
|
|
|
+ "position": {"x": 627.8839285786928, "y": 286},
|
|
|
+ "positionAbsolute": {"x": 627.8839285786928, "y": 286},
|
|
|
+ "selected": False,
|
|
|
+ "sourcePosition": "right",
|
|
|
+ "targetPosition": "left",
|
|
|
+ "type": "custom",
|
|
|
+ "width": 244,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ graph = Graph.init(graph_config)
|
|
|
+
|
|
|
+ # construct variable pool
|
|
|
+ pool = VariablePool(
|
|
|
+ system_variables={
|
|
|
+ SystemVariableKey.QUERY: "dify",
|
|
|
+ SystemVariableKey.FILES: [],
|
|
|
+ SystemVariableKey.CONVERSATION_ID: "abababa",
|
|
|
+ SystemVariableKey.USER_ID: "1",
|
|
|
+ },
|
|
|
+ user_inputs={},
|
|
|
+ environment_variables=[],
|
|
|
+ )
|
|
|
+ pool.add(["pe", "list_output"], ["dify-1", "dify-2"])
|
|
|
+ variable_pool = VariablePool(
|
|
|
+ system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, user_inputs={"query": "hi"}
|
|
|
+ )
|
|
|
+
|
|
|
+ graph_engine = GraphEngine(
|
|
|
+ tenant_id="111",
|
|
|
+ app_id="222",
|
|
|
+ workflow_type=WorkflowType.CHAT,
|
|
|
+ workflow_id="333",
|
|
|
+ graph_config=graph_config,
|
|
|
+ user_id="444",
|
|
|
+ user_from=UserFrom.ACCOUNT,
|
|
|
+ invoke_from=InvokeFrom.WEB_APP,
|
|
|
+ call_depth=0,
|
|
|
+ graph=graph,
|
|
|
+ variable_pool=variable_pool,
|
|
|
+ max_execution_steps=500,
|
|
|
+ max_execution_time=1200,
|
|
|
+ )
|
|
|
+
|
|
|
+ def qc_generator(self):
|
|
|
+ yield RunCompletedEvent(
|
|
|
+ run_result=NodeRunResult(
|
|
|
+ status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
|
|
+ inputs={},
|
|
|
+ process_data={},
|
|
|
+ outputs={"class_name": "financial", "class_id": "1"},
|
|
|
+ metadata={
|
|
|
+ NodeRunMetadataKey.TOTAL_TOKENS: 1,
|
|
|
+ NodeRunMetadataKey.TOTAL_PRICE: 1,
|
|
|
+ NodeRunMetadataKey.CURRENCY: "USD",
|
|
|
+ },
|
|
|
+ edge_source_handle="1",
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ def code_generator(self):
|
|
|
+ yield RunCompletedEvent(
|
|
|
+ run_result=NodeRunResult(
|
|
|
+ status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
|
|
+ inputs={},
|
|
|
+ process_data={},
|
|
|
+ outputs={"result": "dify 123"},
|
|
|
+ metadata={
|
|
|
+ NodeRunMetadataKey.TOTAL_TOKENS: 1,
|
|
|
+ NodeRunMetadataKey.TOTAL_PRICE: 1,
|
|
|
+ NodeRunMetadataKey.CURRENCY: "USD",
|
|
|
+ },
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ with patch.object(QuestionClassifierNode, "_run", new=qc_generator):
|
|
|
+ with app.app_context():
|
|
|
+ with patch.object(CodeNode, "_run", new=code_generator):
|
|
|
+ generator = graph_engine.run()
|
|
|
+ stream_content = ""
|
|
|
+ res_content = "VAT:\ndify 123"
|
|
|
+ for item in generator:
|
|
|
+ if isinstance(item, NodeRunStreamChunkEvent):
|
|
|
+ stream_content += f"{item.chunk_content}\n"
|
|
|
+ if isinstance(item, GraphRunSucceededEvent):
|
|
|
+ assert item.outputs == {"answer": res_content}
|
|
|
+ assert stream_content == res_content + "\n"
|