From 642c85f1fa36b755a8ff078db78bec92ec1b7f19 Mon Sep 17 00:00:00 2001 From: alexrain Date: Tue, 3 Nov 2026 10:40:02 +0800 Subject: [PATCH] fix(agent): keep cot action names and parse multiple code fences --- .../agent/output_parser/cot_output_parser.py | 11 +++++-- .../output_parser/test_cot_output_parser.py | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/api/core/agent/output_parser/cot_output_parser.py b/api/core/agent/output_parser/cot_output_parser.py index 67e96da4f8527e..369e82c5703003 100644 --- a/api/core/agent/output_parser/cot_output_parser.py +++ b/api/core/agent/output_parser/cot_output_parser.py @@ -26,9 +26,10 @@ def parse_action(action) -> str | AgentScratchpadUnit.Action: action = action[0] for key, value in action.items(): - if "input" in key.lower(): + key_lower = key.lower() + if "input" in key_lower: action_input = value - else: + elif key_lower == "action": action_name = value if action_name is not None and action_input is not None: @@ -40,13 +41,17 @@ def parse_action(action) -> str | AgentScratchpadUnit.Action: return json.dumps(action) def extra_json_from_code_block(code_block) -> list[list | dict]: - blocks = re.findall(r"```[json]*\s*([\[{].*[]}])\s*```", code_block, re.DOTALL | re.IGNORECASE) + # One match per fenced block: a non-greedy body keeps two separate + # fences from being read as one object spanning both (#45324). + blocks = re.findall(r"```(?:json)?\s*(.*?)\s*```", code_block, re.DOTALL | re.IGNORECASE) if not blocks: return [] try: json_blocks = [] for block in blocks: json_text = re.sub(r"^[a-zA-Z]+\n", "", block.strip(), flags=re.MULTILINE) + if not json_text.lstrip().startswith(("{", "[")): + continue json_blocks.append(json.loads(json_text, strict=False)) return json_blocks except Exception: diff --git a/api/tests/unit_tests/core/agent/output_parser/test_cot_output_parser.py b/api/tests/unit_tests/core/agent/output_parser/test_cot_output_parser.py index cfb8a3ec225fcf..522e2410e7a8c7 100644 --- a/api/tests/unit_tests/core/agent/output_parser/test_cot_output_parser.py +++ b/api/tests/unit_tests/core/agent/output_parser/test_cot_output_parser.py @@ -99,6 +99,38 @@ def test_json_list_unwrap(self, make_chunk, usage_dict) -> None: AgentScratchpadUnit.Action(action_name="lookup", action_input="abc") ] + def test_action_name_ignores_supplementary_keys(self, make_chunk, usage_dict) -> None: + content = ( + '{"action": "google_search", "action_input": {"query": "latest AI news"}, ' + '"thought": "I should search Google for the latest AI news"}' + ) + chunks = [make_chunk(content)] + result = list(CotAgentOutputParser.handle_react_stream_output(chunks, usage_dict)) + assert result == [ + AgentScratchpadUnit.Action( + action_name="google_search", + action_input={"query": "latest AI news"}, + ) + ] + + def test_code_block_multiple_fences_are_parsed_independently(self, make_chunk, usage_dict) -> None: + content = """```json +{"action": "a1", "input": "x"} +``` + +Some prose between blocks. + +```json +{"action": "a2", "input": "y"} +```""" + chunks = [make_chunk(content)] + result = list(CotAgentOutputParser.handle_react_stream_output(chunks, usage_dict)) + actions = [item for item in result if isinstance(item, AgentScratchpadUnit.Action)] + assert actions == [ + AgentScratchpadUnit.Action(action_name="a1", action_input="x"), + AgentScratchpadUnit.Action(action_name="a2", action_input="y"), + ] + def test_json_missing_fields_returns_string(self, make_chunk, usage_dict) -> None: content = '{"foo": "bar"}' chunks = [make_chunk(content)]