mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-10 09:25:10 +00:00
fix(tools): harden composite-description handling after review
- Anchor the pre-baked-composite check to the actual three-line block shape instead of a naive substring match, so authored prose that merely mentions "Tool Description:" is never truncated (CodeRabbit / Bugbot review finding). Shared as strip_composite_description_prefix() and reused by the function- calling schema builder, which had the same naive split. - Make render_text_description_and_args tolerate duck-typed tools without a real formatted_description string (fixes CI: step-executor tests pass Mock tools whose auto-created attribute is not a str). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import asyncio
|
||||
from collections.abc import Callable
|
||||
import inspect
|
||||
import json
|
||||
import re
|
||||
import textwrap
|
||||
from typing import TYPE_CHECKING, Annotated, Any, cast, get_type_hints
|
||||
import warnings
|
||||
@@ -111,6 +112,30 @@ def build_schema_hint(args_schema: type[BaseModel]) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
# Matches a description that IS a pre-composed LLM block (as written by
|
||||
# older versions into the field, and by adapters that still bake it in).
|
||||
# Anchored to the full three-line shape so authored prose that merely
|
||||
# mentions "Tool Description:" is never mistaken for a composite. Greedy
|
||||
# ``.*`` keeps only the text after the LAST marker, matching the historical
|
||||
# split behavior for nested pre-baked blocks.
|
||||
_COMPOSITE_DESCRIPTION_RE = re.compile(
|
||||
r"^Tool Name:.*\nTool Arguments:.*\nTool Description:\s*",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def strip_composite_description_prefix(description: str) -> str:
|
||||
"""Return the authored text from a pre-composed LLM description block.
|
||||
|
||||
Descriptions that don't start with the composite shape are returned
|
||||
unchanged.
|
||||
"""
|
||||
match = _COMPOSITE_DESCRIPTION_RE.match(description)
|
||||
if match:
|
||||
return description[match.end() :]
|
||||
return description
|
||||
|
||||
|
||||
def format_description_for_llm(
|
||||
name: str,
|
||||
args_schema: type[BaseModel] | None,
|
||||
@@ -123,10 +148,12 @@ def format_description_for_llm(
|
||||
``description`` field itself is never mutated — prompt rendering calls
|
||||
this on demand.
|
||||
|
||||
Idempotent: if ``description`` already contains a composed block (e.g.
|
||||
a tool deserialized from a checkpoint written by an older version, or
|
||||
an adapter that bakes the composite into the field), only the authored
|
||||
text after the last ``"Tool Description:"`` marker is used.
|
||||
Idempotent: if ``description`` already *is* a composed block (e.g. a
|
||||
tool deserialized from a checkpoint written by an older version, or an
|
||||
adapter that bakes the composite into the field), only the authored
|
||||
text after the marker is used. The check is anchored to the composite
|
||||
shape, so authored prose that merely mentions ``"Tool Description:"``
|
||||
passes through untouched.
|
||||
|
||||
Args:
|
||||
name: The tool name (sanitized for the prompt).
|
||||
@@ -136,8 +163,7 @@ def format_description_for_llm(
|
||||
Returns:
|
||||
The composed, LLM-facing description block.
|
||||
"""
|
||||
if "Tool Description:" in description:
|
||||
description = description.split("Tool Description:")[-1].strip()
|
||||
description = strip_composite_description_prefix(description)
|
||||
if args_schema is not None:
|
||||
schema = generate_model_description(args_schema)
|
||||
args_json = json.dumps(schema["json_schema"]["schema"], indent=2)
|
||||
|
||||
@@ -27,7 +27,10 @@ from crewai.agents.parser import (
|
||||
from crewai.llms.base_llm import BaseLLM, call_stop_override
|
||||
from crewai.tools import BaseTool as CrewAITool
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
from crewai.tools.structured_tool import (
|
||||
CrewStructuredTool,
|
||||
strip_composite_description_prefix,
|
||||
)
|
||||
from crewai.tools.tool_types import ToolResult
|
||||
from crewai.utilities.errors import AgentRepositoryError
|
||||
from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||
@@ -147,7 +150,14 @@ def render_text_description_and_args(
|
||||
Returns:
|
||||
Plain text description of tools.
|
||||
"""
|
||||
tool_strings = [tool.formatted_description for tool in tools]
|
||||
# Fall back to the raw description for duck-typed tools (including test
|
||||
# mocks) that don't provide a real formatted_description string.
|
||||
tool_strings = [
|
||||
formatted
|
||||
if isinstance((formatted := getattr(tool, "formatted_description", None)), str)
|
||||
else tool.description
|
||||
for tool in tools
|
||||
]
|
||||
return "\n".join(tool_strings)
|
||||
|
||||
|
||||
@@ -190,10 +200,10 @@ def convert_tools_to_openai_schema(
|
||||
except Exception:
|
||||
parameters = {}
|
||||
|
||||
# BaseTool formats description as "Tool Name: ...\nTool Arguments: ...\nTool Description: {original}"
|
||||
description = tool.description
|
||||
if "Tool Description:" in description:
|
||||
description = description.split("Tool Description:")[-1].strip()
|
||||
# Old checkpoints and some adapters bake the composed LLM block
|
||||
# ("Tool Name: ...\nTool Arguments: ...\nTool Description: {authored}")
|
||||
# into the description field; keep only the authored text here.
|
||||
description = strip_composite_description_prefix(tool.description)
|
||||
|
||||
sanitized_name = sanitize_tool_name(tool.name)
|
||||
|
||||
|
||||
@@ -752,6 +752,20 @@ class TestAuthoredDescriptionPreserved:
|
||||
"Tool Description: Edited description."
|
||||
)
|
||||
|
||||
def test_prose_mentioning_the_marker_is_not_truncated(self):
|
||||
"""Authored text that merely mentions "Tool Description:" must reach
|
||||
the LLM untouched — only descriptions that ARE a pre-composed block
|
||||
(anchored three-line shape) get stripped."""
|
||||
tool_instance = self._make_tool()
|
||||
prose = (
|
||||
"Formats prompts. The output includes a line reading "
|
||||
"'Tool Description:' followed by the tool's summary."
|
||||
)
|
||||
tool_instance.description = prose
|
||||
assert tool_instance.formatted_description.endswith(
|
||||
f"Tool Description: {prose}"
|
||||
)
|
||||
|
||||
def test_composite_is_not_reapplied_to_prebaked_descriptions(self):
|
||||
"""A description that already contains a composed block (old
|
||||
checkpoints, adapters that bake the composite into the field) must
|
||||
|
||||
Reference in New Issue
Block a user