diff --git a/src/crewai/utilities/string_utils.py b/src/crewai/utilities/string_utils.py index 9a1857781..255e66a0b 100644 --- a/src/crewai/utilities/string_utils.py +++ b/src/crewai/utilities/string_utils.py @@ -59,7 +59,7 @@ def interpolate_only( # The regex pattern to find valid variable placeholders # Matches {variable_name} where variable_name starts with a letter/underscore # and contains only letters, numbers, and underscores - pattern = r"\{([A-Za-z_][A-Za-z0-9_]*)\}" + pattern = r"\{([A-Za-z_][A-Za-z0-9_\-]*)\}" # Find all matching variables in the input string variables = re.findall(pattern, input_string) diff --git a/tests/cassettes/test_task_interpolation_with_hyphens.yaml b/tests/cassettes/test_task_interpolation_with_hyphens.yaml new file mode 100644 index 000000000..f0f1e87c2 --- /dev/null +++ b/tests/cassettes/test_task_interpolation_with_hyphens.yaml @@ -0,0 +1,121 @@ +interactions: +- request: + body: '{"messages": [{"role": "system", "content": "You are Researcher. You''re + an expert researcher, specialized in technology, software engineering, AI and + startups. You work as a freelancer and is now working on doing research and + analysis for a new customer.\nYour personal goal is: be an assistant that responds + with say hello world\nTo give my best complete final answer to the task respond + using the exact following format:\n\nThought: I now can give a great answer\nFinal + Answer: Your final answer must be the great and the most complete as possible, + it must be outcome described.\n\nI MUST use these formats, my job depends on + it!"}, {"role": "user", "content": "\nCurrent Task: be an assistant that responds + with say hello world\n\nThis is the expected criteria for your final answer: + The response should be addressing: say hello world\nyou MUST return the actual + complete content as the final answer, not a summary.\n\nBegin! This is VERY + important to you, use the tools available and give your best Final Answer, your + job depends on it!\n\nThought:"}], "model": "gpt-4o-mini", "stop": ["\nObservation:"]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '1108' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.68.2 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.68.2 + x-stainless-raw-response: + - 'true' + x-stainless-read-timeout: + - '600.0' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: !!binary | + H4sIAAAAAAAAA4xSTW/UMBC951cMPicoScMu3RuIooUDcOOrVeS1J4mp4zG2sy2q9r9XTrqbtBSJ + iyX7zXt+b2buEgCmJNsAEx0Porc6e/vt4nf3xVxweVZ+3v/Q17fF9+pjs92+O//0iqWRQbtfKMKR + 9VJQbzUGRWaChUMeMKoW62pdropVfjYCPUnUkdbakFWU9cqorMzLKsvXWfH6gd2REujZBn4mAAB3 + 4xl9Gom3bAN5enzp0XveItucigCYIx1fGPde+cBNYOkMCjIBzWj9Axi6AcENtGqPwKGNtoEbf4MO + 4NK8V4ZreDPeN7BFrSmFr+S0fLGUdNgMnsdYZtB6AXBjKPDYljHM1QNyONnX1FpHO/+EyhpllO9q + h9yTiVZ9IMtG9JAAXI1tGh4lZ9ZRb0Md6BrH78p8NemxeTozWhzBQIHrBass02f0aomBK+0XjWaC + iw7lTJ2nwgepaAEki9R/u3lOe0quTPs/8jMgBNqAsrYOpRKPE89lDuPy/qvs1OXRMPPo9kpgHRS6 + OAmJDR/0tFLM//EB+7pRpkVnnZr2qrH1utjl5bo6bzhLDsk9AAAA//8DAAxaM/dlAwAA + headers: + CF-RAY: + - 93fdd19cdbfb6428-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 14 May 2025 22:26:43 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=eCtOgOCsKt_ybdNPdtFAocCmuQbNltR52chaHVe7Y_Q-1747261603-1.0.1.1-827eoA7wHS5SOkTsTqoMq6OSioi0VznQBVjvmabNSVX1bf5PpWZvblw58iggZ_wyKDB0EuVoeLKFspgBJa0kuQYR17hu43Y2C14sgdvOXIE; + path=/; expires=Wed, 14-May-25 22:56:43 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=QUa5MnypdaVxO826bwdQaN4G6CBEV8HYVV.7OLF.qvQ-1747261603742-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '307' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '309' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999757' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_61d9066e0258b7095517f9f9c01d38e9 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/task_test.py b/tests/task_test.py index b09f69646..42d709a4f 100644 --- a/tests/task_test.py +++ b/tests/task_test.py @@ -837,9 +837,6 @@ def test_interpolate_inputs(): def test_interpolate_only(): """Test the interpolate_only method for various scenarios including JSON structure preservation.""" - task = Task( - description="Unused in this test", expected_output="Unused in this test" - ) # Test JSON structure preservation json_string = '{"info": "Look at {placeholder}", "nested": {"val": "{nestedVal}"}}' @@ -871,10 +868,6 @@ def test_interpolate_only(): def test_interpolate_only_with_dict_inside_expected_output(): """Test the interpolate_only method for various scenarios including JSON structure preservation.""" - task = Task( - description="Unused in this test", - expected_output="Unused in this test: {questions}", - ) json_string = '{"questions": {"main_question": "What is the user\'s name?", "secondary_question": "What is the user\'s age?"}}' result = interpolate_only( @@ -1094,11 +1087,6 @@ def test_task_execution_times(): def test_interpolate_with_list_of_strings(): - task = Task( - description="Test list interpolation", - expected_output="List: {items}", - ) - # Test simple list of strings input_str = "Available items: {items}" inputs = {"items": ["apple", "banana", "cherry"]} @@ -1112,11 +1100,6 @@ def test_interpolate_with_list_of_strings(): def test_interpolate_with_list_of_dicts(): - task = Task( - description="Test list of dicts interpolation", - expected_output="People: {people}", - ) - input_data = { "people": [ {"name": "Alice", "age": 30, "skills": ["Python", "AI"]}, @@ -1137,11 +1120,6 @@ def test_interpolate_with_list_of_dicts(): def test_interpolate_with_nested_structures(): - task = Task( - description="Test nested structures", - expected_output="Company: {company}", - ) - input_data = { "company": { "name": "TechCorp", @@ -1165,11 +1143,6 @@ def test_interpolate_with_nested_structures(): def test_interpolate_with_special_characters(): - task = Task( - description="Test special characters in dicts", - expected_output="Data: {special_data}", - ) - input_data = { "special_data": { "quotes": """This has "double" and 'single' quotes""", @@ -1188,11 +1161,6 @@ def test_interpolate_with_special_characters(): def test_interpolate_mixed_types(): - task = Task( - description="Test mixed type interpolation", - expected_output="Mixed: {data}", - ) - input_data = { "data": { "name": "Test Dataset", @@ -1214,11 +1182,6 @@ def test_interpolate_mixed_types(): def test_interpolate_complex_combination(): - task = Task( - description="Test complex combination", - expected_output="Report: {report}", - ) - input_data = { "report": [ { @@ -1243,11 +1206,6 @@ def test_interpolate_complex_combination(): def test_interpolate_invalid_type_validation(): - task = Task( - description="Test invalid type validation", - expected_output="Should never reach here", - ) - # Test with invalid top-level type with pytest.raises(ValueError) as excinfo: interpolate_only("{data}", {"data": set()}) # type: ignore we are purposely testing this failure @@ -1268,11 +1226,6 @@ def test_interpolate_invalid_type_validation(): def test_interpolate_custom_object_validation(): - task = Task( - description="Test custom object rejection", - expected_output="Should never reach here", - ) - class CustomObject: def __init__(self, value): self.value = value @@ -1304,11 +1257,6 @@ def test_interpolate_custom_object_validation(): def test_interpolate_valid_complex_types(): - task = Task( - description="Test valid complex types", - expected_output="Validation should pass", - ) - # Valid complex structure valid_data = { "name": "Valid Dataset", @@ -1328,11 +1276,6 @@ def test_interpolate_valid_complex_types(): def test_interpolate_edge_cases(): - task = Task( - description="Test edge cases", - expected_output="Edge case handling", - ) - # Test empty dict and list assert interpolate_only("{}", {"data": {}}) == "{}" assert interpolate_only("[]", {"data": []}) == "[]" @@ -1347,11 +1290,6 @@ def test_interpolate_edge_cases(): def test_interpolate_valid_types(): - task = Task( - description="Test valid types including null and boolean", - expected_output="Should pass validation", - ) - # Test with boolean and null values (valid JSON types) valid_data = { "name": "Test", @@ -1373,11 +1311,11 @@ def test_interpolate_valid_types(): def test_task_with_no_max_execution_time(): researcher = Agent( - role="Researcher", - goal="Make the best research and analysis on content about AI and AI agents", - backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.", - allow_delegation=False, - max_execution_time=None + role="Researcher", + goal="Make the best research and analysis on content about AI and AI agents", + backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.", + allow_delegation=False, + max_execution_time=None, ) task = Task( @@ -1386,7 +1324,7 @@ def test_task_with_no_max_execution_time(): agent=researcher, ) - with patch.object(Agent, "_execute_without_timeout", return_value = "ok") as execute: + with patch.object(Agent, "_execute_without_timeout", return_value="ok") as execute: result = task.execute_sync(agent=researcher) assert result.raw == "ok" execute.assert_called_once() @@ -1395,6 +1333,7 @@ def test_task_with_no_max_execution_time(): @pytest.mark.vcr(filter_headers=["authorization"]) def test_task_with_max_execution_time(): from crewai.tools import tool + """Test that execution raises TimeoutError when max_execution_time is exceeded.""" @tool("what amazing tool", result_as_answer=True) @@ -1412,7 +1351,7 @@ def test_task_with_max_execution_time(): ), allow_delegation=False, tools=[my_tool], - max_execution_time=4 + max_execution_time=4, ) task = Task( @@ -1428,6 +1367,7 @@ def test_task_with_max_execution_time(): @pytest.mark.vcr(filter_headers=["authorization"]) def test_task_with_max_execution_time_exceeded(): from crewai.tools import tool + """Test that execution raises TimeoutError when max_execution_time is exceeded.""" @tool("what amazing tool", result_as_answer=True) @@ -1445,7 +1385,7 @@ def test_task_with_max_execution_time_exceeded(): ), allow_delegation=False, tools=[my_tool], - max_execution_time=1 + max_execution_time=1, ) task = Task( @@ -1455,4 +1395,28 @@ def test_task_with_max_execution_time_exceeded(): ) with pytest.raises(TimeoutError): - task.execute_sync(agent=researcher) \ No newline at end of file + task.execute_sync(agent=researcher) + + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_task_interpolation_with_hyphens(): + agent = Agent( + role="Researcher", + goal="be an assistant that responds with {interpolation-with-hyphens}", + backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.", + allow_delegation=False, + ) + task = Task( + description="be an assistant that responds with {interpolation-with-hyphens}", + expected_output="The response should be addressing: {interpolation-with-hyphens}", + agent=agent, + ) + crew = Crew( + agents=[agent], + tasks=[task], + verbose=True, + ) + result = crew.kickoff(inputs={"interpolation-with-hyphens": "say hello world"}) + assert "say hello world" in task.prompt() + + assert result.raw == "Hello, World!"