Support to collect extra package dependencies of Tools (#330)

* feat: add explictly package_dependencies in the Tools

* feat: collect package_dependencies from Tool to add in tool.specs.json

* feat: add default value in run_params Tool' specs

* fix: support get boolean values

This commit also refactor test to make easier define newest attributes into a Tool
This commit is contained in:
Lucas Gomide
2025-06-16 11:09:19 -03:00
committed by GitHub
parent 5a99f07765
commit fac32d9503
29 changed files with 129 additions and 127 deletions

View File

@@ -1,12 +1,12 @@
import json
from typing import List, Optional
from typing import List, Optional, Type
import pytest
from pydantic import BaseModel, Field
from unittest import mock
from generate_tool_specs import ToolSpecExtractor
from crewai.tools.base_tool import EnvVar
from crewai.tools.base_tool import BaseTool, EnvVar
class MockToolSchema(BaseModel):
query: str = Field(..., description="The query parameter")
@@ -14,54 +14,26 @@ class MockToolSchema(BaseModel):
filters: Optional[List[str]] = Field(None, description="Optional filters to apply")
class MockTool:
name = "Mock Search Tool"
description = "A tool that mocks search functionality"
args_schema = MockToolSchema
class MockTool(BaseTool):
name: str = "Mock Search Tool"
description: str = "A tool that mocks search functionality"
args_schema: Type[BaseModel] = MockToolSchema
another_parameter: str = Field("Another way to define a default value", description="")
my_parameter: str = Field("This is default value", description="What a description")
my_parameter_bool: bool = Field(False)
package_dependencies: List[str] = Field(["this-is-a-required-package", "another-required-package"], description="")
env_vars: List[EnvVar] = [
EnvVar(name="SERPER_API_KEY", description="API key for Serper", required=True, default=None),
EnvVar(name="API_RATE_LIMIT", description="API rate limit", required=False, default="100")
]
@pytest.fixture
def extractor():
ext = ToolSpecExtractor()
MockTool.__pydantic_core_schema__ = create_mock_schema(MockTool)
MockTool.args_schema.__pydantic_core_schema__ = create_mock_schema_args(MockTool.args_schema)
return ext
def create_mock_schema(cls):
return {
"type": "model",
"cls": cls,
"schema": {
"type": "model-fields",
"fields": {
"name": {"type": "model-field", "schema": {"type": "default", "schema": {"type": "str"}, "default": cls.name}, "metadata": {}},
"description": {"type": "model-field", "schema": {"type": "default", "schema": {"type": "str"}, "default": cls.description}, "metadata": {}},
"args_schema": {"type": "model-field", "schema": {"type": "default", "schema": {"type": "is-subclass", "cls": BaseModel}, "default": cls.args_schema}, "metadata": {}},
"env_vars": {
"type": "model-field", "schema": {"type": "default", "schema": {"type": "list", "items_schema": {"type": "model", "cls": "INSPECT CLASS", "schema": {"type": "model-fields", "fields": {"name": {"type": "model-field", "schema": {"type": "str"}, "metadata": {}}, "description": {"type": "model-field", "schema": {"type": "str"}, "metadata": {}}, "required": {"type": "model-field", "schema": {"type": "default", "schema": {"type": "bool"}, "default": True}, "metadata": {}}, "default": {"type": "model-field", "schema": {"type": "default", "schema": {"type": "nullable", "schema": {"type": "str"}}, "default": None}, "metadata": {}},}, "model_name": "EnvVar", "computed_fields": []}, "custom_init": False, "root_model": False, "config": {"title": "EnvVar"}, "ref": "crewai.tools.base_tool.EnvVar:4593650640", "metadata": {"pydantic_js_functions": ["INSPECT __get_pydantic_json_schema__"]}}}, "default": [EnvVar(name='SERPER_API_KEY', description='API key for Serper', required=True, default=None), EnvVar(name='API_RATE_LIMIT', description='API rate limit', required=False, default="100")]}, "metadata": {}
}
},
"model_name": cls.__name__
}
}
def create_mock_schema_args(cls):
return {
"type": "model",
"cls": cls,
"schema": {
"type": "model-fields",
"fields": {
"query": {"type": "model-field", "schema": {"type": "default", "schema": {"type": "str"}, "default": "The query parameter"}},
"count": {"type": "model-field", "schema": {"type": "default", "schema": {"type": "int"}, "default": 5}, "metadata": {"pydantic_js_updates": {"description": "Number of results to return"}}},
"filters": {"type": "model-field", "schema": {"type": "nullable", "schema": {"type": "list", "items_schema": {"type": "str"}}}}
},
"model_name": cls.__name__
}
}
def test_unwrap_schema(extractor):
nested_schema = {
"type": "function-after",
@@ -72,19 +44,6 @@ def test_unwrap_schema(extractor):
assert result["value"] == "test"
@pytest.mark.parametrize(
"field, fallback, expected",
[
({"schema": {"default": "test_value"}}, None, "test_value"),
({}, "fallback_value", "fallback_value"),
({"schema": {"default": 123}}, "fallback_value", "fallback_value")
]
)
def test_extract_field_default(extractor, field, fallback, expected):
result = extractor._extract_field_default(field, fallback=fallback)
assert result == expected
@pytest.mark.parametrize(
"schema, expected",
[
@@ -112,7 +71,7 @@ def test_extract_param_type(extractor, info, expected_type):
assert extractor._extract_param_type(info) == expected_type
def test_extract_tool_info(extractor):
def test_extract_all_tools(extractor):
with mock.patch("generate_tool_specs.dir", return_value=["MockTool"]), \
mock.patch("generate_tool_specs.getattr", return_value=MockTool):
extractor.extract_all_tools()
@@ -120,6 +79,16 @@ def test_extract_tool_info(extractor):
assert len(extractor.tools_spec) == 1
tool_info = extractor.tools_spec[0]
assert tool_info.keys() == {
"name",
"humanized_name",
"description",
"run_params",
"env_vars",
"init_params",
"package_dependencies",
}
assert tool_info["name"] == "MockTool"
assert tool_info["humanized_name"] == "Mock Search Tool"
assert tool_info["description"] == "A tool that mocks search functionality"
@@ -142,12 +111,16 @@ def test_extract_tool_info(extractor):
params = {p["name"]: p for p in tool_info["run_params"]}
assert params["query"]["description"] == "The query parameter"
assert params["query"]["type"] == "str"
assert params["query"]["default"] == ""
assert params["count"]["description"] == "Number of results to return"
assert params["count"]["type"] == "int"
assert params["count"]["default"] == 5
assert params["filters"]["description"] == ""
assert params["filters"]["description"] == "Optional filters to apply"
assert params["filters"]["type"] == "list[str]"
assert params["filters"]["default"] == ""
assert tool_info["package_dependencies"] == ["this-is-a-required-package", "another-required-package"]
def test_save_to_json(extractor, tmp_path):