mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-08 14:13:54 -05:00
fix: handle 'Action: None' in parser to prevent OutputParserError
When LLMs output 'Action: None' (or variations like 'Action: N/A'), the parser now correctly treats this as a signal for a direct response instead of raising an OutputParserError. This fixes issue #4186 where the parser would fail and leak internal 'Thought:' text to users instead of providing a clean response. Changes: - Add ACTION_NONE_REGEX constant to match non-action values - Update parse() to detect and handle Action: None patterns - Convert Action: None to AgentFinish with the thought as output - Add comprehensive tests for all variations Closes #4186 Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
@@ -26,3 +26,9 @@ ACTION_REGEX: Final[re.Pattern[str]] = re.compile(
|
|||||||
ACTION_INPUT_ONLY_REGEX: Final[re.Pattern[str]] = re.compile(
|
ACTION_INPUT_ONLY_REGEX: Final[re.Pattern[str]] = re.compile(
|
||||||
r"\s*Action\s*\d*\s*Input\s*\d*\s*:\s*(.*)", re.DOTALL
|
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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from json_repair import repair_json # type: ignore[import-untyped]
|
|||||||
from crewai.agents.constants import (
|
from crewai.agents.constants import (
|
||||||
ACTION_INPUT_ONLY_REGEX,
|
ACTION_INPUT_ONLY_REGEX,
|
||||||
ACTION_INPUT_REGEX,
|
ACTION_INPUT_REGEX,
|
||||||
|
ACTION_NONE_REGEX,
|
||||||
ACTION_REGEX,
|
ACTION_REGEX,
|
||||||
FINAL_ANSWER_ACTION,
|
FINAL_ANSWER_ACTION,
|
||||||
MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE,
|
MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE,
|
||||||
@@ -118,6 +119,34 @@ def parse(text: str) -> AgentAction | AgentFinish:
|
|||||||
thought=thought, tool=clean_action, tool_input=safe_tool_input, text=text
|
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):
|
if not ACTION_REGEX.search(text):
|
||||||
raise OutputParserError(
|
raise OutputParserError(
|
||||||
f"{MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE}\n{_I18N.slice('final_answer_format')}",
|
f"{MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE}\n{_I18N.slice('final_answer_format')}",
|
||||||
|
|||||||
@@ -360,3 +360,92 @@ def test_integration_valid_and_invalid():
|
|||||||
|
|
||||||
|
|
||||||
# TODO: ADD TEST TO MAKE SURE ** REMOVAL DOESN'T MESS UP ANYTHING
|
# 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
|
||||||
|
|||||||
Reference in New Issue
Block a user