mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-01 05:08:12 +00:00
fix: only treat interpolatable placeholders as crew inputs
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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():
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user