[Bug] CotAgentOutputParser overwrites action_name with non-input keys and collapses multi-block code fences

Open Beginner friendly
#45,324 1 comment 1 reaction 0 assignees View on GitHub

Maintainers usually reply within 1 day

@lazerg is already working on this.

Since Nov 2, 2026.

  • #45325 by @lazerg — open
  • #45370 by @alexrain — closed without merging

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
70/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
backend

Research direction

Start in api/core/agent/output_parser/cot_output_parser.py, at the loop that assigns action_name in parse_action and at the regex in extra_json_from_code_block. Run the two reproduction snippets in the issue to confirm the overwritten action name and the fused code block. Done when the action name stays as the explicit action key even with extra keys like thought, and multiple fenced JSON blocks are parsed separately. The issue names no test file, so look for existing parser tests nearby.

Written by the indexing model from the issue text.

Description

Problem Description

In api/core/agent/output_parser/cot_output_parser.py:

1. Action Name Overwrite Defect (Lines 28-33)
for key, value in action.items():
    if "input" in key.lower():
        action_input = value
    else:
        action_name = value

When an LLM outputs an action dictionary containing supplementary keys like thought, reasoning, or tool_id:

{
  "action": "google_search",
  "action_input": {"query": "latest AI news"},
  "thought": "I should search Google for the latest AI news"
}

Any non-input key hits the else: branch and blindly overwrites action_name. Here, action_name becomes "I should search Google..." instead of "google_search", causing subsequent tool invocation to fail with ToolNotFound.

2. Greedy Code Block Extraction Defect (Line 43)
blocks = re.findall(r"```[json]*\s*([\[{].*[]}])\s*```", code_block, re.DOTALL | re.IGNORECASE)
  • [json]* matches any permutation of characters j, s, o, n rather than an optional (?:json)? language identifier.
  • .* with re.DOTALL is greedy. When an agent output contains multiple code blocks in a multi-step stream, the regex matches from the first { across closing code fences and text until the final } of the last block. json.loads throws JSONDecodeError, and line 52 catches the exception and returns [], causing complete tool call dropping.
Reproduction Code
import re, json

# 1. Action Overwrite Repro
action = {
    "action": "google_search",
    "action_input": {"query": "AI news"},
    "thought": "Let me search"
}
action_name = None
action_input = None
for key, value in action.items():
    if "input" in key.lower():
        action_input = value
    else:
        action_name = value
print("action_name:", action_name)  # "Let me search" (Overwritten!)

# 2. Greedy Multi-Block Repro
code_text = """```json
{"action": "calc", "action_input": "2+2"}

Next:

{"action": "verify", "action_input": "4"}
```"""
blocks = re.findall(r"```[json]*\s*([\[{].*[]}])\s*```", code_text, re.DOTALL | re.IGNORECASE)
print("Blocks matched:", len(blocks))  # 1 fused block (invalid JSON)
Proposed Fix
  1. In parse_action, prioritize explicit keys (action, tool, action_name) before general scanning.
  2. In extra_json_from_code_block, use non-greedy matching [\s\S]*? with (?:json)?:
def parse_action(action) -> str | AgentScratchpadUnit.Action:
    if isinstance(action, str):
        try:
            action = json.loads(action, strict=False)
        except json.JSONDecodeError:
            return action or ""

    if isinstance(action, list) and len(action) == 1:
        action = action[0]

    if not isinstance(action, dict):
        return json.dumps(action)

    action_name = None
    action_input = None

    for key in ["action", "tool", "tool_name", "action_name"]:
        if key in action:
            action_name = action[key]
            break

    for key in ["action_input", "tool_input", "parameters", "arguments", "input"]:
        if key in action:
            action_input = action[key]
            break

    if action_name is None or action_input is None:
        for key, value in action.items():
            if "input" in key.lower() and action_input is None:
                action_input = value
            elif any(k in key.lower() for k in ["action", "tool"]) and action_name is None:
                action_name = value

    if action_name is not None and action_input is not None:
        return AgentScratchpadUnit.Action(
            action_name=action_name,
            action_input=action_input,
        )
    return json.dumps(action)

def extra_json_from_code_block(code_block) -> list[list | dict]:
    blocks = re.findall(r"```(?:json)?\s*([\[{][\s\S]*?[\]}])\s*```", code_block, re.IGNORECASE)
    if not blocks:
        return []
    json_blocks = []
    for block in blocks:
        try:
            json_text = re.sub(r"^[a-zA-Z]+\n", "", block.strip(), flags=re.MULTILINE)
            json_blocks.append(json.loads(json_text, strict=False))
        except Exception:
            continue
    return json_blocks

Tested locally: preserves exact tool name across all action payloads and correctly parses multiple code blocks in streaming CoT pipelines.

Dominant language
Python
Stars
160k
Forks
25.4k
Avg merge
1d 1h
Merged PRs (30d)
608

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from langgenius/dify

All issues in langgenius/dify

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.