diff --git a/CHANGELOG.md b/CHANGELOG.md index d38286dcc..34fc5a7c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated `ToolkitTask` system prompt to no longer mention `memory_name` and `artifact_namespace`. - Models in `ToolkitTask` with native tool calling no longer need to provide their final answer as `Answer:`. - `EventListener.event_types` will now listen on child types of any provided type. +- Only install Tool dependencies if the Tool provides a `requirements.txt` and the dependencies are not already met. ### Fixed diff --git a/MIGRATION.md b/MIGRATION.md index 74c83c347..f52435f7e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -35,7 +35,6 @@ Defaults.drivers_config = AnthropicDriversConfig( ) ``` - ## 0.33.X to 0.34.X ### Removed `CompletionChunkEvent` diff --git a/docs/griptape-tools/official-tools/prompt-summary-tool.md b/docs/griptape-tools/official-tools/prompt-summary-tool.md index e2b4c49ec..23c35c367 100644 --- a/docs/griptape-tools/official-tools/prompt-summary-tool.md +++ b/docs/griptape-tools/official-tools/prompt-summary-tool.md @@ -4,7 +4,7 @@ The [PromptSummaryTool](../../reference/griptape/tools/prompt_summary/tool.md) e --8<-- "docs/griptape-tools/official-tools/src/prompt_summary_tool_1.py" ``` -``` +```` [08/12/24 15:54:46] INFO ToolkitTask 8be73eb542c44418ba880399044c017a Input: How can I build Neovim from source for MacOS according to this https://github.com/neovim/neovim/blob/master/BUILD.md [08/12/24 15:54:47] INFO Subtask cd362a149e1d400997be93c1342d1663 @@ -103,4 +103,4 @@ The [PromptSummaryTool](../../reference/griptape/tools/prompt_summary/tool.md) e By following these steps, you should be able to build and install Neovim from source on macOS. For more detailed instructions and troubleshooting tips, refer to the [BUILD.md](https://github.com/neovim/neovim/blob/master/BUILD.md) file in the Neovim repository. -``` +```` diff --git a/griptape/tools/base_tool.py b/griptape/tools/base_tool.py index 7efa9f77f..8601c62ee 100644 --- a/griptape/tools/base_tool.py +++ b/griptape/tools/base_tool.py @@ -7,8 +7,10 @@ import subprocess import sys from abc import ABC +from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Optional +import pkg_resources import schema from attrs import Attribute, Factory, define, field from schema import Literal, Or, Schema @@ -55,7 +57,11 @@ class BaseTool(ActivityMixin, SerializableMixin, ABC): off_prompt: bool = field(default=False, kw_only=True, metadata={"serializable": True}) def __attrs_post_init__(self) -> None: - if self.install_dependencies_on_init: + if ( + self.install_dependencies_on_init + and self.has_requirements + and not self.are_requirements_met(self.requirements_path) + ): self.install_dependencies(os.environ.copy()) @output_memory.validator # pyright: ignore[reportAttributeAccessIssue] @@ -84,6 +90,10 @@ def abs_file_path(self) -> str: def abs_dir_path(self) -> str: return os.path.dirname(self.abs_file_path) + @property + def has_requirements(self) -> bool: + return os.path.exists(self.requirements_path) + # This method has to remain a method and can't be decorated with @property because # of the max depth recursion issue in `self.activities`. def schema(self) -> dict: @@ -223,3 +233,13 @@ def to_native_tool_name(self, activity: Callable) -> str: raise ValueError("Activity name can only contain letters, numbers, and underscores.") return f"{tool_name}_{activity_name}" + + def are_requirements_met(self, requirements_path: str) -> bool: + requirements = Path(requirements_path).read_text().splitlines() + + try: + pkg_resources.require(requirements) + + return True + except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict): + return False diff --git a/tests/unit/tools/test_base_tool.py b/tests/unit/tools/test_base_tool.py index 5ac3849d5..94a4f1442 100644 --- a/tests/unit/tools/test_base_tool.py +++ b/tests/unit/tools/test_base_tool.py @@ -1,5 +1,6 @@ import inspect import os +import tempfile import pytest from schema import Or, Schema, SchemaMissingKeyError @@ -315,3 +316,31 @@ def test_method_kwargs_var_injection(self, tool): params = {"values": {"test_kwarg": "foo", "test_kwarg_kwargs": "bar"}} assert tool.test_with_kwargs(params) == "ack foo" + + def test_has_requirements(self, tool): + assert tool.has_requirements + + class InlineTool(BaseTool): + pass + + assert InlineTool().has_requirements is False + + def test_are_requirements_met(self, tool): + assert tool.are_requirements_met(tool.requirements_path) + + class InlineTool(BaseTool): + pass + + # Temp file does not work on Github Actions Windows runner. + if os.name != "nt": + with tempfile.NamedTemporaryFile() as temp: + temp.write(b"nonexistent-package==1.0.0\nanother-package==2.0.0") + temp.seek(0) + + assert InlineTool().are_requirements_met(temp.name) is False + + with tempfile.NamedTemporaryFile() as temp: + temp.write(b"pip") + temp.seek(0) + + assert InlineTool().are_requirements_met(temp.name) is True