From d6254918fd0e37dbb8dafceea6581da6a00bb307 Mon Sep 17 00:00:00 2001 From: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:44:55 -0700 Subject: [PATCH] Lorenze/max retry defaults tools (#3362) * feat: enhance BaseTool and CrewStructuredTool with usage tracking This commit introduces a mechanism to track the usage count of tools within the CrewAI framework. The `BaseTool` class now includes a `_increment_usage_count` method that updates the current usage count, which is also reflected in the associated `CrewStructuredTool`. Additionally, a new test has been added to ensure that the maximum usage count is respected when invoking tools, enhancing the overall reliability and functionality of the tool system. * feat: add max usage count feature to tools documentation This commit introduces a new section in the tools overview documentation that explains the maximum usage count feature for tools within the CrewAI framework. Users can now set a limit on how many times a tool can be used, enhancing control over tool usage. An example of implementing the `FileReadTool` with a maximum usage count is also provided, improving the clarity and usability of the documentation. * undo field string --- docs/en/tools/overview.mdx | 15 + src/crewai/tools/base_tool.py | 10 +- src/crewai/tools/structured_tool.py | 18 + .../test_max_usage_count_is_respected.yaml | 845 ++++++++++++++++++ tests/tools/test_base_tool.py | 59 +- 5 files changed, 935 insertions(+), 12 deletions(-) create mode 100644 tests/cassettes/test_max_usage_count_is_respected.yaml diff --git a/docs/en/tools/overview.mdx b/docs/en/tools/overview.mdx index 8d69017bb..36d926a08 100644 --- a/docs/en/tools/overview.mdx +++ b/docs/en/tools/overview.mdx @@ -117,4 +117,19 @@ agent = Agent( ) ``` +## **Max Usage Count** + +You can set a maximum usage count for a tool to prevent it from being used more than a certain number of times. +By default, the max usage count is unlimited. + + + +```python +from crewai_tools import FileReadTool + +tool = FileReadTool(max_usage_count=5, ...) +``` + + + Ready to explore? Pick a category above to discover tools that fit your use case! diff --git a/src/crewai/tools/base_tool.py b/src/crewai/tools/base_tool.py index bf8d72cb3..5ddad0872 100644 --- a/src/crewai/tools/base_tool.py +++ b/src/crewai/tools/base_tool.py @@ -14,12 +14,14 @@ from pydantic import BaseModel as PydanticBaseModel from crewai.tools.structured_tool import CrewStructuredTool + class EnvVar(BaseModel): name: str description: str required: bool = True default: Optional[str] = None + class BaseTool(BaseModel, ABC): class _ArgsSchemaPlaceholder(PydanticBaseModel): pass @@ -108,7 +110,7 @@ class BaseTool(BaseModel, ABC): def to_structured_tool(self) -> CrewStructuredTool: """Convert this tool to a CrewStructuredTool instance.""" self._set_args_schema() - return CrewStructuredTool( + structured_tool = CrewStructuredTool( name=self.name, description=self.description, args_schema=self.args_schema, @@ -117,6 +119,8 @@ class BaseTool(BaseModel, ABC): max_usage_count=self.max_usage_count, current_usage_count=self.current_usage_count, ) + structured_tool._original_tool = self + return structured_tool @classmethod def from_langchain(cls, tool: Any) -> "BaseTool": @@ -276,7 +280,9 @@ def to_langchain( return [t.to_structured_tool() if isinstance(t, BaseTool) else t for t in tools] -def tool(*args, result_as_answer: bool = False, max_usage_count: int | None = None) -> Callable: +def tool( + *args, result_as_answer: bool = False, max_usage_count: int | None = None +) -> Callable: """ Decorator to create a tool from a function. diff --git a/src/crewai/tools/structured_tool.py b/src/crewai/tools/structured_tool.py index 22f8283aa..725031cd4 100644 --- a/src/crewai/tools/structured_tool.py +++ b/src/crewai/tools/structured_tool.py @@ -10,6 +10,11 @@ from pydantic import BaseModel, Field, create_model from crewai.utilities.logger import Logger +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from crewai.tools.base_tool import BaseTool + class CrewStructuredTool: """A structured tool that can operate on any number of inputs. @@ -18,6 +23,8 @@ class CrewStructuredTool: that integrates better with CrewAI's ecosystem. """ + _original_tool: BaseTool | None = None + def __init__( self, name: str, @@ -47,6 +54,7 @@ class CrewStructuredTool: self.result_as_answer = result_as_answer self.max_usage_count = max_usage_count self.current_usage_count = current_usage_count + self._original_tool = None # Validate the function signature matches the schema self._validate_function_signature() @@ -219,6 +227,8 @@ class CrewStructuredTool: """ parsed_args = self._parse_args(input) + self._increment_usage_count() + if inspect.iscoroutinefunction(self.func): return await self.func(**parsed_args, **kwargs) else: @@ -242,6 +252,8 @@ class CrewStructuredTool: """Main method for tool execution.""" parsed_args = self._parse_args(input) + self._increment_usage_count() + if inspect.iscoroutinefunction(self.func): result = asyncio.run(self.func(**parsed_args, **kwargs)) return result @@ -253,6 +265,12 @@ class CrewStructuredTool: return result + def _increment_usage_count(self) -> None: + """Increment the usage count.""" + self.current_usage_count += 1 + if self._original_tool is not None: + self._original_tool.current_usage_count = self.current_usage_count + @property def args(self) -> dict: """Get the tool's input arguments schema.""" diff --git a/tests/cassettes/test_max_usage_count_is_respected.yaml b/tests/cassettes/test_max_usage_count_is_respected.yaml new file mode 100644 index 000000000..a2ca5a4d6 --- /dev/null +++ b/tests/cassettes/test_max_usage_count_is_respected.yaml @@ -0,0 +1,845 @@ +interactions: +- request: + body: '{"messages": [{"role": "system", "content": "You are Iterating Agent. You + are an agent that iterates a given number of times\nYour personal goal is: Call + the iterating tool 5 times\nYou ONLY have access to the following tools, and + should NEVER make up tools that are not listed here:\n\nTool Name: iterating_tool\nTool + Arguments: {''input_text'': {''description'': None, ''type'': ''str''}}\nTool + Description: A tool that iterates a given number of times\n\nIMPORTANT: Use + the following format in your response:\n\n```\nThought: you should always think + about what to do\nAction: the action to take, only one name of [iterating_tool], + just the name, exactly as it''s written.\nAction Input: the input to the action, + just a simple JSON object, enclosed in curly braces, using \" to wrap keys and + values.\nObservation: the result of the action\n```\n\nOnce all necessary information + is gathered, return the following format:\n\n```\nThought: I now know the final + answer\nFinal Answer: the final answer to the original input question\n```"}, + {"role": "user", "content": "\nCurrent Task: Call the iterating tool 5 times\n\nThis + is the expected criteria for your final answer: A list of the iterations\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, zstd + connection: + - keep-alive + content-length: + - '1452' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.93.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.93.0 + 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 | + H4sIAAAAAAAAAwAAAP//hFPRjtMwEHzPV6z83J5ySVvu8nYCIZ1ACKSCEOSUOs4mMefYlr2Boqr/ + jpy0TQ7uxIsV7exMZmftQwTAZMUyYKLlJDqrlq837zbXn/flx6/fVPIp/fI+3ZbaxVv568ObR7YI + DFP+QEFn1pUwnVVI0ugRFg45YVC9frVeb5I0WacD0JkKVaA1lpYrs0ziZLWMb5bx5kRsjRToWQbf + IwCAw3AGi7rCPcsgXpwrHXrPG2TZpQmAOaNChXHvpSeuiS0mUBhNqAfXu90u19vW9E1LGdyDRqyA + DAiuFFCLIAkdJ6kbIGPGUi2dJyDZ4VWu70SYNpv6itB3rsO9tj1lcMiZDF8F4Z5ylkHO3g4qJ5rR + OTvOLTqse89DQrpXagZwrQ0NjCGchxNyvMShTGOdKf1fVFZLLX1bOOTe6DC6J2PZgB4jgIch9v5J + ksw601kqyDzi8Lvk9nbUY9OiJzRdn0AyxNWsnq4Wz+gVFRKXys8WxwQXLVYTddoy7ytpZkA0m/pf + N89pj5OPG/qv/AQIgZawKqzDSoqnE09tDsM7eKntkvJgmHl0P6XAgiS6sIkKa96r8Yoy/9sTdkUt + dYPOOjne09oWq01Z1zHG4oZFx+gPAAAA//8DAHvlcCKwAwAA + headers: + CF-RAY: + - 971b3f72effa6897-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 19 Aug 2025 17:07:35 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=6OZC5kPO9TEmVSACP2sSOZK9ZEZ5I4T_VUlfmzsoY9Y-1755623255-1.0.1.1-ek3SaNBOhXmCg7K3J7LIsE0aCrnK5YfSumHDT6nc8Df1Zh3bzMLHLDqTUwtqiG8SwxiIFXeGP4.Vt2sx9b3FCkxoyrqNpgrBL5DAffAGHm8; + path=/; expires=Tue, 19-Aug-25 17:37:35 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=R_H7SrOF3QFEWePZfvxzuyKWZAt5ulsNbP28.6DC9wM-1755623255760-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + 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: + - '2564' + openai-project: + - proj_xitITlrFeen7zjNSzML82h9x + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '2751' + x-ratelimit-limit-project-tokens: + - '30000000' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '30000000' + x-ratelimit-remaining-project-tokens: + - '29999674' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '29999674' + x-ratelimit-reset-project-tokens: + - 0s + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_d654df1116aa42ca8ee7d10b4b424303 + status: + code: 200 + message: OK +- request: + body: '{"messages": [{"role": "system", "content": "You are Iterating Agent. You + are an agent that iterates a given number of times\nYour personal goal is: Call + the iterating tool 5 times\nYou ONLY have access to the following tools, and + should NEVER make up tools that are not listed here:\n\nTool Name: iterating_tool\nTool + Arguments: {''input_text'': {''description'': None, ''type'': ''str''}}\nTool + Description: A tool that iterates a given number of times\n\nIMPORTANT: Use + the following format in your response:\n\n```\nThought: you should always think + about what to do\nAction: the action to take, only one name of [iterating_tool], + just the name, exactly as it''s written.\nAction Input: the input to the action, + just a simple JSON object, enclosed in curly braces, using \" to wrap keys and + values.\nObservation: the result of the action\n```\n\nOnce all necessary information + is gathered, return the following format:\n\n```\nThought: I now know the final + answer\nFinal Answer: the final answer to the original input question\n```"}, + {"role": "user", "content": "\nCurrent Task: Call the iterating tool 5 times\n\nThis + is the expected criteria for your final answer: A list of the iterations\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:"}, {"role": "assistant", "content": + "```\nThought: I need to call the iterating tool the first time.\nAction: iterating_tool\nAction + Input: {\"input_text\": \"First iteration\"}\nObservation: Iteration 0: First + iteration"}], "model": "gpt-4o", "stop": ["\nObservation:"]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '1673' + content-type: + - application/json + cookie: + - __cf_bm=6OZC5kPO9TEmVSACP2sSOZK9ZEZ5I4T_VUlfmzsoY9Y-1755623255-1.0.1.1-ek3SaNBOhXmCg7K3J7LIsE0aCrnK5YfSumHDT6nc8Df1Zh3bzMLHLDqTUwtqiG8SwxiIFXeGP4.Vt2sx9b3FCkxoyrqNpgrBL5DAffAGHm8; + _cfuvid=R_H7SrOF3QFEWePZfvxzuyKWZAt5ulsNbP28.6DC9wM-1755623255760-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.93.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.93.0 + 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 | + H4sIAAAAAAAAA4RTTW/bMAy9+1cQOidFlg8v8G1LB6zosbvNhaNItK3NFjWJ7loE+e+D7CR2uw67 + CAIf3xP5SB0TAGG0yECoWrJqXTPfpffp2tWqu93u7m+Xu+3Dl6/m96+X+pB+VmIWGXT4gYovrBtF + rWuQDdkBVh4lY1T98HGzSZer5SbtgZY0NpFWOZ6vab5cLNfzxXa+SM/EmozCIDL4ngAAHPszlmg1 + PosMFrNLpMUQZIUiuyYBCE9NjAgZggksLYvZCCqyjLaver/f5/ZbTV1VcwZ3UMsnhHMXqIFrhNL4 + wGAYvYyNgbQaLEaQwHlS52tMDajIaiCLN7n9pGJ6dmHaqmCi5hKHO+s6zuCYCxNvBeMz5yKDXDwM + KtcXc3GaVu+x7IKM5tmuaSaAtJa4Z/S+PZ6R09Wphirn6RDeUEVprAl14VEGstGVwOREj54SgMd+ + It0rk4Xz1DoumH5i/9xqvRn0xLgDE3R7BplYNtP4avaOXqGRpWnCZKZCSVWjHqnjAshOG5oAyaTr + v6t5T3vofBjRf+VHQCl0jLpwHrVRrzse0zzGL/KvtKvLfcEioH8yCgs26OMkNJaya4btFeElMLZF + aWyF3nkzrHDpinV6KMsFLtRWJKfkDwAAAP//AwAMYddzywMAAA== + headers: + CF-RAY: + - 971b3f84cf146897-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 19 Aug 2025 17:07:38 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + 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: + - '1900' + openai-project: + - proj_xitITlrFeen7zjNSzML82h9x + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '2551' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '30000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '29999629' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_f4181fe581264993ac5c6deba4f1c287 + status: + code: 200 + message: OK +- request: + body: '{"messages": [{"role": "system", "content": "You are Iterating Agent. You + are an agent that iterates a given number of times\nYour personal goal is: Call + the iterating tool 5 times\nYou ONLY have access to the following tools, and + should NEVER make up tools that are not listed here:\n\nTool Name: iterating_tool\nTool + Arguments: {''input_text'': {''description'': None, ''type'': ''str''}}\nTool + Description: A tool that iterates a given number of times\n\nIMPORTANT: Use + the following format in your response:\n\n```\nThought: you should always think + about what to do\nAction: the action to take, only one name of [iterating_tool], + just the name, exactly as it''s written.\nAction Input: the input to the action, + just a simple JSON object, enclosed in curly braces, using \" to wrap keys and + values.\nObservation: the result of the action\n```\n\nOnce all necessary information + is gathered, return the following format:\n\n```\nThought: I now know the final + answer\nFinal Answer: the final answer to the original input question\n```"}, + {"role": "user", "content": "\nCurrent Task: Call the iterating tool 5 times\n\nThis + is the expected criteria for your final answer: A list of the iterations\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:"}, {"role": "assistant", "content": + "```\nThought: I need to call the iterating tool the first time.\nAction: iterating_tool\nAction + Input: {\"input_text\": \"First iteration\"}\nObservation: Iteration 0: First + iteration"}, {"role": "assistant", "content": "```\nThought: I have completed + the first iteration and need to proceed to the second one.\nAction: iterating_tool\nAction + Input: {\"input_text\": \"Second iteration\"}\nObservation: Iteration 0: Second + iteration"}], "model": "gpt-4o", "stop": ["\nObservation:"]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '1922' + content-type: + - application/json + cookie: + - __cf_bm=6OZC5kPO9TEmVSACP2sSOZK9ZEZ5I4T_VUlfmzsoY9Y-1755623255-1.0.1.1-ek3SaNBOhXmCg7K3J7LIsE0aCrnK5YfSumHDT6nc8Df1Zh3bzMLHLDqTUwtqiG8SwxiIFXeGP4.Vt2sx9b3FCkxoyrqNpgrBL5DAffAGHm8; + _cfuvid=R_H7SrOF3QFEWePZfvxzuyKWZAt5ulsNbP28.6DC9wM-1755623255760-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.93.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.93.0 + 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 | + H4sIAAAAAAAAAwAAAP//hFNNb9swDL37VxA6J0XmfCz1bRswNBh2GXrYsBSOItG2ElsUJLrIUOS/ + D7KT2N067CIIfHxP5CP1kgAIo0UGQlWSVePq6afVl9Xq9HAI3z6uj8eHz6os9OGrvt98T9sfYhIZ + tD+g4ivrTlHjamRDtoeVR8kYVd+9Xy5X6TxdrjugIY11pJWOpwuaprN0MZ2tp7PVhViRURhEBj8T + AICX7owlWo0nkcFsco00GIIsUWS3JADhqY4RIUMwgaVlMRlARZbRdlXvdrutfayoLSvOYAOVfEa4 + dIEauEIIqMhqMIxexs5AWg0WI0rgPKnLNeZyZbwGsni3tR9UzM6uRFvmTFRf47CxruUMXrbCxFvO + eOKtyGArHjuR23tbcR4X77Fog4ze2bauR4C0lrhjdLY9XZDzzaiaSudpH/6gisJYE6rcowxkoymB + yYkOPScAT91A2lceC+epcZwzHbF7bn6/6PXEsAIjdH0BmVjWQ3wxTydv6OUaWZo6jEYqlFQV6oE6 + zF+22tAISEZd/13NW9p95/2E/is/AEqhY9S586iNet3xkOYx/pB/pd1c7goWAf2zUZizQR8nobGQ + bd0vrwi/AmOTF8aW6J03/QYXLl+s9kUxw5lai+Sc/AYAAP//AwDpY7tZygMAAA== + headers: + CF-RAY: + - 971b3f958e746897-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 19 Aug 2025 17:07:39 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + 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: + - '890' + openai-project: + - proj_xitITlrFeen7zjNSzML82h9x + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '906' + x-ratelimit-limit-project-tokens: + - '30000000' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '30000000' + x-ratelimit-remaining-project-tokens: + - '29999577' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '29999577' + x-ratelimit-reset-project-tokens: + - 0s + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_b3632e88268747218e4cc4cc08d87bca + status: + code: 200 + message: OK +- request: + body: '{"messages": [{"role": "system", "content": "You are Iterating Agent. You + are an agent that iterates a given number of times\nYour personal goal is: Call + the iterating tool 5 times\nYou ONLY have access to the following tools, and + should NEVER make up tools that are not listed here:\n\nTool Name: iterating_tool\nTool + Arguments: {''input_text'': {''description'': None, ''type'': ''str''}}\nTool + Description: A tool that iterates a given number of times\n\nIMPORTANT: Use + the following format in your response:\n\n```\nThought: you should always think + about what to do\nAction: the action to take, only one name of [iterating_tool], + just the name, exactly as it''s written.\nAction Input: the input to the action, + just a simple JSON object, enclosed in curly braces, using \" to wrap keys and + values.\nObservation: the result of the action\n```\n\nOnce all necessary information + is gathered, return the following format:\n\n```\nThought: I now know the final + answer\nFinal Answer: the final answer to the original input question\n```"}, + {"role": "user", "content": "\nCurrent Task: Call the iterating tool 5 times\n\nThis + is the expected criteria for your final answer: A list of the iterations\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:"}, {"role": "assistant", "content": + "```\nThought: I need to call the iterating tool the first time.\nAction: iterating_tool\nAction + Input: {\"input_text\": \"First iteration\"}\nObservation: Iteration 0: First + iteration"}, {"role": "assistant", "content": "```\nThought: I have completed + the first iteration and need to proceed to the second one.\nAction: iterating_tool\nAction + Input: {\"input_text\": \"Second iteration\"}\nObservation: Iteration 0: Second + iteration"}, {"role": "assistant", "content": "```\nThought: I have completed + the second iteration and need to proceed to the third one.\nAction: iterating_tool\nAction + Input: {\"input_text\": \"Third iteration\"}\nObservation: Iteration 0: Third + iteration\n\n\nYou ONLY have access to the following tools, and should NEVER + make up tools that are not listed here:\n\nTool Name: iterating_tool\nTool Arguments: + {''input_text'': {''description'': None, ''type'': ''str''}}\nTool Description: + A tool that iterates a given number of times\n\nIMPORTANT: Use the following + format in your response:\n\n```\nThought: you should always think about what + to do\nAction: the action to take, only one name of [iterating_tool], just the + name, exactly as it''s written.\nAction Input: the input to the action, just + a simple JSON object, enclosed in curly braces, using \" to wrap keys and values.\nObservation: + the result of the action\n```\n\nOnce all necessary information is gathered, + return the following format:\n\n```\nThought: I now know the final answer\nFinal + Answer: the final answer to the original input question\n```"}], "model": "gpt-4o", + "stop": ["\nObservation:"]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '3018' + content-type: + - application/json + cookie: + - __cf_bm=6OZC5kPO9TEmVSACP2sSOZK9ZEZ5I4T_VUlfmzsoY9Y-1755623255-1.0.1.1-ek3SaNBOhXmCg7K3J7LIsE0aCrnK5YfSumHDT6nc8Df1Zh3bzMLHLDqTUwtqiG8SwxiIFXeGP4.Vt2sx9b3FCkxoyrqNpgrBL5DAffAGHm8; + _cfuvid=R_H7SrOF3QFEWePZfvxzuyKWZAt5ulsNbP28.6DC9wM-1755623255760-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.93.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.93.0 + 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 | + H4sIAAAAAAAAAwAAAP//hFPBbtswDL37Kwidk8JzErfxbdgwtNh1GwrMhaPItK3MFgWJLloE+fdB + dhK7W4ddBIGP74l8pI4RgNClyECoRrLqbLv8lH5Nb8k9Hu7T5PP37f361R1Wj1us6IeVYhEYtD+g + 4gvrRlFnW2RNZoSVQ8kYVD/cbjZpsko22wHoqMQ20GrLyzUtkzhZL+O7ZZyeiQ1phV5k8DMCADgO + ZyjRlPgiMogXl0iH3ssaRXZNAhCO2hAR0nvtWRoWiwlUZBjNUPVut8vNt4b6uuEMHqCRzwjnLrAE + bhC40a4EzehkaAykKcFgAAmsI3W+htSKescNkMGb3HxUIT27ME1dMFF7icODsT1ncMyFDreC8YVz + kUEuvowq1xdzcZpX77DqvQzmmb5tZ4A0hnhgDL49nZHT1amWauto7/+gikob7ZvCofRkgiueyYoB + PUUAT8NE+jcmC+uos1ww/cLhuTRJRz0x7cCEru7OIBPLdsZK14t39IoSWerWz2YqlFQNlhN1WgDZ + l5pmQDTr+u9q3tMeOx9H9F/5CVAKLWNZWIelVm87ntIchi/yr7Sry0PBwqN71goL1ujCJEqsZN+O + 2yv8q2fsikqbGp11elzhyhbrdF9VMcbqTkSn6DcAAAD//wMACjMtRssDAAA= + headers: + CF-RAY: + - 971b3f9bbef86897-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 19 Aug 2025 17:07:40 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + 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: + - '1182' + openai-project: + - proj_xitITlrFeen7zjNSzML82h9x + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '1208' + x-ratelimit-limit-project-tokens: + - '30000000' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '30000000' + x-ratelimit-remaining-project-tokens: + - '29999320' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '29999320' + x-ratelimit-reset-project-tokens: + - 1ms + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 1ms + x-request-id: + - req_7fc641fabc634f29ae085ef176071402 + status: + code: 200 + message: OK +- request: + body: '{"messages": [{"role": "system", "content": "You are Iterating Agent. You + are an agent that iterates a given number of times\nYour personal goal is: Call + the iterating tool 5 times\nYou ONLY have access to the following tools, and + should NEVER make up tools that are not listed here:\n\nTool Name: iterating_tool\nTool + Arguments: {''input_text'': {''description'': None, ''type'': ''str''}}\nTool + Description: A tool that iterates a given number of times\n\nIMPORTANT: Use + the following format in your response:\n\n```\nThought: you should always think + about what to do\nAction: the action to take, only one name of [iterating_tool], + just the name, exactly as it''s written.\nAction Input: the input to the action, + just a simple JSON object, enclosed in curly braces, using \" to wrap keys and + values.\nObservation: the result of the action\n```\n\nOnce all necessary information + is gathered, return the following format:\n\n```\nThought: I now know the final + answer\nFinal Answer: the final answer to the original input question\n```"}, + {"role": "user", "content": "\nCurrent Task: Call the iterating tool 5 times\n\nThis + is the expected criteria for your final answer: A list of the iterations\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:"}, {"role": "assistant", "content": + "```\nThought: I need to call the iterating tool the first time.\nAction: iterating_tool\nAction + Input: {\"input_text\": \"First iteration\"}\nObservation: Iteration 0: First + iteration"}, {"role": "assistant", "content": "```\nThought: I have completed + the first iteration and need to proceed to the second one.\nAction: iterating_tool\nAction + Input: {\"input_text\": \"Second iteration\"}\nObservation: Iteration 0: Second + iteration"}, {"role": "assistant", "content": "```\nThought: I have completed + the second iteration and need to proceed to the third one.\nAction: iterating_tool\nAction + Input: {\"input_text\": \"Third iteration\"}\nObservation: Iteration 0: Third + iteration\n\n\nYou ONLY have access to the following tools, and should NEVER + make up tools that are not listed here:\n\nTool Name: iterating_tool\nTool Arguments: + {''input_text'': {''description'': None, ''type'': ''str''}}\nTool Description: + A tool that iterates a given number of times\n\nIMPORTANT: Use the following + format in your response:\n\n```\nThought: you should always think about what + to do\nAction: the action to take, only one name of [iterating_tool], just the + name, exactly as it''s written.\nAction Input: the input to the action, just + a simple JSON object, enclosed in curly braces, using \" to wrap keys and values.\nObservation: + the result of the action\n```\n\nOnce all necessary information is gathered, + return the following format:\n\n```\nThought: I now know the final answer\nFinal + Answer: the final answer to the original input question\n```"}, {"role": "assistant", + "content": "```\nThought: I have completed the third iteration and need to proceed + to the fourth one.\nAction: iterating_tool\nAction Input: {\"input_text\": \"Fourth + iteration\"}\nObservation: Iteration 0: Fourth iteration"}], "model": "gpt-4o", + "stop": ["\nObservation:"]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '3267' + content-type: + - application/json + cookie: + - __cf_bm=6OZC5kPO9TEmVSACP2sSOZK9ZEZ5I4T_VUlfmzsoY9Y-1755623255-1.0.1.1-ek3SaNBOhXmCg7K3J7LIsE0aCrnK5YfSumHDT6nc8Df1Zh3bzMLHLDqTUwtqiG8SwxiIFXeGP4.Vt2sx9b3FCkxoyrqNpgrBL5DAffAGHm8; + _cfuvid=R_H7SrOF3QFEWePZfvxzuyKWZAt5ulsNbP28.6DC9wM-1755623255760-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.93.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.93.0 + 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 | + H4sIAAAAAAAAA4RTTW/bMAy9+1cQOieFmyZO5ts2bECxDwztgB3mwlFk2tZmi4JEFx2C/PdBchKn + W4ddBIGP74l8pPYJgNCVyEGoVrLqbTd/m33INvJ+0NnnT/ff3tTZR72483eSv7xLScwCg3Y/UPGJ + daWotx2yJjPCyqFkDKrX69UqW9wssjQCPVXYBVpjeb6k+SJdLOfpZp5mR2JLWqEXOXxPAAD28Qwl + mgqfRA5RJkZ69F42KPJzEoBw1IWIkN5rz9KwmE2gIsNoYtXb7bYwX1sampZzuIVWPiIcu8AKuEWo + aXDcgmZ0MnQG0lRgMKAE1pE6XmOurrkFMnhVmNcqZOcnomlKJupOcbg1duAc9oXQ4VYyPnEhcijE + +yhyfq8Qh8viHdaDl8E7M3TdBSCNIY6MaNvDETmcjeqosY52/g+qqLXRvi0dSk8mmOKZrIjoIQF4 + iAMZnnksrKPecsn0E+Nz2Xo16olpBSb05tURZGLZTfH19XL2gl5ZIUvd+YuRCiVVi9VEneYvh0rT + BZBcdP13NS9pj52PE/qv/AQohZaxKq3DSqvnHU9pDsMP+Vfa2eVYsPDoHrXCkjW6MIkKazl04/IK + /8sz9mWtTYPOOj1ucG3LZbar6xRTtRHJIfkNAAD//wMANy12ZMoDAAA= + headers: + CF-RAY: + - 971b3fa3ea2f6897-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 19 Aug 2025 17:07:41 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + 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: + - '780' + openai-project: + - proj_xitITlrFeen7zjNSzML82h9x + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '878' + x-ratelimit-limit-project-tokens: + - '30000000' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '30000000' + x-ratelimit-remaining-project-tokens: + - '29999268' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '29999268' + x-ratelimit-reset-project-tokens: + - 1ms + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 1ms + x-request-id: + - req_603a6c645bac468888838d21c64db11f + status: + code: 200 + message: OK +- request: + body: '{"messages": [{"role": "system", "content": "You are Iterating Agent. You + are an agent that iterates a given number of times\nYour personal goal is: Call + the iterating tool 5 times\nYou ONLY have access to the following tools, and + should NEVER make up tools that are not listed here:\n\nTool Name: iterating_tool\nTool + Arguments: {''input_text'': {''description'': None, ''type'': ''str''}}\nTool + Description: A tool that iterates a given number of times\n\nIMPORTANT: Use + the following format in your response:\n\n```\nThought: you should always think + about what to do\nAction: the action to take, only one name of [iterating_tool], + just the name, exactly as it''s written.\nAction Input: the input to the action, + just a simple JSON object, enclosed in curly braces, using \" to wrap keys and + values.\nObservation: the result of the action\n```\n\nOnce all necessary information + is gathered, return the following format:\n\n```\nThought: I now know the final + answer\nFinal Answer: the final answer to the original input question\n```"}, + {"role": "user", "content": "\nCurrent Task: Call the iterating tool 5 times\n\nThis + is the expected criteria for your final answer: A list of the iterations\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:"}, {"role": "assistant", "content": + "```\nThought: I need to call the iterating tool the first time.\nAction: iterating_tool\nAction + Input: {\"input_text\": \"First iteration\"}\nObservation: Iteration 0: First + iteration"}, {"role": "assistant", "content": "```\nThought: I have completed + the first iteration and need to proceed to the second one.\nAction: iterating_tool\nAction + Input: {\"input_text\": \"Second iteration\"}\nObservation: Iteration 0: Second + iteration"}, {"role": "assistant", "content": "```\nThought: I have completed + the second iteration and need to proceed to the third one.\nAction: iterating_tool\nAction + Input: {\"input_text\": \"Third iteration\"}\nObservation: Iteration 0: Third + iteration\n\n\nYou ONLY have access to the following tools, and should NEVER + make up tools that are not listed here:\n\nTool Name: iterating_tool\nTool Arguments: + {''input_text'': {''description'': None, ''type'': ''str''}}\nTool Description: + A tool that iterates a given number of times\n\nIMPORTANT: Use the following + format in your response:\n\n```\nThought: you should always think about what + to do\nAction: the action to take, only one name of [iterating_tool], just the + name, exactly as it''s written.\nAction Input: the input to the action, just + a simple JSON object, enclosed in curly braces, using \" to wrap keys and values.\nObservation: + the result of the action\n```\n\nOnce all necessary information is gathered, + return the following format:\n\n```\nThought: I now know the final answer\nFinal + Answer: the final answer to the original input question\n```"}, {"role": "assistant", + "content": "```\nThought: I have completed the third iteration and need to proceed + to the fourth one.\nAction: iterating_tool\nAction Input: {\"input_text\": \"Fourth + iteration\"}\nObservation: Iteration 0: Fourth iteration"}, {"role": "assistant", + "content": "```\nThought: I have completed the fourth iteration and need to + proceed to the fifth one.\nAction: iterating_tool\nAction Input: {\"input_text\": + \"Fifth iteration\"}\nObservation: Iteration 0: Fifth iteration"}], "model": + "gpt-4o", "stop": ["\nObservation:"]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '3514' + content-type: + - application/json + cookie: + - __cf_bm=6OZC5kPO9TEmVSACP2sSOZK9ZEZ5I4T_VUlfmzsoY9Y-1755623255-1.0.1.1-ek3SaNBOhXmCg7K3J7LIsE0aCrnK5YfSumHDT6nc8Df1Zh3bzMLHLDqTUwtqiG8SwxiIFXeGP4.Vt2sx9b3FCkxoyrqNpgrBL5DAffAGHm8; + _cfuvid=R_H7SrOF3QFEWePZfvxzuyKWZAt5ulsNbP28.6DC9wM-1755623255760-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.93.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.93.0 + 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 | + H4sIAAAAAAAAA4xTy26jMBTd8xVXXpOK0IRE7KqRInVazaZZTROBY1/AU2NT26QzqvLvI0MSSNuJ + ZsPC58E99/EeABDBSQqEVdSxupGTb8lDcne7pt8Xb/r14XX5tJiufiT1z8f944smoVfo3S9k7qS6 + YbpuJDqhVQ8zg9Shd50u5vMkvo2TuANqzVF6Wdm4yUxP4iieTaLlJEqOwkoLhpak8BwAALx3X1+i + 4vibpBCFp5caraUlkvRMAiBGS/9CqLXCOqocCQeQaeVQdVXneb5R60q3ZeVSuIeK7hGOKZADlRLm + IBwa6kPZm41aCUUl3Cn7hiaF5w25P6EQpbASxrpBsCEhfGA8IdOKX6WsK2GuM1a6Na66ThHFJWO7 + UXmej/tgsGgt9WNQrZQjgCqlXZ/YT2B7RA7nnktdNkbv7AcpKYQStsoMUquV7691uiEdeggAtt1s + 24txkcbounGZ0y/Y/W4Rz3s/MmzTgM6TI+i0o3KkWk7DL/wyjo4KaUfbQRhlFfJBOqwSbbnQIyAY + pf5czVfefXKhyv+xHwDGsHHIs8YgF+wy8UAz6I/tX7Rzl7uCiUWzFwwzJ9D4SXAsaCv7OyD2j3VY + Z4VQJZrGiP4YiiabJbuiiDBiSxIcgr8AAAD//wMAnPTcwxUEAAA= + headers: + CF-RAY: + - 971b3faa6ba46897-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 19 Aug 2025 17:07:43 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + 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: + - '1392' + openai-project: + - proj_xitITlrFeen7zjNSzML82h9x + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '1841' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '30000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '29999217' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 1ms + x-request-id: + - req_19dc255cec9d4763be7d5f597c80e936 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/tools/test_base_tool.py b/tests/tools/test_base_tool.py index b4f3d2488..0c11e1e09 100644 --- a/tests/tools/test_base_tool.py +++ b/tests/tools/test_base_tool.py @@ -1,9 +1,12 @@ import asyncio -import inspect -import unittest -from typing import Any, Callable, Dict, List +from typing import Callable from unittest.mock import patch +import pytest + +from crewai.agent import Agent +from crewai.crew import Crew +from crewai.task import Task from crewai.tools import BaseTool, tool @@ -111,25 +114,26 @@ def test_result_as_answer_in_tool_decorator(): def my_tool_with_result_as_answer(question: str) -> str: """This tool will return its result as the final answer.""" return question - + assert my_tool_with_result_as_answer.result_as_answer is True - + converted_tool = my_tool_with_result_as_answer.to_structured_tool() assert converted_tool.result_as_answer is True - + @tool("Tool with default result_as_answer") def my_tool_with_default(question: str) -> str: """This tool uses the default result_as_answer value.""" return question - + assert my_tool_with_default.result_as_answer is False - + converted_tool = my_tool_with_default.to_structured_tool() assert converted_tool.result_as_answer is False class SyncTool(BaseTool): """Test implementation with a synchronous _run method""" + name: str = "sync_tool" description: str = "A synchronous tool for testing" @@ -140,6 +144,7 @@ class SyncTool(BaseTool): class AsyncTool(BaseTool): """Test implementation with an asynchronous _run method""" + name: str = "async_tool" description: str = "An asynchronous tool for testing" @@ -174,7 +179,7 @@ def test_run_calls_asyncio_run_for_async_tools(): """Test that asyncio.run is called when using async tools.""" async_tool = AsyncTool() - with patch('asyncio.run') as mock_run: + with patch("asyncio.run") as mock_run: mock_run.return_value = "Processed test asynchronously" async_result = async_tool.run(input_text="test") @@ -186,9 +191,43 @@ def test_run_does_not_call_asyncio_run_for_sync_tools(): """Test that asyncio.run is NOT called when using sync tools.""" sync_tool = SyncTool() - with patch('asyncio.run') as mock_run: + with patch("asyncio.run") as mock_run: sync_result = sync_tool.run(input_text="test") mock_run.assert_not_called() assert sync_result == "Processed test synchronously" + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_max_usage_count_is_respected(): + class IteratingTool(BaseTool): + name: str = "iterating_tool" + description: str = "A tool that iterates a given number of times" + + def _run(self, input_text: str): + return f"Iteration {input_text}" + + tool = IteratingTool(max_usage_count=5) + + agent = Agent( + role="Iterating Agent", + goal="Call the iterating tool 5 times", + backstory="You are an agent that iterates a given number of times", + tools=[tool], + ) + + task = Task( + description="Call the iterating tool 5 times", + expected_output="A list of the iterations", + agent=agent, + ) + + crew = Crew( + agents=[agent], + tasks=[task], + verbose=True, + ) + + crew.kickoff() + assert tool.max_usage_count == 5 + assert tool.current_usage_count == 5