From f71dc0c5a19bb28bd3396a0a1ea75008e6b55c97 Mon Sep 17 00:00:00 2001 From: kimtaewoong Date: Fri, 4 Jul 2025 03:06:30 +0900 Subject: [PATCH 1/9] feat: add LLM node thinking tags configuration - Add a configuration option to control thinking tag processing in LLM nodes. - When set to false, removes blocks from reasoning models such as DeepSeek-R1 and Qwen. - Defaults to true for backward compatibility. --- docker/.env.example | 4 ++++ docker/docker-compose.yaml | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docker/.env.example b/docker/.env.example index a024566c8f..3e12ef1271 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -799,6 +799,10 @@ HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760 HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576 HTTP_REQUEST_NODE_SSL_VERIFY=True +# LLM node thinking tags preservation (default: true) +# Set to false to remove tags from reasoning models like DeepSeek-R1, Qwen +LLM_NODE_THINKING_TAGS_ENABLED=true + # Respect X-* headers to redirect clients RESPECT_XFORWARD_HEADERS_ENABLED=false diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 7f91fd8796..03a49d213d 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -353,13 +353,14 @@ x-shared-env: &shared-api-worker-env WORKFLOW_PARALLEL_DEPTH_LIMIT: ${WORKFLOW_PARALLEL_DEPTH_LIMIT:-3} WORKFLOW_FILE_UPLOAD_LIMIT: ${WORKFLOW_FILE_UPLOAD_LIMIT:-10} WORKFLOW_NODE_EXECUTION_STORAGE: ${WORKFLOW_NODE_EXECUTION_STORAGE:-rdbms} + LLM_NODE_THINKING_TAGS_ENABLED: ${LLM_NODE_THINKING_TAGS_ENABLED:-true} HTTP_REQUEST_NODE_MAX_BINARY_SIZE: ${HTTP_REQUEST_NODE_MAX_BINARY_SIZE:-10485760} HTTP_REQUEST_NODE_MAX_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576} HTTP_REQUEST_NODE_SSL_VERIFY: ${HTTP_REQUEST_NODE_SSL_VERIFY:-True} RESPECT_XFORWARD_HEADERS_ENABLED: ${RESPECT_XFORWARD_HEADERS_ENABLED:-false} SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128} SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128} - LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100} + LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100} MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10} MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10} MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99} From e1aa04a4dae0fb19c0e35440b459299f14eb884b Mon Sep 17 00:00:00 2001 From: kimtaewoong Date: Fri, 4 Jul 2025 03:08:46 +0900 Subject: [PATCH 2/9] feat: add thinking tags control for reasoning models in LLM node - Enable the LLM node to control thinking tags from reasoning models (such as DeepSeek-R1, Qwen, etc.) through configurable processing. --- api/core/workflow/nodes/llm/node.py | 47 +++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index b5225ce548..05c1364a78 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -2,6 +2,8 @@ import base64 import io import json import logging +import os +import re from collections.abc import Generator, Mapping, Sequence from typing import TYPE_CHECKING, Any, Optional, cast @@ -96,6 +98,9 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +# Environment variable to control thinking tags preservation (default: true to maintain backward compatibility) +LLM_NODE_THINKING_TAGS_ENABLED = os.getenv("LLM_NODE_THINKING_TAGS_ENABLED", "true").lower() == "true" + class LLMNode(BaseNode[LLMNodeData]): _node_data_cls = LLMNodeData @@ -374,7 +379,12 @@ class LLMNode(BaseNode[LLMNodeData]): except OutputParserError as e: raise LLMNodeError(f"Failed to parse structured output: {e}") - yield ModelInvokeCompletedEvent(text=full_text_buffer.getvalue(), usage=usage, finish_reason=finish_reason) + # Apply thinking tags removal if disabled + result_text = full_text_buffer.getvalue() + if not LLM_NODE_THINKING_TAGS_ENABLED: + result_text = self._remove_thinking_tags(result_text) + + yield ModelInvokeCompletedEvent(text=result_text, usage=usage, finish_reason=finish_reason) def _image_file_to_markdown(self, file: "File", /): text_chunk = f"![]({file.generate_url()})" @@ -900,8 +910,13 @@ class LLMNode(BaseNode[LLMNodeData]): for text_part in self._save_multimodal_output_and_convert_result_to_markdown(invoke_result.message.content): buffer.write(text_part) + # Apply thinking tags removal if disabled + result_text = buffer.getvalue() + if not LLM_NODE_THINKING_TAGS_ENABLED: + result_text = self._remove_thinking_tags(result_text) + return ModelInvokeCompletedEvent( - text=buffer.getvalue(), + text=result_text, usage=invoke_result.usage, finish_reason=None, ) @@ -1002,6 +1017,32 @@ class LLMNode(BaseNode[LLMNodeData]): logger.warning("unknown contents type encountered, type=%s", type(contents)) yield str(contents) + def _remove_thinking_tags(self, text: str) -> str: + """ + Remove thinking tags like from the response text. + This handles reasoning models like qwen, deepseek-r1 that include thinking process. + + Args: + text: The text content to clean + + Returns: + Cleaned text with thinking tags removed + """ + if not isinstance(text, str) or not text.strip(): + return text + + # Remove ... blocks (case-insensitive, multiline) + # Pattern explanation: + # \s* - optional whitespace before + # .*? - the thinking tag block (non-greedy) + # \s* - optional whitespace after + cleaned_text = re.sub(r"\s*.*?\s*", " ", text, flags=re.IGNORECASE | re.DOTALL) + + # Clean up multiple spaces and strip + cleaned_text = re.sub(r"\s+", " ", cleaned_text).strip() + + return cleaned_text + def _combine_message_content_with_role( *, contents: Optional[str | list[PromptMessageContentUnionTypes]] = None, role: PromptMessageRole @@ -1141,4 +1182,4 @@ def _handle_completion_template( contents=[TextPromptMessageContent(data=result_text)], role=PromptMessageRole.USER ) prompt_messages.append(prompt_message) - return prompt_messages + return prompt_messages \ No newline at end of file From 33d239279bcbb67f7aebd6760fece8d96412b861 Mon Sep 17 00:00:00 2001 From: kimtaewoong Date: Fri, 4 Jul 2025 03:37:30 +0900 Subject: [PATCH 3/9] test: add unit and integration tests for thinking tags removal - Test thinking tags processing for reasoning models like DeepSeek-R1 and Qwen with environment variable configuration. --- .../workflow/nodes/test_llm.py | 118 ++++++++++++++ .../core/workflow/nodes/llm/test_node.py | 150 ++++++++++++++++++ 2 files changed, 268 insertions(+) diff --git a/api/tests/integration_tests/workflow/nodes/test_llm.py b/api/tests/integration_tests/workflow/nodes/test_llm.py index 389d1071f3..4f73fe6af9 100644 --- a/api/tests/integration_tests/workflow/nodes/test_llm.py +++ b/api/tests/integration_tests/workflow/nodes/test_llm.py @@ -287,3 +287,121 @@ def test_extract_json(): ] result = {"name": "test", "age": 123} assert all(_parse_structured_output(item) == result for item in llm_texts) + + +@pytest.mark.parametrize( + ("thinking_tags_enabled", "should_preserve_tags"), + [ + ("true", True), # LLM_NODE_THINKING_TAGS_ENABLED=true -> tags should be preserved + ("false", False), # LLM_NODE_THINKING_TAGS_ENABLED=false -> tags should be removed + ], +) +def test_execute_llm_with_thinking_tags(flask_req_ctx, thinking_tags_enabled, should_preserve_tags): + """Test LLM node with thinking tags removal controlled via environment variable.""" + import os + + with patch.dict(os.environ, {"LLM_NODE_THINKING_TAGS_ENABLED": thinking_tags_enabled}): + # Reload the module to pick up the environment variable change + import importlib + + from core.workflow.nodes.llm import node + + importlib.reload(node) + + node_instance = init_llm_node( + config={ + "id": "llm", + "data": { + "title": f"thinking tags test ({'preserved' if should_preserve_tags else 'removed'})", + "type": "llm", + "model": { + "provider": "langgenius/openrouter", + "name": "qwen/qwen-2.5-72b-instruct", + "mode": "chat", + "completion_params": {}, + }, + "prompt_template": [ + { + "role": "system", + "text": "you are a helpful assistant.", + }, + {"role": "user", "text": "Say hello"}, + ], + "memory": None, + "context": {"enabled": False}, + "vision": {"enabled": False}, + }, + }, + ) + + # Create mock LLM result with thinking tags + mock_usage = LLMUsage( + prompt_tokens=10, + prompt_unit_price=Decimal("0.001"), + prompt_price_unit=Decimal("1000"), + prompt_price=Decimal("0.00001"), + completion_tokens=15, + completion_unit_price=Decimal("0.002"), + completion_price_unit=Decimal("1000"), + completion_price=Decimal("0.00003"), + total_tokens=25, + total_price=Decimal("0.00004"), + currency="USD", + latency=0.3, + ) + + # Mock response with thinking tags (simulating Qwen reasoning behavior) + mock_message = AssistantPromptMessage( + content="Let me think about this greeting...Hello! How can I help you today?" + ) + + mock_llm_result = LLMResult( + model="qwen/qwen-2.5-72b-instruct", + prompt_messages=[], + message=mock_message, + usage=mock_usage, + ) + + mock_model_instance = MagicMock() + mock_model_instance.invoke_llm.return_value = mock_llm_result + + mock_model_config = MagicMock() + mock_model_config.mode = "chat" + mock_model_config.provider = "langgenius/openrouter" + mock_model_config.model = "qwen/qwen-2.5-72b-instruct" + mock_model_config.provider_model_bundle.configuration.tenant_id = "9d2074fc-6f86-45a9-b09d-6ecc63b9056b" + + def mock_fetch_model_config_func(_node_data_model): + return mock_model_instance, mock_model_config + + def mock_get_model_instance(_self, **kwargs): + return mock_model_instance + + with ( + patch.object(node_instance, "_fetch_model_config", mock_fetch_model_config_func), + patch("core.model_manager.ModelManager.get_model_instance", mock_get_model_instance), + ): + # Execute node + result = node_instance._run() + assert isinstance(result, Generator) + + # Verify behavior based on the parameter + for item in result: + if isinstance(item, RunCompletedEvent): + assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED + output_text = item.run_result.outputs.get("text") + assert output_text is not None + + if should_preserve_tags: + # Verify thinking tags are preserved when enabled + assert "" in output_text + assert "" in output_text + assert "Let me think about this greeting..." in output_text + assert "Hello! How can I help you today?" in output_text + else: + # Verify thinking tags are removed when disabled + assert "" not in output_text + assert "" not in output_text + assert "Hello! How can I help you today?" in output_text + # Verify thinking content is not in output + assert "Let me think about this greeting..." not in output_text \ No newline at end of file diff --git a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py index 336c2befcc..5b5332e611 100644 --- a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py @@ -662,3 +662,153 @@ class TestSaveMultimodalOutputAndConvertResultToMarkdown: assert list(gen) == [] mock_file_saver.save_binary_string.assert_not_called() mock_file_saver.save_remote_url.assert_not_called() + + +class TestThinkingTagsRemoval: + """Test cases for thinking tags removal functionality in LLM Node.""" + + def test_remove_single_thinking_tag(self, llm_node): + """Test removal of single thinking tag block.""" + input_text = "This is my thinking processHello, how can I help you?" + expected = "Hello, how can I help you?" + + result = llm_node._remove_thinking_tags(input_text) + assert result == expected + + def test_remove_multiple_thinking_tags(self, llm_node): + """Test removal of multiple thinking tag blocks.""" + input_text = "First thoughtHelloSecond thought World!" + expected = "Hello World!" + + result = llm_node._remove_thinking_tags(input_text) + assert result == expected + + def test_remove_multiline_thinking_tag(self, llm_node): + """Test removal of multiline thinking tag blocks.""" + input_text = """ +This is a multiline +thinking process +with multiple lines +Final answer here.""" + expected = "Final answer here." + + result = llm_node._remove_thinking_tags(input_text) + assert result == expected + + def test_case_insensitive_removal(self, llm_node): + """Test case-insensitive thinking tag removal.""" + input_text = "Uppercase thinkingResponse" + expected = "Response" + + result = llm_node._remove_thinking_tags(input_text) + assert result == expected + + def test_mixed_case_removal(self, llm_node): + """Test mixed case thinking tag removal.""" + input_text = "Mixed case thinkingResponse" + expected = "Response" + + result = llm_node._remove_thinking_tags(input_text) + assert result == expected + + def test_no_thinking_tags(self, llm_node): + """Test text without thinking tags remains unchanged.""" + input_text = "Hello, this is a normal response without thinking tags." + expected = input_text + + result = llm_node._remove_thinking_tags(input_text) + assert result == expected + + def test_empty_string(self, llm_node): + """Test empty string handling.""" + input_text = "" + expected = "" + + result = llm_node._remove_thinking_tags(input_text) + assert result == expected + + def test_only_thinking_tag(self, llm_node): + """Test string with only thinking tag.""" + input_text = "Only thinking, no response" + expected = "" + + result = llm_node._remove_thinking_tags(input_text) + assert result == expected + + def test_whitespace_handling(self, llm_node): + """Test proper whitespace handling after tag removal.""" + input_text = "Thinking Response with spaces" + expected = "Response with spaces" + + result = llm_node._remove_thinking_tags(input_text) + assert result == expected + + def test_whitespace_after_tag(self, llm_node): + """Test whitespace removal after thinking tags.""" + input_text = "Thinking \n \t Final response" + expected = "Final response" + + result = llm_node._remove_thinking_tags(input_text) + assert result == expected + + def test_none_input(self, llm_node): + """Test None input handling.""" + result = llm_node._remove_thinking_tags(None) + assert result is None + + def test_non_string_input(self, llm_node): + """Test non-string input handling.""" + result = llm_node._remove_thinking_tags(123) + assert result == 123 + + def test_complex_real_world_example(self, llm_node): + """Test with a complex real-world example from DeepSeek-R1.""" + input_text = """ + +Okay, let me try to figure out what the user is asking here. The message is just "gdgd". +That's pretty short and doesn't make much sense on its own. I need to consider different +possibilities. + +First, maybe it's a typo or a shorthand. "GDGD" could be an acronym. Let me think about +common acronyms. "GDGD" might stand for "Good Good Good Good" but that seems unlikely. + +It looks like your message might be incomplete or unclear. Could you please provide +more context or rephrase your question? I'm here to help!""" + + expected = ( + "It looks like your message might be incomplete or unclear. Could you please " + "provide more context or rephrase your question? I'm here to help!" + ) + + result = llm_node._remove_thinking_tags(input_text) + assert result == expected + + def test_multiple_whitespace_tags(self, llm_node): + """Test multiple thinking tags with various whitespace.""" + input_text = "First \nSecond Final" + expected = "Final" + + result = llm_node._remove_thinking_tags(input_text) + assert result == expected + + @mock.patch.dict("os.environ", {"LLM_NODE_THINKING_TAGS_ENABLED": "true"}) + def test_environment_variable_enabled(self): + """Test that environment variable is properly read when enabled.""" + from core.workflow.nodes.llm.node import LLM_NODE_THINKING_TAGS_ENABLED + assert LLM_NODE_THINKING_TAGS_ENABLED is True + + @mock.patch.dict("os.environ", {"LLM_NODE_THINKING_TAGS_ENABLED": "false"}) + def test_environment_variable_disabled(self): + """Test that environment variable is properly read when disabled.""" + # Need to reimport to get the updated value + import importlib + import core.workflow.nodes.llm.node + importlib.reload(core.workflow.nodes.llm.node) + from core.workflow.nodes.llm.node import LLM_NODE_THINKING_TAGS_ENABLED + assert LLM_NODE_THINKING_TAGS_ENABLED is False + + def test_environment_variable_default(self): + """Test that environment variable defaults to True.""" + from core.workflow.nodes.llm.node import LLM_NODE_THINKING_TAGS_ENABLED + # Default should be True for backward compatibility + assert LLM_NODE_THINKING_TAGS_ENABLED is True From c7a9c50ddbcd95b7abbad3739aa889e90d17e1cd Mon Sep 17 00:00:00 2001 From: kimtaewoong Date: Fri, 4 Jul 2025 04:03:20 +0900 Subject: [PATCH 4/9] chore: apply code style reformat via dev/reformat --- api/core/workflow/nodes/llm/node.py | 2 +- api/tests/integration_tests/workflow/nodes/test_llm.py | 2 +- api/tests/unit_tests/core/workflow/nodes/llm/test_node.py | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 05c1364a78..280f3e310d 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -1182,4 +1182,4 @@ def _handle_completion_template( contents=[TextPromptMessageContent(data=result_text)], role=PromptMessageRole.USER ) prompt_messages.append(prompt_message) - return prompt_messages \ No newline at end of file + return prompt_messages diff --git a/api/tests/integration_tests/workflow/nodes/test_llm.py b/api/tests/integration_tests/workflow/nodes/test_llm.py index 4f73fe6af9..da014c6a22 100644 --- a/api/tests/integration_tests/workflow/nodes/test_llm.py +++ b/api/tests/integration_tests/workflow/nodes/test_llm.py @@ -404,4 +404,4 @@ def test_execute_llm_with_thinking_tags(flask_req_ctx, thinking_tags_enabled, sh assert "" not in output_text assert "Hello! How can I help you today?" in output_text # Verify thinking content is not in output - assert "Let me think about this greeting..." not in output_text \ No newline at end of file + assert "Let me think about this greeting..." not in output_text diff --git a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py index 5b5332e611..79841f2b02 100644 --- a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py @@ -795,6 +795,7 @@ more context or rephrase your question? I'm here to help!""" def test_environment_variable_enabled(self): """Test that environment variable is properly read when enabled.""" from core.workflow.nodes.llm.node import LLM_NODE_THINKING_TAGS_ENABLED + assert LLM_NODE_THINKING_TAGS_ENABLED is True @mock.patch.dict("os.environ", {"LLM_NODE_THINKING_TAGS_ENABLED": "false"}) @@ -802,13 +803,17 @@ more context or rephrase your question? I'm here to help!""" """Test that environment variable is properly read when disabled.""" # Need to reimport to get the updated value import importlib + import core.workflow.nodes.llm.node + importlib.reload(core.workflow.nodes.llm.node) from core.workflow.nodes.llm.node import LLM_NODE_THINKING_TAGS_ENABLED + assert LLM_NODE_THINKING_TAGS_ENABLED is False def test_environment_variable_default(self): """Test that environment variable defaults to True.""" from core.workflow.nodes.llm.node import LLM_NODE_THINKING_TAGS_ENABLED + # Default should be True for backward compatibility assert LLM_NODE_THINKING_TAGS_ENABLED is True From 89e3027a9b012a56189c8811e892a614f20b7aec Mon Sep 17 00:00:00 2001 From: kimtaewoong Date: Fri, 4 Jul 2025 04:37:36 +0900 Subject: [PATCH 5/9] feat: change default value of LLM_NODE_THINKING_TAGS_ENABLED to True - Update _env_flag helper function's default value to True - This change ensures thinking tags are enabled by default - Improves consistency with expected behavior --- api/core/workflow/nodes/llm/node.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 280f3e310d..9e338d87f1 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -99,7 +99,17 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) # Environment variable to control thinking tags preservation (default: true to maintain backward compatibility) -LLM_NODE_THINKING_TAGS_ENABLED = os.getenv("LLM_NODE_THINKING_TAGS_ENABLED", "true").lower() == "true" +# helper & env‑flag + +def _env_flag(name: str, default: bool = True) -> bool: + """Return an env var as bool (1/0 | true/false | yes/no).""" + val = os.getenv(name) + if val is None: + return default + return val.lower() in {"1", "true", "yes"} + +# keep `` blocks unless explicitly disabled +LLM_NODE_THINKING_TAGS_ENABLED = _env_flag("LLM_NODE_THINKING_TAGS_ENABLED") class LLMNode(BaseNode[LLMNodeData]): From ebd27801a15ae5b2e3dde40600decdbe98c8abb8 Mon Sep 17 00:00:00 2001 From: kimtaewoong Date: Fri, 4 Jul 2025 04:37:53 +0900 Subject: [PATCH 6/9] test: refactor thinking tags removal tests - Remove redundant test cases --- .../core/workflow/nodes/llm/test_node.py | 68 +++---------------- 1 file changed, 8 insertions(+), 60 deletions(-) diff --git a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py index 79841f2b02..c79c813e09 100644 --- a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py @@ -3,6 +3,8 @@ import uuid from collections.abc import Sequence from typing import Optional from unittest import mock +import os +import importlib import pytest @@ -35,7 +37,7 @@ from core.workflow.nodes.llm.entities import ( VisionConfigOptions, ) from core.workflow.nodes.llm.file_saver import LLMFileSaver -from core.workflow.nodes.llm.node import LLMNode +from core.workflow.nodes.llm.node import LLMNode, LLM_NODE_THINKING_TAGS_ENABLED from models.enums import UserFrom from models.provider import ProviderType from models.workflow import WorkflowType @@ -703,14 +705,6 @@ with multiple lines result = llm_node._remove_thinking_tags(input_text) assert result == expected - def test_mixed_case_removal(self, llm_node): - """Test mixed case thinking tag removal.""" - input_text = "Mixed case thinkingResponse" - expected = "Response" - - result = llm_node._remove_thinking_tags(input_text) - assert result == expected - def test_no_thinking_tags(self, llm_node): """Test text without thinking tags remains unchanged.""" input_text = "Hello, this is a normal response without thinking tags." @@ -743,14 +737,6 @@ with multiple lines result = llm_node._remove_thinking_tags(input_text) assert result == expected - def test_whitespace_after_tag(self, llm_node): - """Test whitespace removal after thinking tags.""" - input_text = "Thinking \n \t Final response" - expected = "Final response" - - result = llm_node._remove_thinking_tags(input_text) - assert result == expected - def test_none_input(self, llm_node): """Test None input handling.""" result = llm_node._remove_thinking_tags(None) @@ -761,59 +747,21 @@ with multiple lines result = llm_node._remove_thinking_tags(123) assert result == 123 - def test_complex_real_world_example(self, llm_node): - """Test with a complex real-world example from DeepSeek-R1.""" - input_text = """ - -Okay, let me try to figure out what the user is asking here. The message is just "gdgd". -That's pretty short and doesn't make much sense on its own. I need to consider different -possibilities. - -First, maybe it's a typo or a shorthand. "GDGD" could be an acronym. Let me think about -common acronyms. "GDGD" might stand for "Good Good Good Good" but that seems unlikely. - -It looks like your message might be incomplete or unclear. Could you please provide -more context or rephrase your question? I'm here to help!""" - - expected = ( - "It looks like your message might be incomplete or unclear. Could you please " - "provide more context or rephrase your question? I'm here to help!" - ) - - result = llm_node._remove_thinking_tags(input_text) - assert result == expected - - def test_multiple_whitespace_tags(self, llm_node): - """Test multiple thinking tags with various whitespace.""" - input_text = "First \nSecond Final" - expected = "Final" - - result = llm_node._remove_thinking_tags(input_text) - assert result == expected - @mock.patch.dict("os.environ", {"LLM_NODE_THINKING_TAGS_ENABLED": "true"}) def test_environment_variable_enabled(self): """Test that environment variable is properly read when enabled.""" - from core.workflow.nodes.llm.node import LLM_NODE_THINKING_TAGS_ENABLED - + importlib.reload(core.workflow.nodes.llm.node) assert LLM_NODE_THINKING_TAGS_ENABLED is True @mock.patch.dict("os.environ", {"LLM_NODE_THINKING_TAGS_ENABLED": "false"}) def test_environment_variable_disabled(self): """Test that environment variable is properly read when disabled.""" - # Need to reimport to get the updated value - import importlib - - import core.workflow.nodes.llm.node - importlib.reload(core.workflow.nodes.llm.node) - from core.workflow.nodes.llm.node import LLM_NODE_THINKING_TAGS_ENABLED - assert LLM_NODE_THINKING_TAGS_ENABLED is False def test_environment_variable_default(self): """Test that environment variable defaults to True.""" - from core.workflow.nodes.llm.node import LLM_NODE_THINKING_TAGS_ENABLED - - # Default should be True for backward compatibility - assert LLM_NODE_THINKING_TAGS_ENABLED is True + with mock.patch.dict("os.environ"): + os.environ.pop("LLM_NODE_THINKING_TAGS_ENABLED", None) + importlib.reload(core.workflow.nodes.llm.node) + assert LLM_NODE_THINKING_TAGS_ENABLED is True From 485e1425d55e7eda6723ddae83dd6146b6e6241f Mon Sep 17 00:00:00 2001 From: kimtaewoong Date: Fri, 4 Jul 2025 04:38:58 +0900 Subject: [PATCH 7/9] chore: apply code style reformat via dev/reformat --- api/core/workflow/nodes/llm/node.py | 2 ++ api/tests/unit_tests/core/workflow/nodes/llm/test_node.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 9e338d87f1..1dff81f870 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -101,6 +101,7 @@ logger = logging.getLogger(__name__) # Environment variable to control thinking tags preservation (default: true to maintain backward compatibility) # helper & env‑flag + def _env_flag(name: str, default: bool = True) -> bool: """Return an env var as bool (1/0 | true/false | yes/no).""" val = os.getenv(name) @@ -108,6 +109,7 @@ def _env_flag(name: str, default: bool = True) -> bool: return default return val.lower() in {"1", "true", "yes"} + # keep `` blocks unless explicitly disabled LLM_NODE_THINKING_TAGS_ENABLED = _env_flag("LLM_NODE_THINKING_TAGS_ENABLED") diff --git a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py index c79c813e09..519a6cf44e 100644 --- a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py @@ -1,10 +1,10 @@ import base64 +import importlib +import os import uuid from collections.abc import Sequence from typing import Optional from unittest import mock -import os -import importlib import pytest @@ -37,7 +37,7 @@ from core.workflow.nodes.llm.entities import ( VisionConfigOptions, ) from core.workflow.nodes.llm.file_saver import LLMFileSaver -from core.workflow.nodes.llm.node import LLMNode, LLM_NODE_THINKING_TAGS_ENABLED +from core.workflow.nodes.llm.node import LLM_NODE_THINKING_TAGS_ENABLED, LLMNode from models.enums import UserFrom from models.provider import ProviderType from models.workflow import WorkflowType From 3b4167edbf6cf335dca66a40eb3d0b2be8e158e2 Mon Sep 17 00:00:00 2001 From: kimtaewoong Date: Fri, 4 Jul 2025 05:01:44 +0900 Subject: [PATCH 8/9] test: Fix imports and update test cases for LLM node thinking tags --- .../unit_tests/core/workflow/nodes/llm/test_node.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py index 519a6cf44e..a77d89ba1e 100644 --- a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py @@ -8,6 +8,7 @@ from unittest import mock import pytest +import core.workflow.nodes.llm.node from core.app.entities.app_invoke_entities import InvokeFrom, ModelConfigWithCredentialsEntity from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle from core.entities.provider_entities import CustomConfiguration, SystemConfiguration @@ -37,7 +38,7 @@ from core.workflow.nodes.llm.entities import ( VisionConfigOptions, ) from core.workflow.nodes.llm.file_saver import LLMFileSaver -from core.workflow.nodes.llm.node import LLM_NODE_THINKING_TAGS_ENABLED, LLMNode +from core.workflow.nodes.llm.node import LLMNode from models.enums import UserFrom from models.provider import ProviderType from models.workflow import WorkflowType @@ -751,17 +752,17 @@ with multiple lines def test_environment_variable_enabled(self): """Test that environment variable is properly read when enabled.""" importlib.reload(core.workflow.nodes.llm.node) - assert LLM_NODE_THINKING_TAGS_ENABLED is True + assert core.workflow.nodes.llm.node.LLM_NODE_THINKING_TAGS_ENABLED is True @mock.patch.dict("os.environ", {"LLM_NODE_THINKING_TAGS_ENABLED": "false"}) def test_environment_variable_disabled(self): """Test that environment variable is properly read when disabled.""" importlib.reload(core.workflow.nodes.llm.node) - assert LLM_NODE_THINKING_TAGS_ENABLED is False + assert core.workflow.nodes.llm.node.LLM_NODE_THINKING_TAGS_ENABLED is False def test_environment_variable_default(self): """Test that environment variable defaults to True.""" with mock.patch.dict("os.environ"): os.environ.pop("LLM_NODE_THINKING_TAGS_ENABLED", None) importlib.reload(core.workflow.nodes.llm.node) - assert LLM_NODE_THINKING_TAGS_ENABLED is True + assert core.workflow.nodes.llm.node.LLM_NODE_THINKING_TAGS_ENABLED is True From e10eacd7f0f61431b53a951c78874629170da042 Mon Sep 17 00:00:00 2001 From: kimtaewoong Date: Fri, 4 Jul 2025 05:02:27 +0900 Subject: [PATCH 9/9] chore: fix environment variable ordering in docker-compose.yaml --- docker/docker-compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 03a49d213d..0a8e4e524d 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -353,14 +353,14 @@ x-shared-env: &shared-api-worker-env WORKFLOW_PARALLEL_DEPTH_LIMIT: ${WORKFLOW_PARALLEL_DEPTH_LIMIT:-3} WORKFLOW_FILE_UPLOAD_LIMIT: ${WORKFLOW_FILE_UPLOAD_LIMIT:-10} WORKFLOW_NODE_EXECUTION_STORAGE: ${WORKFLOW_NODE_EXECUTION_STORAGE:-rdbms} - LLM_NODE_THINKING_TAGS_ENABLED: ${LLM_NODE_THINKING_TAGS_ENABLED:-true} HTTP_REQUEST_NODE_MAX_BINARY_SIZE: ${HTTP_REQUEST_NODE_MAX_BINARY_SIZE:-10485760} HTTP_REQUEST_NODE_MAX_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576} HTTP_REQUEST_NODE_SSL_VERIFY: ${HTTP_REQUEST_NODE_SSL_VERIFY:-True} + LLM_NODE_THINKING_TAGS_ENABLED: ${LLM_NODE_THINKING_TAGS_ENABLED:-true} RESPECT_XFORWARD_HEADERS_ENABLED: ${RESPECT_XFORWARD_HEADERS_ENABLED:-false} SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128} SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128} - LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100} + LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100} MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10} MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10} MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99}