diff --git a/src/crewai_tools/tools/__init__.py b/src/crewai_tools/tools/__init__.py index df0ec7286..f5ac94052 100644 --- a/src/crewai_tools/tools/__init__.py +++ b/src/crewai_tools/tools/__init__.py @@ -1,5 +1,6 @@ from .browserbase_load_tool.browserbase_load_tool import BrowserbaseLoadTool from .code_docs_search_tool.code_docs_search_tool import CodeDocsSearchTool +from .code_interpreter_tool.code_interpreter_tool import CodeInterpreterTool from .composio_tool.composio_tool import ComposioTool from .csv_search_tool.csv_search_tool import CSVSearchTool from .directory_read_tool.directory_read_tool import DirectoryReadTool diff --git a/src/crewai_tools/tools/code_interpreter_tool/Dockerfile b/src/crewai_tools/tools/code_interpreter_tool/Dockerfile new file mode 100644 index 000000000..ae9b2ffd6 --- /dev/null +++ b/src/crewai_tools/tools/code_interpreter_tool/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.11-slim + +# Install common utilities +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + wget \ + software-properties-common + +# Clean up +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + +# Set the working directory +WORKDIR /workspace diff --git a/src/crewai_tools/tools/code_interpreter_tool/README.md b/src/crewai_tools/tools/code_interpreter_tool/README.md new file mode 100644 index 000000000..e66a82e39 --- /dev/null +++ b/src/crewai_tools/tools/code_interpreter_tool/README.md @@ -0,0 +1,29 @@ +# CodeInterpreterTool + +## Description +This tool is used to give the Agent the ability to run code (Python3) from the code generated by the Agent itself. The code is executed in a sandboxed environment, so it is safe to run any code. + +It is incredible useful since it allows the Agent to generate code, run it in the same environment, get the result and use it to make decisions. + +## Requirements + +- Docker + +## Installation +Install the crewai_tools package +```shell +pip install 'crewai[tools]' +``` + +## Example + +Remember that when using this tool, the code must be generated by the Agent itself. The code must be a Python3 code. And it will take some time for the first time to run because it needs to build the Docker image. + +```python +from crewai_tools import CodeInterpreterTool + +Agent( + ... + tools=[CodeInterpreterTool()], +) +``` diff --git a/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py b/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py new file mode 100644 index 000000000..06cb081f0 --- /dev/null +++ b/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py @@ -0,0 +1,94 @@ +import os +from typing import Optional, Type + +import docker +from crewai_tools.tools.base_tool import BaseTool +from pydantic.v1 import BaseModel, Field + + +class FixedCodeInterpreterSchemaSchema(BaseModel): + """Input for CodeInterpreterTool.""" + + pass + + +class CodeInterpreterSchema(FixedCodeInterpreterSchemaSchema): + """Input for CodeInterpreterTool.""" + + code: str = Field( + ..., + description="Python3 code used to be interpreted in the Docker container. ALWAYS PRINT the final result and the output of the code", + ) + libraries_used: Optional[str] = Field( + None, + description="List of libraries used in the code with proper installing names separated by commas. Example: numpy,pandas,beautifulsoup4", + ) + + +class CodeInterpreterTool(BaseTool): + name: str = "Code Interpreter" + description: str = "Interprets Python code in a Docker container. ALWAYS PRINT the final result and the output of the code" + args_schema: Type[BaseModel] = CodeInterpreterSchema + code: Optional[str] = None + + def _verify_docker_image(self) -> None: + """ + Verify if the Docker image is available + """ + image_tag = "code-interpreter:latest" + + client = docker.from_env() + images = client.images.list() + all_tags = [tag for image in images for tag in image.tags] + + if image_tag not in all_tags: + client.images.build( + path=os.path.dirname(os.path.abspath(__file__)), + tag=image_tag, + rm=True, + ) + + def __init__(self, code: Optional[str] = None, **kwargs) -> None: + super().__init__(**kwargs) + if code is not None: + self._verify_docker_image() + self.code = code + self.description = "Interprets Python code in a Docker container. ALWAYS PRINT the final result and the output of the code" + self.args_schema = FixedCodeInterpreterSchemaSchema + self._generate_description() + + def _run(self, **kwargs) -> str: + code = kwargs.get("code", self.code) + libraries_used = kwargs.get("libraries_used", None) + return self.run_code_in_docker(code, libraries_used) + + def _install_libraries( + self, container: docker.models.containers.Container, libraries: list[str] + ) -> None: + """ + Install missing libraries in the Docker container + """ + for library in libraries: + container.exec_run(f"pip install {library}") + + def _init_docker_container(self) -> docker.models.containers.Container: + client = docker.from_env() + return client.containers.run( + "code-interpreter", detach=True, tty=True, working_dir="/workspace" + ) + + def run_code_in_docker(self, code: str, libraries_used: str) -> str: + container = self._init_docker_container() + + if libraries_used: + self._install_libraries(container, libraries_used.split(",")) + + cmd_to_run = f'python3 -c "{code}"' + exec_result = container.exec_run(cmd_to_run) + + container.stop() + container.remove() + + if exec_result.exit_code != 0: + return f"Something went wrong while running the code: \n{exec_result.output.decode('utf-8')}" + return exec_result.output.decode("utf-8") diff --git a/tests/tools/test_code_interpreter_tool.py b/tests/tools/test_code_interpreter_tool.py new file mode 100644 index 000000000..a9ffb9dbc --- /dev/null +++ b/tests/tools/test_code_interpreter_tool.py @@ -0,0 +1,38 @@ +import unittest +from unittest.mock import patch + +from crewai_tools.tools.code_interpreter_tool.code_interpreter_tool import ( + CodeInterpreterTool, +) + + +class TestCodeInterpreterTool(unittest.TestCase): + @patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.docker") + def test_run_code_in_docker(self, docker_mock): + tool = CodeInterpreterTool() + code = "print('Hello, World!')" + libraries_used = "numpy,pandas" + expected_output = "Hello, World!\n" + + docker_mock.from_env().containers.run().exec_run().exit_code = 0 + docker_mock.from_env().containers.run().exec_run().output = ( + expected_output.encode() + ) + result = tool.run_code_in_docker(code, libraries_used) + + self.assertEqual(result, expected_output) + + @patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.docker") + def test_run_code_in_docker_with_error(self, docker_mock): + tool = CodeInterpreterTool() + code = "print(1/0)" + libraries_used = "numpy,pandas" + expected_output = "Something went wrong while running the code: \nZeroDivisionError: division by zero\n" + + docker_mock.from_env().containers.run().exec_run().exit_code = 1 + docker_mock.from_env().containers.run().exec_run().output = ( + b"ZeroDivisionError: division by zero\n" + ) + result = tool.run_code_in_docker(code, libraries_used) + + self.assertEqual(result, expected_output)