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)