Compare commits

...

1 Commits

Author SHA1 Message Date
Gabe
2f5928e4bb fix: only treat interpolatable placeholders as crew inputs 2026-06-09 13:42:42 -03:00
4 changed files with 93 additions and 6 deletions

View File

@@ -7,7 +7,6 @@ from copy import copy as shallow_copy
from hashlib import md5
import json
from pathlib import Path
import re
from typing import (
TYPE_CHECKING,
Annotated,
@@ -142,7 +141,10 @@ from crewai.utilities.streaming import (
signal_end,
signal_error,
)
from crewai.utilities.string_utils import sanitize_tool_name
from crewai.utilities.string_utils import (
extract_template_variables,
sanitize_tool_name,
)
from crewai.utilities.task_output_storage_handler import TaskOutputStorageHandler
from crewai.utilities.training_handler import CrewTrainingHandler
@@ -1960,20 +1962,24 @@ class Crew(FlowTrackable, BaseModel):
Scans each task's 'description' + 'expected_output', and each agent's
'role', 'goal', and 'backstory'.
Only placeholders that interpolation can actually fill are returned;
non-identifier expressions such as ``{x if x else "y"}`` are ignored so
they are not surfaced as required inputs (matching interpolation
behavior, see :func:`extract_template_variables`).
Returns a set of all discovered placeholder names.
"""
placeholder_pattern = re.compile(r"\{(.+?)}")
required_inputs: set[str] = set()
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(extract_template_variables(text))
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(extract_template_variables(text))
return required_inputs

View File

@@ -23,6 +23,26 @@ def _duplicate_separator_pattern(separator: str) -> re.Pattern[str]:
return re.compile(f"(?:{re.escape(separator)}){{2,}}")
def extract_template_variables(input_string: str | None) -> list[str]:
"""Return the template variable names referenced in a string.
Only recognizes placeholders that interpolation can actually fill, i.e.
``{name}`` where ``name`` starts with a letter/underscore and contains only
letters, numbers, underscores, and hyphens. Expressions such as
``{x if x else "y"}`` or JSON snippets are intentionally ignored so they are
never treated as required inputs.
Args:
input_string: The string to scan. May be ``None`` or empty.
Returns:
The matched variable names, in order of appearance (with duplicates).
"""
if not input_string:
return []
return _VARIABLE_PATTERN.findall(input_string)
def sanitize_tool_name(name: str, max_length: int = _MAX_TOOL_NAME_LENGTH) -> str:
"""Sanitize tool name for LLM provider compatibility.

View File

@@ -3895,6 +3895,29 @@ def test_fetch_inputs():
)
def test_fetch_inputs_ignores_non_identifier_placeholders():
agent = Agent(
role="Report writer",
goal="Write a report for {company_name}.",
backstory="Expert reporter.",
)
task = Task(
description=(
'Greet {company_name if company_name else "Individual Client"} '
"and summarize {search_period}."
),
expected_output="A summary for {company_name}.",
agent=agent,
)
crew = Crew(agents=[agent], tasks=[task])
# Only the simple {company_name} placeholders are returned; the inline conditional
# expression (which interpolation cannot fill) is ignored.
assert crew.fetch_inputs() == {"company_name", "search_period"}
@pytest.mark.vcr()
def test_task_tools_preserve_code_execution_tools():
"""

View File

@@ -1,7 +1,45 @@
from typing import Any, Dict, List, Union
import pytest
from crewai.utilities.string_utils import interpolate_only
from crewai.utilities.string_utils import (
extract_template_variables,
interpolate_only,
)
class TestExtractTemplateVariables:
"""Tests for extract_template_variables in string_utils.py."""
def test_extracts_simple_identifiers(self):
assert extract_template_variables("Hi {name}, see {topic}.") == [
"name",
"topic",
]
def test_allows_underscores_and_hyphens(self):
assert extract_template_variables("{user_name} {role-detail}") == [
"user_name",
"role-detail",
]
def test_ignores_inline_expressions(self):
text = '{company_name if company_name else "Individual Client"}'
assert extract_template_variables(text) == []
def test_ignores_json_like_braces(self):
assert extract_template_variables('{"key": "value"}') == []
def test_matches_what_interpolation_fills(self):
text = 'Use {topic} and {x if x else "y"}.'
variables = extract_template_variables(text)
assert variables == ["topic"]
# interpolation fills exactly the extracted variable and leaves the rest
result = interpolate_only(text, {"topic": "AI"})
assert result == 'Use AI and {x if x else "y"}.'
@pytest.mark.parametrize("value", [None, ""])
def test_handles_empty_input(self, value):
assert extract_template_variables(value) == []
class TestInterpolateOnly: