diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index 9f69129f1a..e659849e95 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -7,7 +7,6 @@ from hashlib import md5 import json from pathlib import Path -import re from typing import ( TYPE_CHECKING, Annotated, @@ -142,7 +141,10 @@ def get_supported_content_types(provider: str, api: str | None = None) -> list[s signal_end, signal_error, ) -from crewai.utilities.string_utils import sanitize_tool_name +from crewai.utilities.string_utils import ( + TEMPLATE_VARIABLE_PATTERN, + sanitize_tool_name, +) from crewai.utilities.task_output_storage_handler import TaskOutputStorageHandler from crewai.utilities.training_handler import CrewTrainingHandler @@ -1971,21 +1973,25 @@ def fetch_inputs(self) -> set[str]: 'role', 'goal', and 'backstory'. Returns a set of all discovered placeholder names. + + Only valid template variables are reported — the same pattern used by + ``interpolate_only`` to substitute them. Literal braces (e.g. embedded + JSON examples or output schemas) are not interpolated, so they are not + treated as required inputs. """ - placeholder_pattern = re.compile(r"\{(.+?)}") required_inputs: set[str] = set() # Scan tasks for inputs for task in self.tasks: # description and expected_output might contain e.g. {topic}, {user_name} text = f"{task.description or ''} {task.expected_output or ''}" - required_inputs.update(placeholder_pattern.findall(text)) + required_inputs.update(TEMPLATE_VARIABLE_PATTERN.findall(text)) # Scan agents for inputs for agent in self.agents: # role, goal, backstory might have placeholders like {role_detail}, etc. text = f"{agent.role or ''} {agent.goal or ''} {agent.backstory or ''}" - required_inputs.update(placeholder_pattern.findall(text)) + required_inputs.update(TEMPLATE_VARIABLE_PATTERN.findall(text)) return required_inputs diff --git a/lib/crewai/src/crewai/utilities/string_utils.py b/lib/crewai/src/crewai/utilities/string_utils.py index a817f1ffb0..8932b9875a 100644 --- a/lib/crewai/src/crewai/utilities/string_utils.py +++ b/lib/crewai/src/crewai/utilities/string_utils.py @@ -9,7 +9,9 @@ import unicodedata -_VARIABLE_PATTERN: Final[re.Pattern[str]] = re.compile(r"\{([A-Za-z_][A-Za-z0-9_\-]*)}") +TEMPLATE_VARIABLE_PATTERN: Final[re.Pattern[str]] = re.compile( + r"\{([A-Za-z_][A-Za-z0-9_\-]*)}" +) _QUOTE_PATTERN: Final[re.Pattern[str]] = re.compile(r"[\'\"]+") _CAMEL_LOWER_UPPER: Final[re.Pattern[str]] = re.compile(r"([a-z])([A-Z])") _CAMEL_UPPER_LOWER: Final[re.Pattern[str]] = re.compile(r"([A-Z]+)([A-Z][a-z])") @@ -134,7 +136,7 @@ def _validate_type(validate_value: Any) -> None: "Inputs dictionary cannot be empty when interpolating variables" ) - variables = _VARIABLE_PATTERN.findall(input_string) + variables = TEMPLATE_VARIABLE_PATTERN.findall(input_string) result = input_string # Check if all variables exist in inputs diff --git a/lib/crewai/tests/test_crew.py b/lib/crewai/tests/test_crew.py index 4916504505..e2d2633215 100644 --- a/lib/crewai/tests/test_crew.py +++ b/lib/crewai/tests/test_crew.py @@ -3966,6 +3966,33 @@ def test_fetch_inputs(): ) +def test_fetch_inputs_ignores_literal_braces(): + """fetch_inputs must only report real template variables. + + Literal braces (embedded JSON examples, output schemas) and non-identifier + braces are not interpolated by ``interpolate_only``, so they must not be + reported as required inputs. Otherwise ``kickoff`` accepts inputs that are + silently ignored, while ``fetch_inputs`` advertises placeholders that can + never be filled. + """ + agent = Agent( + role="Researcher", + goal="Research on {topic}.", + backstory='Reply as JSON like {"name": "value"}.', + ) + + task = Task( + description='Analyze {topic} and return {"result": [1, 2, 3]}.', + expected_output="Summary of {topic}. Ignore {123} markers.", + agent=agent, + ) + + crew = Crew(agents=[agent], tasks=[task]) + + # Only the genuine identifier placeholder should be reported. + assert crew.fetch_inputs() == {"topic"} + + @pytest.mark.vcr() def test_task_tools_preserve_code_execution_tools(): """