From da5f60e7f3f9c39c9eef94141df9c1d4732a91b3 Mon Sep 17 00:00:00 2001 From: lucasgomide Date: Mon, 24 Mar 2025 13:59:17 -0300 Subject: [PATCH] fix: properly clone ConditionalTask instances Previously copying a Task always returned an instance of Task even when we are cloning a subclass, such ConditionalTask. This commit ensures that the clone preserve the original class type --- src/crewai/task.py | 12 +- .../test_before_kickoff_callback.yaml | 113 ++++++++++++++++++ tests/task_test.py | 19 +++ 3 files changed, 142 insertions(+), 2 deletions(-) diff --git a/src/crewai/task.py b/src/crewai/task.py index 10358147c..c74e0081e 100644 --- a/src/crewai/task.py +++ b/src/crewai/task.py @@ -572,7 +572,15 @@ class Task(BaseModel): def copy( self, agents: List["BaseAgent"], task_mapping: Dict[str, "Task"] ) -> "Task": - """Create a deep copy of the Task.""" + """Creates a deep copy of the Task while preserving its original class type. + + Args: + agents: List of agents available for the task. + task_mapping: Dictionary mapping task IDs to Task instances. + + Returns: + A copy of the task with the same class type as the original. + """ exclude = { "id", "agent", @@ -595,7 +603,7 @@ class Task(BaseModel): cloned_agent = get_agent_by_role(self.agent.role) if self.agent else None cloned_tools = copy(self.tools) if self.tools else [] - copied_task = Task( + copied_task = self.__class__( **copied_data, context=cloned_context, agent=cloned_agent, diff --git a/tests/cassettes/test_before_kickoff_callback.yaml b/tests/cassettes/test_before_kickoff_callback.yaml index ecc2833c3..75c10cfb5 100644 --- a/tests/cassettes/test_before_kickoff_callback.yaml +++ b/tests/cassettes/test_before_kickoff_callback.yaml @@ -710,4 +710,117 @@ interactions: - req_4ceac9bc8ae57f631959b91d2ab63c4d http_version: HTTP/1.1 status_code: 200 +- request: + body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test agent + backstory\nYour personal goal is: Test agent goal\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: Test task description\n\nThis is the expected criteria for your final + answer: Test expected output\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", "stop": ["\nObservation:"]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '840' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.61.0 + x-stainless-arch: + - x64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.61.0 + x-stainless-raw-response: + - 'true' + 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: + content: "{\n \"id\": \"chatcmpl-BExKOliqPgvHyozZaBu5oN50CHtsa\",\n \"object\": + \"chat.completion\",\n \"created\": 1742904348,\n \"model\": \"gpt-4o-2024-08-06\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"I now can give a great answer \\nFinal + Answer: Test expected output\",\n \"refusal\": null,\n \"annotations\": + []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 158,\n \"completion_tokens\": + 15,\n \"total_tokens\": 173,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n + \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_90d33c15d4\"\n}\n" + headers: + CF-RAY: + - 925e4749af02f227-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 25 Mar 2025 12:05:48 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=VHa7Z7dJYptxXpaMxgldvK6HqIM.m74xpi.80N_EBDc-1742904348-1.0.1.1-VthD2riCSnAprFYhOZxfIrTjT33tybJHpHWB25Q_Hx4vuACCyF00tix6e6eorDReGcW3jb5cUzbGqYi47TrMsS4LYjxBv5eCo7cU9OuFajs; + path=/; expires=Tue, 25-Mar-25 12:35:48 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=Is8fSaH3lU8yHyT3fI7cRZiDqIYSI6sPpzfzvEV8HMc-1742904348760-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: + - '377' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-ratelimit-limit-requests: + - '50000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '49999' + x-ratelimit-remaining-tokens: + - '149999822' + x-ratelimit-reset-requests: + - 1ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_fd6b93e3b1a30868482c72306e7f63c2 + http_version: HTTP/1.1 + status_code: 200 version: 1 diff --git a/tests/task_test.py b/tests/task_test.py index 67ce99910..297ed084f 100644 --- a/tests/task_test.py +++ b/tests/task_test.py @@ -787,6 +787,25 @@ def test_conditional_task_definition_based_on_dict(): assert task.agent is None +def test_conditional_task_copy_preserves_type(): + task_config = { + "description": "Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work', check examples to based your evaluation.", + "expected_output": "The score of the title.", + } + original_task = Task(**task_config) + copied_task = original_task.copy(agents=[], task_mapping={}) + assert isinstance(copied_task, Task) + + original_conditional_config = { + "description": "Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'. Check examples to base your evaluation on.", + "expected_output": "The score of the title.", + "condition": lambda x: True, + } + original_conditional_task = ConditionalTask(**original_conditional_config) + copied_conditional_task = original_conditional_task.copy(agents=[], task_mapping={}) + assert isinstance(copied_conditional_task, ConditionalTask) + + def test_interpolate_inputs(): task = Task( description="Give me a list of 5 interesting ideas about {topic} to explore for an article, what makes them unique and interesting.",