From 0cfc82d73113753b592fda7d17ca060125103b61 Mon Sep 17 00:00:00 2001 From: Novice <857526207@qq.com> Date: Tue, 6 May 2025 13:16:08 +0800 Subject: [PATCH] fix(structured-output): reasoning model's json format parsing (#19261) --- api/core/workflow/nodes/llm/node.py | 16 +++++---- .../workflow/nodes/test_llm.py | 35 +++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index 5481bd383a..f42bc6784d 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -149,7 +149,7 @@ class LLMNode(BaseNode[LLMNodeData]): self._llm_file_saver = llm_file_saver def _run(self) -> Generator[NodeEvent | InNodeEvent, None, None]: - def process_structured_output(text: str) -> Optional[dict[str, Any] | list[Any]]: + def process_structured_output(text: str) -> Optional[dict[str, Any]]: """Process structured output if enabled""" if not self.node_data.structured_output_enabled or not self.node_data.structured_output: return None @@ -797,18 +797,22 @@ class LLMNode(BaseNode[LLMNodeData]): stop = model_config.stop return filtered_prompt_messages, stop - def _parse_structured_output(self, result_text: str) -> dict[str, Any] | list[Any]: - structured_output: dict[str, Any] | list[Any] = {} + def _parse_structured_output(self, result_text: str) -> dict[str, Any]: + structured_output: dict[str, Any] = {} try: parsed = json.loads(result_text) - if not isinstance(parsed, (dict | list)): + if not isinstance(parsed, dict): raise LLMNodeError(f"Failed to parse structured output: {result_text}") structured_output = parsed except json.JSONDecodeError as e: # if the result_text is not a valid json, try to repair it parsed = json_repair.loads(result_text) - if not isinstance(parsed, (dict | list)): - raise LLMNodeError(f"Failed to parse structured output: {result_text}") + if not isinstance(parsed, dict): + # handle reasoning model like deepseek-r1 got '\n\n\n' prefix + if isinstance(parsed, list): + parsed = next((item for item in parsed if isinstance(item, dict)), {}) + else: + raise LLMNodeError(f"Failed to parse structured output: {result_text}") structured_output = parsed return structured_output diff --git a/api/tests/integration_tests/workflow/nodes/test_llm.py b/api/tests/integration_tests/workflow/nodes/test_llm.py index 22354df196..777a04bd7f 100644 --- a/api/tests/integration_tests/workflow/nodes/test_llm.py +++ b/api/tests/integration_tests/workflow/nodes/test_llm.py @@ -185,3 +185,38 @@ def test_execute_llm_with_jinja2(setup_code_executor_mock, setup_model_mock): assert item.run_result.process_data is not None assert "sunny" in json.dumps(item.run_result.process_data) assert "what's the weather today?" in json.dumps(item.run_result.process_data) + + +def test_extract_json(): + node = init_llm_node( + config={ + "id": "llm", + "data": { + "title": "123", + "type": "llm", + "model": {"provider": "openai", "name": "gpt-3.5-turbo", "mode": "chat", "completion_params": {}}, + "prompt_config": { + "structured_output": { + "enabled": True, + "schema": { + "type": "object", + "properties": {"name": {"type": "string"}, "age": {"type": "number"}}, + }, + } + }, + "prompt_template": [{"role": "user", "text": "{{#sys.query#}}"}], + "memory": None, + "context": {"enabled": False}, + "vision": {"enabled": False}, + }, + }, + ) + llm_texts = [ + '\n\n{"name": "test", "age": 123', # resoning model (deepseek-r1) + '{"name":"test","age":123}', # json schema model (gpt-4o) + '{\n "name": "test",\n "age": 123\n}', # small model (llama-3.2-1b) + '```json\n{"name": "test", "age": 123}\n```', # json markdown (deepseek-chat) + '{"name":"test",age:123}', # without quotes (qwen-2.5-0.5b) + ] + result = {"name": "test", "age": 123} + assert all(node._parse_structured_output(item) == result for item in llm_texts)