Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/crewai/src/crewai/agents/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,9 @@
ACTION_INPUT_ONLY_REGEX: Final[re.Pattern[str]] = re.compile(
r"\s*Action\s*\d*\s*Input\s*\d*\s*:\s*(.*)", re.DOTALL
)
# Regex to match "Action: None" or similar non-action values (None, N/A, etc.)
# This captures the action value and any text that follows it
ACTION_NONE_REGEX: Final[re.Pattern[str]] = re.compile(
r"Action\s*\d*\s*:\s*(none|n/a|na|no action|no_action)(?:\s*[-:(]?\s*(.*))?",
re.IGNORECASE | re.DOTALL,
)
29 changes: 29 additions & 0 deletions lib/crewai/src/crewai/agents/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from crewai.agents.constants import (
ACTION_INPUT_ONLY_REGEX,
ACTION_INPUT_REGEX,
ACTION_NONE_REGEX,
ACTION_REGEX,
FINAL_ANSWER_ACTION,
MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE,
Expand Down Expand Up @@ -118,6 +119,34 @@ def parse(text: str) -> AgentAction | AgentFinish:
thought=thought, tool=clean_action, tool_input=safe_tool_input, text=text
)

# Check for "Action: None" or similar non-action values
# This handles cases where the LLM indicates it cannot/should not use a tool
action_none_match = ACTION_NONE_REGEX.search(text)
if action_none_match:
# Extract any additional content after "Action: None"
additional_content = action_none_match.group(2)
if additional_content:
additional_content = additional_content.strip()
# Remove trailing parenthesis if present (from patterns like "Action: None (reason)")
if additional_content.startswith("(") and ")" in additional_content:
additional_content = additional_content.split(")", 1)[-1].strip()
elif additional_content.startswith(")"):
additional_content = additional_content[1:].strip()

# Build the final answer from thought and any additional content
final_answer = thought
if additional_content:
if final_answer:
final_answer = f"{final_answer}\n\n{additional_content}"
else:
final_answer = additional_content

# If we still have no content, use a generic message
if not final_answer:
final_answer = "I cannot perform this action with the available tools."

return AgentFinish(thought=thought, output=final_answer, text=text)

if not ACTION_REGEX.search(text):
raise OutputParserError(
f"{MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE}\n{_I18N.slice('final_answer_format')}",
Expand Down
89 changes: 89 additions & 0 deletions lib/crewai/tests/agents/test_crew_agent_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,92 @@ def test_integration_valid_and_invalid():


# TODO: ADD TEST TO MAKE SURE ** REMOVAL DOESN'T MESS UP ANYTHING


# Tests for Action: None handling (Issue #4186)
def test_action_none_basic():
"""Test that 'Action: None' is parsed as AgentFinish."""
text = "Thought: I cannot use any tool for this.\nAction: None"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "I cannot use any tool for this." in result.output


def test_action_none_with_reason_in_parentheses():
"""Test 'Action: None (reason)' format."""
text = "Thought: The tool is not available.\nAction: None (direct response required)"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "The tool is not available." in result.output


def test_action_none_lowercase():
"""Test that 'Action: none' (lowercase) is handled."""
text = "Thought: I should respond directly.\nAction: none"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "I should respond directly." in result.output


def test_action_na():
"""Test that 'Action: N/A' is handled."""
text = "Thought: No action needed here.\nAction: N/A"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "No action needed here." in result.output


def test_action_na_lowercase():
"""Test that 'Action: n/a' (lowercase) is handled."""
text = "Thought: This requires a direct answer.\nAction: n/a"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "This requires a direct answer." in result.output


def test_action_none_with_dash_separator():
"""Test 'Action: None - reason' format."""
text = "Thought: I need to provide a direct response.\nAction: None - direct response"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "I need to provide a direct response." in result.output


def test_action_none_with_additional_content():
"""Test 'Action: None' with additional content after."""
text = "Thought: I analyzed the request.\nAction: None\nHere is my direct response to your question."
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "I analyzed the request." in result.output


def test_action_no_action():
"""Test that 'Action: no action' is handled."""
text = "Thought: I will respond without using tools.\nAction: no action"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "I will respond without using tools." in result.output


def test_action_none_without_thought():
"""Test 'Action: None' without a thought prefix."""
text = "Action: None"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert result.output == "I cannot perform this action with the available tools."


def test_action_none_preserves_original_text():
"""Test that the original text is preserved in the result."""
text = "Thought: I cannot delegate this task.\nAction: None"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert result.text == text


def test_action_none_with_colon_separator():
"""Test 'Action: None: reason' format."""
text = "Thought: Direct response needed.\nAction: None: providing direct answer"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "Direct response needed." in result.output
Loading