diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index ea2b4cacf80..aad87d7eaac 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -897,6 +897,14 @@ class WorkflowConfig(BaseSettings): default=5, ) + WORKFLOW_AGENT_V2_CONTEXT_VALUE_MAX_LENGTH: PositiveInt = Field( + description=( + "Maximum number of characters for a previous node output value included " + "in an Agent node's workflow context before it is truncated" + ), + default=400_000, + ) + MAX_VARIABLE_SIZE: PositiveInt = Field( description="Maximum size in bytes for a single variable in workflows. Default to 200 KB.", default=200 * 1024, diff --git a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py index 848fd4f80bc..3e83bbf5285 100644 --- a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py +++ b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py @@ -436,8 +436,9 @@ class WorkflowAgentRuntimeRequestBuilder: return json.dumps(prompt_payload, ensure_ascii=False, separators=(",", ":")) text = str(value) - if len(text) > 2000: - return text[:2000] + "...[truncated]" + max_length = dify_config.WORKFLOW_AGENT_V2_CONTEXT_VALUE_MAX_LENGTH + if len(text) > max_length: + return text[:max_length] + "...[truncated]" return text @classmethod diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py index bd2302e6a6a..98f13452a1f 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py @@ -1398,6 +1398,47 @@ def test_previous_node_remote_url_file_mapping_is_not_truncated_in_workflow_cont assert "...[truncated]" not in _workflow_user_prompt(result) +def test_previous_node_long_text_output_is_not_truncated_under_default_limit(): + long_text = "a" * 2500 + + class LongTextVariablePool(FakeVariablePool): + def get(self, selector): + if list(selector) == ["previous-node", "text"]: + return StringSegment(value=long_text) + return super().get(selector) + + context = replace(_context(), variable_pool=LongTextVariablePool()) + context.binding.node_job_config = WorkflowNodeJobConfig.model_validate( + { + "workflow_prompt": "Review {{#previous-node.text#}} before responding.", + } + ) + + result = WorkflowAgentRuntimeRequestBuilder(credentials_provider=FakeCredentialsProvider()).build(context) + + user_prompt = _workflow_user_prompt(result) + assert f" - previous-node.text: {long_text}" in user_prompt + assert "...[truncated]" not in user_prompt + + +def test_previous_node_text_output_respects_configured_max_length(monkeypatch): + monkeypatch.setattr( + "core.workflow.nodes.agent_v2.runtime_request_builder.dify_config.WORKFLOW_AGENT_V2_CONTEXT_VALUE_MAX_LENGTH", + 10, + ) + context = _context() + context.binding.node_job_config = WorkflowNodeJobConfig.model_validate( + { + "workflow_prompt": "Review {{#previous-node.text#}} before responding.", + } + ) + + result = WorkflowAgentRuntimeRequestBuilder(credentials_provider=FakeCredentialsProvider()).build(context) + + user_prompt = _workflow_user_prompt(result) + assert " - previous-node.text: Previous r...[truncated]" in user_prompt + + # ── Agent config declaration layer ──────────────────────────────────────────── diff --git a/docker/envs/core-services/shared.env.example b/docker/envs/core-services/shared.env.example index ed34a894809..16ba82e67a1 100644 --- a/docker/envs/core-services/shared.env.example +++ b/docker/envs/core-services/shared.env.example @@ -201,6 +201,7 @@ TEMPLATE_TRANSFORM_MAX_LENGTH=400000 WORKFLOW_MAX_EXECUTION_STEPS=500 WORKFLOW_MAX_EXECUTION_TIME=1200 WORKFLOW_CALL_MAX_DEPTH=5 +WORKFLOW_AGENT_V2_CONTEXT_VALUE_MAX_LENGTH=400000 MAX_VARIABLE_SIZE=204800 WORKFLOW_GENERATOR_NODE_BUILDER_MAX_WORKERS=6 WORKFLOW_FILE_UPLOAD_LIMIT=10