diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index 0ecd8e63a4..151dcd183e 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -8,6 +8,7 @@ from datetime import datetime import inspect import json +import re import threading from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast from uuid import uuid4 @@ -3149,6 +3150,10 @@ async def _ainject_files_from_inputs(self, inputs: dict[str, Any]) -> None: def _format_prompt(prompt: str, inputs: dict[str, str]) -> str: """Format prompt template with input values. + Substitutes every placeholder in a single pass so a substituted value + that itself contains a placeholder token (e.g. task input mentioning + the literal ``{tools}``) is not clobbered by a later replacement. + Args: prompt: Template string. inputs: Values to substitute. @@ -3156,9 +3161,11 @@ def _format_prompt(prompt: str, inputs: dict[str, str]) -> str: Returns: Formatted prompt. """ - prompt = prompt.replace("{input}", inputs["input"]) - prompt = prompt.replace("{tool_names}", inputs["tool_names"]) - return prompt.replace("{tools}", inputs["tools"]) + return re.sub( + r"\{(input|tool_names|tools)\}", + lambda match: inputs[match.group(1)], + prompt, + ) def _handle_human_feedback(self, formatted_answer: AgentFinish) -> AgentFinish: """Process human feedback and refine answer. diff --git a/lib/crewai/tests/agents/test_agent_executor.py b/lib/crewai/tests/agents/test_agent_executor.py index e4de4a484b..3af214ea06 100644 --- a/lib/crewai/tests/agents/test_agent_executor.py +++ b/lib/crewai/tests/agents/test_agent_executor.py @@ -475,6 +475,29 @@ def test_format_prompt(self, mock_dependencies): assert "tool1, tool2" in result assert "desc" in result + def test_format_prompt_preserves_placeholder_tokens_in_values( + self, mock_dependencies + ): + """A value containing a placeholder token must not be re-substituted. + + Regression: sequential ``str.replace`` substituted ``{input}`` first, so + task input that legitimately mentioned ``{tools}`` or ``{tool_names}`` + had those tokens clobbered by the later replacements. + """ + executor = _build_executor(**mock_dependencies) + inputs = { + "input": "explain the {tools} and {tool_names} placeholders", + "tool_names": "search", + "tools": "search: web search", + } + + result = executor._format_prompt("Task: {input}\nTools: {tools}", inputs) + + assert result == ( + "Task: explain the {tools} and {tool_names} placeholders\n" + "Tools: search: web search" + ) + def test_is_training_mode_false(self, mock_dependencies): """Test training mode detection when not in training.""" executor = _build_executor(**mock_dependencies)