From 2b47377a7849be73c8ecac19338aaa7811bdbd27 Mon Sep 17 00:00:00 2001 From: Eduardo Chiarotti Date: Wed, 19 Jun 2024 20:45:04 -0300 Subject: [PATCH 1/6] feat: add code-interpreter tool --- .../tools/code_interpreter_tool/Dockerfile | 21 +++++++ .../tools/code_interpreter_tool/README.md | 0 .../code_interpreter_tool.py | 57 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 src/crewai_tools/tools/code_interpreter_tool/Dockerfile create mode 100644 src/crewai_tools/tools/code_interpreter_tool/README.md create mode 100644 src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py 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..b72a51a88 --- /dev/null +++ b/src/crewai_tools/tools/code_interpreter_tool/Dockerfile @@ -0,0 +1,21 @@ +# Use an official Ubuntu as a parent image +FROM ubuntu:20.04 + +# Set environment variables +ENV DEBIAN_FRONTEND=noninteractive + +# Install common utilities +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + wget \ + software-properties-common + +# Install Python +RUN apt-get install -y python3 python3-pip + +# 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..e69de29bb 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..a2066ca03 --- /dev/null +++ b/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py @@ -0,0 +1,57 @@ +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 DirectoryReadTool.""" + + pass + + +class CodeInterpreterSchema(FixedCodeInterpreterSchemaSchema): + """Input for DirectoryReadTool.""" + + code: str = Field( + ..., + description="Python3 code used to be interpreted in the Docker container and output the result", + ) + + +class CodeInterpreterTool(BaseTool): + name: str = "Code Interpreter" + description: str = "Interprets Python code in a Docker container" + args_schema: Type[BaseModel] = CodeInterpreterSchema + code: Optional[str] = None + + def __init__(self, code: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if code is not None: + self.code = code + self.description = ( + "A tool that can be used to run Python code in a Docker container" + ) + self.args_schema = FixedCodeInterpreterSchemaSchema + self._generate_description() + + def _run(self, **kwargs): + code = kwargs.get("code", self.code) + return self.run_code_in_docker(code) + + def run_code_in_docker(self, code): + client = docker.from_env() + container = client.containers.run( + "code-interpreter", + command=f'python3 -c "{code}"', + detach=True, + working_dir="/workspace", + ) + + result = container.logs().decode("utf-8") + + container.stop() + container.remove() + + return result From da75d51fe8431aae6b7333cdeee0ecf5d9e91843 Mon Sep 17 00:00:00 2001 From: Eduardo Chiarotti Date: Thu, 20 Jun 2024 20:24:26 -0300 Subject: [PATCH 2/6] feat: add Dockerfile, Makefile and update version of code --- .../tools/code_interpreter_tool/Dockerfile | 9 +--- .../tools/code_interpreter_tool/Makefile | 6 +++ .../code_interpreter_tool.py | 51 ++++++++++++------- 3 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 src/crewai_tools/tools/code_interpreter_tool/Makefile diff --git a/src/crewai_tools/tools/code_interpreter_tool/Dockerfile b/src/crewai_tools/tools/code_interpreter_tool/Dockerfile index b72a51a88..ae9b2ffd6 100644 --- a/src/crewai_tools/tools/code_interpreter_tool/Dockerfile +++ b/src/crewai_tools/tools/code_interpreter_tool/Dockerfile @@ -1,8 +1,4 @@ -# Use an official Ubuntu as a parent image -FROM ubuntu:20.04 - -# Set environment variables -ENV DEBIAN_FRONTEND=noninteractive +FROM python:3.11-slim # Install common utilities RUN apt-get update && apt-get install -y \ @@ -11,9 +7,6 @@ RUN apt-get update && apt-get install -y \ wget \ software-properties-common -# Install Python -RUN apt-get install -y python3 python3-pip - # Clean up RUN apt-get clean && rm -rf /var/lib/apt/lists/* diff --git a/src/crewai_tools/tools/code_interpreter_tool/Makefile b/src/crewai_tools/tools/code_interpreter_tool/Makefile new file mode 100644 index 000000000..5a514db0a --- /dev/null +++ b/src/crewai_tools/tools/code_interpreter_tool/Makefile @@ -0,0 +1,6 @@ +# Makefile +IMAGE_NAME=code-interpreter +TAG=latest + +build: + docker build -t $(IMAGE_NAME):$(TAG) . 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 index a2066ca03..f497a7c96 100644 --- a/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py +++ b/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py @@ -6,23 +6,27 @@ from pydantic.v1 import BaseModel, Field class FixedCodeInterpreterSchemaSchema(BaseModel): - """Input for DirectoryReadTool.""" + """Input for CodeInterpreterTool.""" pass class CodeInterpreterSchema(FixedCodeInterpreterSchemaSchema): - """Input for DirectoryReadTool.""" + """Input for CodeInterpreterTool.""" code: str = Field( ..., - description="Python3 code used to be interpreted in the Docker container and output the result", + 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" + 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 @@ -30,28 +34,41 @@ class CodeInterpreterTool(BaseTool): super().__init__(**kwargs) if code is not None: self.code = code - self.description = ( - "A tool that can be used to run Python code in a Docker container" - ) + 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): code = kwargs.get("code", self.code) - return self.run_code_in_docker(code) + libraries_used = kwargs.get("libraries_used", None) + return self.run_code_in_docker(code, libraries_used) - def run_code_in_docker(self, code): + def run_code_in_docker(self, code, libraries_used): client = docker.from_env() - container = client.containers.run( - "code-interpreter", - command=f'python3 -c "{code}"', - detach=True, - working_dir="/workspace", - ) - result = container.logs().decode("utf-8") + def run_code(container, code): + cmd_to_run = f'python3 -c "{code}"' + exec_result = container.exec_run(cmd_to_run) + return exec_result + + container = client.containers.run( + "code-interpreter", detach=True, tty=True, working_dir="/workspace" + ) + if libraries_used: + self._install_libraries(container, libraries_used.split(",")) + + exec_result = run_code(container, code) 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 result + return exec_result.output.decode("utf-8") + + def _install_libraries(self, container, libraries): + """ + Install missing libraries in the Docker container + """ + for library in libraries: + container.exec_run(f"pip install {library}") From 1a4ac76b1eb11ec70a11784a4f6b1d33948e9171 Mon Sep 17 00:00:00 2001 From: Eduardo Chiarotti Date: Thu, 20 Jun 2024 20:24:47 -0300 Subject: [PATCH 3/6] feat: update code --- .../code_interpreter_tool.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) 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 index f497a7c96..caafd44e3 100644 --- a/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py +++ b/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py @@ -30,7 +30,7 @@ class CodeInterpreterTool(BaseTool): args_schema: Type[BaseModel] = CodeInterpreterSchema code: Optional[str] = None - def __init__(self, code: Optional[str] = None, **kwargs): + def __init__(self, code: Optional[str] = None, **kwargs) -> None: super().__init__(**kwargs) if code is not None: self.code = code @@ -38,37 +38,37 @@ class CodeInterpreterTool(BaseTool): self.args_schema = FixedCodeInterpreterSchemaSchema self._generate_description() - def _run(self, **kwargs): + 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 run_code_in_docker(self, code, libraries_used): - client = docker.from_env() - - def run_code(container, code): - cmd_to_run = f'python3 -c "{code}"' - exec_result = container.exec_run(cmd_to_run) - return exec_result - - container = client.containers.run( - "code-interpreter", detach=True, tty=True, working_dir="/workspace" - ) - if libraries_used: - self._install_libraries(container, libraries_used.split(",")) - - exec_result = run_code(container, code) - - 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") - - def _install_libraries(self, container, libraries): + 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().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") From 94e6651b55cbebd85b0610787de5f4429e0778b1 Mon Sep 17 00:00:00 2001 From: Eduardo Chiarotti Date: Thu, 20 Jun 2024 20:43:19 -0300 Subject: [PATCH 4/6] feat: add code-interpreter tool to init and add unit tests --- src/crewai_tools/tools/__init__.py | 17 ++++++---- tests/tools/test_code_interpreter_tool.py | 38 +++++++++++++++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 tests/tools/test_code_interpreter_tool.py diff --git a/src/crewai_tools/tools/__init__.py b/src/crewai_tools/tools/__init__.py index 4da0c0337..35b81396b 100644 --- a/src/crewai_tools/tools/__init__.py +++ b/src/crewai_tools/tools/__init__.py @@ -1,24 +1,29 @@ 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 .csv_search_tool.csv_search_tool import CSVSearchTool -from .directory_search_tool.directory_search_tool import DirectorySearchTool from .directory_read_tool.directory_read_tool import DirectoryReadTool +from .directory_search_tool.directory_search_tool import DirectorySearchTool from .docx_search_tool.docx_search_tool import DOCXSearchTool from .exa_tools.exa_search_tool import EXASearchTool from .file_read_tool.file_read_tool import FileReadTool from .github_search_tool.github_search_tool import GithubSearchTool -from .serper_dev_tool.serper_dev_tool import SerperDevTool -from .txt_search_tool.txt_search_tool import TXTSearchTool from .json_search_tool.json_search_tool import JSONSearchTool +from .llamaindex_tool.llamaindex_tool import LlamaIndexTool from .mdx_seach_tool.mdx_search_tool import MDXSearchTool from .pdf_search_tool.pdf_search_tool import PDFSearchTool from .pg_seach_tool.pg_search_tool import PGSearchTool from .rag.rag_tool import RagTool -from .scrape_element_from_website.scrape_element_from_website import ScrapeElementFromWebsiteTool +from .scrape_element_from_website.scrape_element_from_website import ( + ScrapeElementFromWebsiteTool, +) from .scrape_website_tool.scrape_website_tool import ScrapeWebsiteTool from .selenium_scraping_tool.selenium_scraping_tool import SeleniumScrapingTool +from .serper_dev_tool.serper_dev_tool import SerperDevTool +from .txt_search_tool.txt_search_tool import TXTSearchTool from .website_search.website_search_tool import WebsiteSearchTool from .xml_search_tool.xml_search_tool import XMLSearchTool -from .youtube_channel_search_tool.youtube_channel_search_tool import YoutubeChannelSearchTool +from .youtube_channel_search_tool.youtube_channel_search_tool import ( + YoutubeChannelSearchTool, +) from .youtube_video_search_tool.youtube_video_search_tool import YoutubeVideoSearchTool -from .llamaindex_tool.llamaindex_tool import LlamaIndexTool 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) From 61cce93fd020fa4fce74cc80f64e07f2a777215d Mon Sep 17 00:00:00 2001 From: Eduardo Chiarotti Date: Thu, 20 Jun 2024 21:41:12 -0300 Subject: [PATCH 5/6] feat: remove unused Makefile, update README and update code --- .../tools/code_interpreter_tool/Makefile | 6 ----- .../tools/code_interpreter_tool/README.md | 27 +++++++++++++++++++ .../code_interpreter_tool.py | 22 ++++++++++++++- 3 files changed, 48 insertions(+), 7 deletions(-) delete mode 100644 src/crewai_tools/tools/code_interpreter_tool/Makefile diff --git a/src/crewai_tools/tools/code_interpreter_tool/Makefile b/src/crewai_tools/tools/code_interpreter_tool/Makefile deleted file mode 100644 index 5a514db0a..000000000 --- a/src/crewai_tools/tools/code_interpreter_tool/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -# Makefile -IMAGE_NAME=code-interpreter -TAG=latest - -build: - docker build -t $(IMAGE_NAME):$(TAG) . diff --git a/src/crewai_tools/tools/code_interpreter_tool/README.md b/src/crewai_tools/tools/code_interpreter_tool/README.md index e69de29bb..672c86f21 100644 --- a/src/crewai_tools/tools/code_interpreter_tool/README.md +++ b/src/crewai_tools/tools/code_interpreter_tool/README.md @@ -0,0 +1,27 @@ +# 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. + +## 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 index caafd44e3..06cb081f0 100644 --- a/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py +++ b/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py @@ -1,3 +1,4 @@ +import os from typing import Optional, Type import docker @@ -30,9 +31,27 @@ class CodeInterpreterTool(BaseTool): 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 @@ -67,7 +86,8 @@ class CodeInterpreterTool(BaseTool): cmd_to_run = f'python3 -c "{code}"' exec_result = container.exec_run(cmd_to_run) - container.stop().remove() + 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')}" From 161c72b29f553f6b3758b9408ff9423349c5bba7 Mon Sep 17 00:00:00 2001 From: Eduardo Chiarotti Date: Thu, 20 Jun 2024 21:55:25 -0300 Subject: [PATCH 6/6] feat: update README --- src/crewai_tools/tools/code_interpreter_tool/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/crewai_tools/tools/code_interpreter_tool/README.md b/src/crewai_tools/tools/code_interpreter_tool/README.md index 672c86f21..e66a82e39 100644 --- a/src/crewai_tools/tools/code_interpreter_tool/README.md +++ b/src/crewai_tools/tools/code_interpreter_tool/README.md @@ -3,6 +3,8 @@ ## 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