test_base_tool.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. """Unit tests for BaseTool functionality."""
  2. import pytest
  3. from agents import RunContextWrapper
  4. from pydantic import BaseModel
  5. from agency_swarm.context import MasterContext
  6. from agency_swarm.tools.base_tool import BaseTool, classproperty
  7. class SampleTool(BaseTool):
  8. name: str = "sample"
  9. def run(self) -> str:
  10. return "ok"
  11. def test_classproperty_descriptor_supports_plain_and_basetool_access() -> None:
  12. """classproperty should resolve for plain classes and BaseTool subclasses."""
  13. class Demo:
  14. _value = "value"
  15. @classproperty
  16. def prop(cls):
  17. return cls._value
  18. assert Demo.prop == "value"
  19. assert Demo().prop == "value"
  20. class ToolWithClassProp(BaseTool):
  21. name: str = "name"
  22. @classproperty
  23. def computed(cls):
  24. return "computed-value"
  25. def run(self) -> str:
  26. return self.name
  27. tool = ToolWithClassProp()
  28. assert tool.name == "name"
  29. assert tool.computed == "computed-value"
  30. def test_base_tool_requires_run_implementation() -> None:
  31. """BaseTool and incomplete subclasses should remain abstract."""
  32. with pytest.raises(TypeError, match="Can't instantiate abstract class BaseTool"):
  33. BaseTool()
  34. with pytest.raises(TypeError, match="Can't instantiate abstract class.*run"):
  35. class IncompleteTool(BaseTool):
  36. pass
  37. IncompleteTool()
  38. def test_base_tool_initialization_and_tool_config_defaults() -> None:
  39. """Concrete tools should initialize context wrapper and preserve ToolConfig defaults/overrides."""
  40. tool = SampleTool()
  41. assert tool.name == "sample"
  42. assert isinstance(tool._context, RunContextWrapper)
  43. assert isinstance(tool._context.context, MasterContext)
  44. assert tool.ToolConfig.strict is False
  45. assert tool.ToolConfig.one_call_at_a_time is False
  46. class StrictSequentialTool(BaseTool):
  47. class ToolConfig:
  48. strict = True
  49. one_call_at_a_time = True
  50. custom_flag = "kept"
  51. def run(self) -> str:
  52. return "ok"
  53. tool = StrictSequentialTool()
  54. assert tool.ToolConfig.strict is True
  55. assert tool.ToolConfig.one_call_at_a_time is True
  56. assert tool.ToolConfig.custom_flag == "kept"
  57. def test_openai_schema_descriptions_required_fields_and_fallback() -> None:
  58. """OpenAI schema should include docstring details and apply fallback descriptions when absent."""
  59. class DocumentedTool(BaseTool):
  60. """A tool for schema checks.
  61. Args:
  62. name: Name field description
  63. count: Count field description
  64. """
  65. name: str
  66. count: int = 5
  67. def run(self) -> str:
  68. return f"{self.name}:{self.count}"
  69. schema = DocumentedTool.openai_schema
  70. params = schema["parameters"]
  71. assert schema["name"] == "DocumentedTool"
  72. assert "A tool for schema checks." in schema["description"]
  73. assert params["properties"]["name"]["description"] == "Name field description"
  74. assert params["properties"]["count"]["description"] == "Count field description"
  75. assert "name" in params["required"]
  76. assert "count" not in params["required"]
  77. class UndocumentedTool(BaseTool):
  78. def run(self) -> str:
  79. return "ok"
  80. schema = UndocumentedTool.openai_schema
  81. assert schema["description"] == "`UndocumentedTool` tool"
  82. def test_openai_schema_strict_mode_applies_additional_properties_to_main_and_defs() -> None:
  83. """Strict mode should set additionalProperties=False on root and nested definitions."""
  84. class NestedModel(BaseModel):
  85. nested_field: str
  86. optional_field: int | None = None
  87. class StrictTool(BaseTool):
  88. class ToolConfig:
  89. strict = True
  90. name: str
  91. payload: list[NestedModel]
  92. def run(self) -> str:
  93. return "ok"
  94. schema = StrictTool.openai_schema
  95. assert schema["strict"] is True
  96. assert schema["parameters"]["additionalProperties"] is False
  97. defs = schema["parameters"].get("$defs", {})
  98. for definition in defs.values():
  99. assert definition["additionalProperties"] is False
  100. def test_base_tool_context_property_and_removed_shared_state() -> None:
  101. """context property should proxy MasterContext and _shared_state should be unavailable."""
  102. tool = SampleTool()
  103. assert isinstance(tool.context, MasterContext)
  104. tool._context = None
  105. assert tool.context is None
  106. with pytest.raises(AttributeError, match=r"_shared_state"):
  107. _ = tool._shared_state