Skip to content
Open
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
50 changes: 47 additions & 3 deletions lib/crewai/src/crewai/utilities/reasoning_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import json
import logging
import re
from typing import TYPE_CHECKING, Any, Final, Literal, cast

from pydantic import BaseModel, Field
Expand All @@ -26,6 +27,49 @@
from crewai.agent.planning_config import PlanningConfig
from crewai.task import Task

# Full instructional phrase still emitted by the default planning prompts.
_READY_FULL_PHRASE: Final[str] = "READY: I am ready to execute the task."
# Models (especially local/Ollama) often conclude with a bare READY / NOT READY
# marker at the start of a line. Anchor at line start to avoid mid-sentence hits
# like "I am ready to begin researching".
_READY_LINE_RE: Final[re.Pattern[str]] = re.compile(
r"^\s*(NOT\s+)?READY\b",
re.IGNORECASE,
)


def response_indicates_ready(response: str) -> bool:
"""Return whether a planning response concludes the agent is READY.

Accepts the full instructional phrase and short-form markers such as a
trailing ``READY`` / ``READY.`` line. ``NOT READY`` always wins over a
bare ``READY`` substring match (e.g. inside ``NOT READY``).

When multiple markers appear (common during plan refinement), the **last**
explicit READY / NOT READY line is authoritative.

Args:
response: Raw LLM planning response text.

Returns:
True if the response indicates readiness to execute the task.
"""
if not response:
return False

last_decision: bool | None = None
for line in response.splitlines():
match = _READY_LINE_RE.match(line)
if match is None:
continue
# Group 1 is the optional "NOT " prefix.
last_decision = match.group(1) is None

if last_decision is not None:
return last_decision

return _READY_FULL_PHRASE in response

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make full-phrase matching case-insensitive.

Line 71 uses a case-sensitive containment check. A lowercase full phrase that is not at line start does not match the short-marker regex and returns False. Normalize both strings with casefold(). Add a regression test for an embedded lowercase full phrase.

  • lib/crewai/src/crewai/utilities/reasoning_handler.py#L71-L71: compare response.casefold() with _READY_FULL_PHRASE.casefold().
  • lib/crewai/tests/utilities/test_response_indicates_ready.py#L14-L20: add an assertion for an embedded lowercase full phrase.
Proposed fix
-    return _READY_FULL_PHRASE in response
+    return _READY_FULL_PHRASE.casefold() in response.casefold()
📍 Affects 2 files
  • lib/crewai/src/crewai/utilities/reasoning_handler.py#L71-L71 (this comment)
  • lib/crewai/tests/utilities/test_response_indicates_ready.py#L14-L20
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/utilities/reasoning_handler.py` at line 71, Update the
full-phrase check in reasoning_handler.py at lines 71-71 to compare
response.casefold() against _READY_FULL_PHRASE.casefold(), preserving the
existing matching behavior. Add a regression assertion in
test_response_indicates_ready.py at lines 14-20 covering an embedded lowercase
full phrase.



class ReasoningPlan(BaseModel):
"""Model representing a reasoning plan for a task."""
Expand Down Expand Up @@ -409,7 +453,7 @@ def _create_reasoning_plan(
return (
response_str,
[],
"READY: I am ready to execute the task." in response_str,
response_indicates_ready(response_str),
)

except Exception as e:
Expand All @@ -433,7 +477,7 @@ def _create_reasoning_plan(
return (
fallback_str,
[],
"READY: I am ready to execute the task." in fallback_str,
response_indicates_ready(fallback_str),
)
except Exception as inner_e:
self.logger.error(f"Error during fallback text parsing: {inner_e!s}")
Expand Down Expand Up @@ -593,7 +637,7 @@ def _parse_planning_response(response: str) -> tuple[str, bool]:
return "No plan was generated.", False

plan = response
ready = "READY: I am ready to execute the task." in response
ready = response_indicates_ready(response)

return plan, ready

Expand Down
104 changes: 104 additions & 0 deletions lib/crewai/tests/utilities/test_response_indicates_ready.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Tests for planning READY marker detection (#6204)."""

import pytest

from crewai.utilities.reasoning_handler import (
AgentReasoning,
response_indicates_ready,
)


class TestResponseIndicatesReady:
"""Unit tests for response_indicates_ready()."""

def test_full_instructional_phrase(self) -> None:
"""完整就绪指令短语应判定为就绪。"""
text = (
"1. Gather data\n2. Analyze\n\n"
"READY: I am ready to execute the task."
)
assert response_indicates_ready(text) is True

def test_not_ready_overrides_full_instructional_phrase(self) -> None:
"""完整就绪短语后的 NOT READY 应作为最终判定。"""
text = "READY: I am ready to execute the task.\nNOT READY"
assert response_indicates_ready(text) is False

def test_bare_ready_on_its_own_line(self) -> None:
"""独占一行的 READY 应判定为就绪。"""
text = "Step 1: brainstorm ideas\nStep 2: refine them\n\nREADY\n"
assert response_indicates_ready(text) is True

def test_bare_ready_with_period(self) -> None:
"""带句号的独立 READY 标记应判定为就绪。"""
assert response_indicates_ready("Plan looks solid.\n\nREADY.") is True

def test_ready_with_colon_short_form(self) -> None:
"""带冒号的 READY 简写应判定为就绪。"""
assert response_indicates_ready("Details...\nREADY: proceed") is True

def test_not_ready_is_false(self) -> None:
"""独立的 NOT READY 标记应判定为未就绪。"""
text = "Still missing context.\n\nNOT READY"
assert response_indicates_ready(text) is False

def test_not_ready_does_not_match_as_ready(self) -> None:
"""NOT READY 中的 READY 子串不应误判为就绪。"""
assert response_indicates_ready("NOT READY") is False
assert response_indicates_ready("NOT READY.") is False

def test_mid_sentence_ready_is_not_a_marker(self) -> None:
"""句子中间的 ready 单词不应视为就绪标记。"""
assert (
response_indicates_ready("I am ready to begin researching sources.")
is False
)

def test_last_marker_wins_after_refinement(self) -> None:
"""计划细化后应以最后一个 READY 标记为准。"""
text = (
"Initial thoughts...\nNOT READY\n"
"Refined plan with missing pieces filled in.\nREADY"
)
assert response_indicates_ready(text) is True

def test_last_marker_can_revert_to_not_ready(self) -> None:
"""最后一个 NOT READY 标记应撤销先前的就绪判定。"""
text = "READY\nActually wait, still incomplete.\nNOT READY"
assert response_indicates_ready(text) is False

def test_empty_and_none_like(self) -> None:
"""空响应和仅含空白的响应应判定为未就绪。"""
assert response_indicates_ready("") is False
assert response_indicates_ready(" \n ") is False

def test_plan_without_marker_is_not_ready(self) -> None:
"""不含就绪标记的计划应判定为未就绪。"""
assert response_indicates_ready("Just a plan with no conclusion.") is False

def test_case_insensitive_ready_line(self) -> None:
"""就绪标记匹配应忽略字母大小写。"""
assert response_indicates_ready("plan body\nready") is True
assert response_indicates_ready("plan body\nnot ready") is False


class TestParsePlanningResponseReady:
"""Ensure _parse_planning_response uses the shared detector."""

@pytest.mark.parametrize(
("response", "expected_ready"),
[
("Plan...\nREADY: I am ready to execute the task.", True),
("Plan...\nREADY", True),
("Plan...\nNOT READY", False),
("", False),
],
)
def test_parse_planning_response(self, response: str, expected_ready: bool) -> None:
"""规划响应解析应复用共享的就绪检测逻辑。"""
plan, ready = AgentReasoning._parse_planning_response(response)
assert ready is expected_ready
if response:
assert plan == response
else:
assert plan == "No plan was generated."