Skip to content

Fix prompt formatting biases affecting JSON output #2824

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
26 changes: 18 additions & 8 deletions src/crewai/tools/base_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,25 @@ def _set_args_schema(self):
)

def _generate_description(self):
args_schema = {
name: {
"description": field.description,
"type": BaseTool._get_arg_annotations(field.annotation),
import json
import logging

logger = logging.getLogger(__name__)

try:
args_schema = {
name: {
"description": field.description,
"type": BaseTool._get_arg_annotations(field.annotation),
}
for name, field in self.args_schema.model_fields.items()
}
for name, field in self.args_schema.model_fields.items()
}

self.description = f"Tool Name: {self.name}\nTool Arguments: {args_schema}\nTool Description: {self.description}"
args_json = json.dumps(args_schema)
except Exception as e:
logger.warning(f"Failed to serialize args schema: {e}")
args_json = str(args_schema)

self.description = f"Tool Name: {self.name}\nTool Arguments: {args_json}\nTool Description: {self.description}"

@staticmethod
def _get_arg_annotations(annotation: type[Any] | None) -> str:
Expand Down
2 changes: 1 addition & 1 deletion src/crewai/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"task": "\nCurrent Task: {input}\n\nBegin! This is VERY important to you, use the tools available and give your best Final Answer, your job depends on it!\n\nThought:",
"memory": "\n\n# Useful context: \n{memory}",
"role_playing": "You are {role}. {backstory}\nYour personal goal is: {goal}",
"tools": "\nYou ONLY have access to the following tools, and should NEVER make up tools that are not listed here:\n\n{tools}\n\nUse the following format:\n\nThought: you should always think about what to do\nAction: the action to take, only one name of [{tool_names}], just the name, exactly as it's written.\nAction Input: the input to the action, just a simple python dictionary, enclosed in curly braces, using \" to wrap keys and values.\nObservation: the result of the action\n\nOnce all necessary information is gathered:\n\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n",
"tools": "\nYou ONLY have access to the following tools, and should NEVER make up tools that are not listed here:\n\n{tools}\n\nUse the following format:\n\nThought: you should always think about what to do\nAction: the action to take, only one name of [{tool_names}], just the name, exactly as it's written.\nAction Input: the input to the action, just a simple python dictionary, enclosed in curly braces, using double quotes (\") to wrap keys and values.\nObservation: the result of the action\n\nOnce all necessary information is gathered:\n\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n",
"no_tools": "\nTo give my best complete final answer to the task use the exact following format:\n\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described.\n\nI MUST use these formats, my job depends on it!",
"format": "I MUST either use a tool (use one at time) OR give my best final answer not both at the same time. To Use the following format:\n\nThought: you should always think about what to do\nAction: the action to take, should be one of [{tool_names}]\nAction Input: the input to the action, dictionary enclosed in curly braces\nObservation: the result of the action\n... (this Thought/Action/Action Input/Result can repeat N times)\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described\n\n",
"final_answer_format": "If you don't need to use any more tools, you must give your best complete final answer, make sure it satisfies the expected criteria, use the EXACT format below:\n\nThought: I now can give a great answer\nFinal Answer: my best complete final answer to the task.\n\n",
Expand Down
85 changes: 85 additions & 0 deletions tests/tools/test_json_edge_cases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import json
from unittest.mock import MagicMock

import pytest
from pydantic import BaseModel, Field

from crewai.tools import BaseTool
from crewai.tools.tool_usage import ToolUsage


class TestComplexInput(BaseModel):
special_chars: str = Field(
..., description="Parameter with special characters: \"'\\{}[]"
)
nested_dict: dict = Field(
..., description="A nested dictionary parameter"
)
unicode_text: str = Field(
..., description="Text with unicode characters: 你好, こんにちは, مرحبا"
)


class TestComplexTool(BaseTool):
name: str = "Complex JSON Tool"
description: str = "A tool for testing complex JSON formatting"
args_schema: type[BaseModel] = TestComplexInput

def _run(self, special_chars: str, nested_dict: dict, unicode_text: str) -> str:
return f"Processed complex input successfully"


def test_complex_json_formatting():
"""Test that complex JSON with special characters and nested structures is formatted correctly."""
tool = TestComplexTool()

assert "Tool Arguments:" in tool.description

description_parts = tool.description.split("Tool Arguments: ")
json_str = description_parts[1].split("\nTool Description:")[0]

parsed_json = json.loads(json_str)

assert "special_chars" in parsed_json
assert "nested_dict" in parsed_json
assert "unicode_text" in parsed_json

assert "\"'\\{}[]" in parsed_json["special_chars"]["description"]

assert "你好" in parsed_json["unicode_text"]["description"]
assert "こんにちは" in parsed_json["unicode_text"]["description"]
assert "مرحبا" in parsed_json["unicode_text"]["description"]


def test_complex_tool_usage_render():
"""Test that complex tool usage renders with proper JSON formatting."""
tool = TestComplexTool()

tool_usage = ToolUsage(
tools_handler=MagicMock(),
tools=[tool],
original_tools=[tool],
tools_description="Tool for testing complex JSON formatting",
tools_names="test_complex_tool",
task=MagicMock(),
function_calling_llm=MagicMock(),
agent=MagicMock(),
action=MagicMock(),
)

rendered = tool_usage._render()

rendered_parts = rendered.split("Tool Arguments: ")
if len(rendered_parts) > 1:
json_str = rendered_parts[1].split("\nTool Description:")[0]

try:
parsed_json = json.loads(json_str)
assert True # If we get here, JSON parsing succeeded

assert "special_chars" in parsed_json
assert "nested_dict" in parsed_json
assert "unicode_text" in parsed_json

except json.JSONDecodeError:
assert False, "The rendered tool arguments are not valid JSON"
78 changes: 78 additions & 0 deletions tests/tools/test_json_formatting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import json
from unittest.mock import MagicMock

import pytest
from pydantic import BaseModel, Field

from crewai.tools import BaseTool
from crewai.tools.tool_usage import ToolUsage


class TestJsonInput(BaseModel):
test_param: str = Field(
..., description="A test parameter"
)
another_param: int = Field(
..., description="Another test parameter"
)


class TestJsonTool(BaseTool):
name: str = "Test JSON Tool"
description: str = "A tool for testing JSON formatting"
args_schema: type[BaseModel] = TestJsonInput

def _run(self, test_param: str, another_param: int) -> str:
return f"Received {test_param} and {another_param}"


def test_tool_description_json_formatting():
"""Test that the tool description uses proper JSON formatting with double quotes."""
tool = TestJsonTool()

assert "Tool Arguments:" in tool.description

description_parts = tool.description.split("Tool Arguments: ")
json_str = description_parts[1].split("\nTool Description:")[0]

parsed_json = json.loads(json_str)

assert "test_param" in parsed_json
assert "another_param" in parsed_json

assert '"test_param"' in json_str
assert '"another_param"' in json_str
assert "'" not in json_str # No single quotes should be present


def test_tool_usage_json_formatting():
"""Test that the tool usage renders with proper JSON formatting."""
tool = TestJsonTool()

tool_usage = ToolUsage(
tools_handler=MagicMock(),
tools=[tool],
original_tools=[tool],
tools_description="Tool for testing JSON formatting",
tools_names="test_json_tool",
task=MagicMock(),
function_calling_llm=MagicMock(),
agent=MagicMock(),
action=MagicMock(),
)

rendered = tool_usage._render()

rendered_parts = rendered.split("Tool Arguments: ")
if len(rendered_parts) > 1:
json_str = rendered_parts[1].split("\nTool Description:")[0]

try:
parsed_json = json.loads(json_str)
assert True # If we get here, JSON parsing succeeded
except json.JSONDecodeError:
assert False, "The rendered tool arguments are not valid JSON"

assert '"test_param"' in json_str
assert '"another_param"' in json_str
assert "'" not in json_str # No single quotes should be present
8 changes: 4 additions & 4 deletions tests/tools/test_tool_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,22 @@ def test_tool_usage_render():

rendered = tool_usage._render()

# Updated checks to match the actual output
# Updated checks to match the actual output with JSON formatting
assert "Tool Name: Random Number Generator" in rendered
assert "Tool Arguments:" in rendered
assert (
"'min_value': {'description': 'The minimum value of the range (inclusive)', 'type': 'int'}"
'"min_value": {"description": "The minimum value of the range (inclusive)", "type": "int"}'
in rendered
)
assert (
"'max_value': {'description': 'The maximum value of the range (inclusive)', 'type': 'int'}"
'"max_value": {"description": "The maximum value of the range (inclusive)", "type": "int"}'
in rendered
)
assert (
"Tool Description: Generates a random number within a specified range"
in rendered
)
assert (
"Tool Name: Random Number Generator\nTool Arguments: {'min_value': {'description': 'The minimum value of the range (inclusive)', 'type': 'int'}, 'max_value': {'description': 'The maximum value of the range (inclusive)', 'type': 'int'}}\nTool Description: Generates a random number within a specified range"
'Tool Name: Random Number Generator\nTool Arguments: {"min_value": {"description": "The minimum value of the range (inclusive)", "type": "int"}, "max_value": {"description": "The maximum value of the range (inclusive)", "type": "int"}}\nTool Description: Generates a random number within a specified range'
in rendered
)