mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-28 18:19:22 +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
|
from hashlib import md5
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Annotated,
|
Annotated,
|
||||||
@@ -142,7 +141,10 @@ from crewai.utilities.streaming import (
|
|||||||
signal_end,
|
signal_end,
|
||||||
signal_error,
|
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.task_output_storage_handler import TaskOutputStorageHandler
|
||||||
from crewai.utilities.training_handler import CrewTrainingHandler
|
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
|
Scans each task's 'description' + 'expected_output', and each agent's
|
||||||
'role', 'goal', and 'backstory'.
|
'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.
|
Returns a set of all discovered placeholder names.
|
||||||
"""
|
"""
|
||||||
placeholder_pattern = re.compile(r"\{(.+?)}")
|
|
||||||
required_inputs: set[str] = set()
|
required_inputs: set[str] = set()
|
||||||
|
|
||||||
for task in self.tasks:
|
for task in self.tasks:
|
||||||
# description and expected_output might contain e.g. {topic}, {user_name}
|
# description and expected_output might contain e.g. {topic}, {user_name}
|
||||||
text = f"{task.description or ''} {task.expected_output or ''}"
|
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:
|
for agent in self.agents:
|
||||||
# role, goal, backstory might have placeholders like {role_detail}, etc.
|
# role, goal, backstory might have placeholders like {role_detail}, etc.
|
||||||
text = f"{agent.role or ''} {agent.goal or ''} {agent.backstory or ''}"
|
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
|
return required_inputs
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,26 @@ def _duplicate_separator_pattern(separator: str) -> re.Pattern[str]:
|
|||||||
return re.compile(f"(?:{re.escape(separator)}){{2,}}")
|
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:
|
def sanitize_tool_name(name: str, max_length: int = _MAX_TOOL_NAME_LENGTH) -> str:
|
||||||
"""Sanitize tool name for LLM provider compatibility.
|
"""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()
|
@pytest.mark.vcr()
|
||||||
def test_task_tools_preserve_code_execution_tools():
|
def test_task_tools_preserve_code_execution_tools():
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,45 @@
|
|||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
import pytest
|
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:
|
class TestInterpolateOnly:
|
||||||
|
|||||||
Reference in New Issue
Block a user