From 3e0a851606c3e539e8b241cd169aa9b23018e73d Mon Sep 17 00:00:00 2001 From: erikkaum Date: Tue, 17 Dec 2024 15:29:47 +0100 Subject: [PATCH 1/2] alternative of how to interface with docker --- .gitignore | 138 ++++++++++++++++++++++++++++++- Dockerfile | 27 ++++++ examples/docker_example.py | 12 +++ src/agents/docker_alternative.py | 66 +++++++++++++++ 4 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 Dockerfile create mode 100644 examples/docker_example.py create mode 100644 src/agents/docker_alternative.py diff --git a/.gitignore b/.gitignore index 897944e..b9ee3ba 100644 --- a/.gitignore +++ b/.gitignore @@ -13,13 +13,147 @@ outputs # VS Code .vscode +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# .python-version + +# pipenv +#Pipfile.lock + +# UV +#uv.lock + +# poetry +#poetry.lock + +# pdm +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + # Environments .env .venv env/ venv/ +ENV/ env.bak/ venv.bak/ -# Jupyter Notebook -.ipynb_checkpoints \ No newline at end of file +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +#.idea/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..758e0da --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Base Python image +FROM python:3.12-slim + +# Set working directory +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + gcc \ + g++ \ + zlib1g-dev \ + libjpeg-dev \ + libpng-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy package files +COPY . /app/ + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Install the package +RUN pip install -e . + +# Command to run when container starts +CMD ["python"] diff --git a/examples/docker_example.py b/examples/docker_example.py new file mode 100644 index 0000000..0db5b53 --- /dev/null +++ b/examples/docker_example.py @@ -0,0 +1,12 @@ +from agents.search import DuckDuckGoSearchTool +from agents.docker_alternative import DockerPythonInterpreter + +container = DockerPythonInterpreter() + +tools = [DuckDuckGoSearchTool] + +output = container.execute("res = web_search(query='whats the capital of Cambodia?'); print(res)", tools=tools) + +print(output) + +container.stop() diff --git a/src/agents/docker_alternative.py b/src/agents/docker_alternative.py new file mode 100644 index 0000000..6fd11e8 --- /dev/null +++ b/src/agents/docker_alternative.py @@ -0,0 +1,66 @@ +import docker +from typing import List, Optional +import warnings + +from agents.tools import Tool + +class DockerPythonInterpreter: + def __init__(self): + self.container = None + try: + self.client = docker.from_env() + self.client.ping() + except docker.errors.DockerException: + raise RuntimeError( + "Could not connect to Docker daemon. Please ensure Docker is installed and running." + ) + + try: + self.container = self.client.containers.run( + "pyrunner:latest", + "tail -f /dev/null", + detach=True, + remove=True, + ) + except docker.errors.DockerException as e: + raise RuntimeError(f"Failed to create Docker container: {e}") + + def stop(self): + """Cleanup: Stop and remove container when object is destroyed""" + if self.container: + try: + self.container.kill() # can consider .stop(), but this is faster + except Exception as e: + warnings.warn(f"Failed to stop Docker container: {e}") + + def execute(self, code: str, tools: Optional[List[Tool]] = None) -> str: + """ + Execute Python code in the container and return stdout and stderr + """ + + tool_instance = tools[0]() + + import_code = f""" +module_path = '{tool_instance.__class__.__module__}' +class_name = '{tool_instance.__class__.__name__}' + +import importlib + +module = importlib.import_module(module_path) +web_search = getattr(module, class_name)() +""" + + full_code = import_code + "\n" + code + + try: + exec_command = self.container.exec_run( + cmd=["python", "-c", full_code], + ) + output = exec_command.output + return output.decode('utf-8') + + except Exception as e: + return f"Error executing code: {str(e)}" + + +__all__ = ["DockerPythonInterpreter"] \ No newline at end of file From 6b05056a7c18a1b655f85dfa738c5b0066ac43bc Mon Sep 17 00:00:00 2001 From: erikkaum Date: Tue, 17 Dec 2024 17:01:34 +0100 Subject: [PATCH 2/2] another example --- Dockerfile | 5 ++-- examples/docker_example.py | 31 +++++++++++++++++++-- examples/dummytool.py | 14 ++++++++++ server.py | 46 ++++++++++++++++++++++++++++++++ src/agents/docker_alternative.py | 22 ++++++++------- 5 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 examples/dummytool.py create mode 100644 server.py diff --git a/Dockerfile b/Dockerfile index 758e0da..321f455 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,5 +23,6 @@ RUN pip install --no-cache-dir -r requirements.txt # Install the package RUN pip install -e . -# Command to run when container starts -CMD ["python"] +COPY server.py /app/server.py + +CMD ["python", "/app/server.py"] diff --git a/examples/docker_example.py b/examples/docker_example.py index 0db5b53..f229bb9 100644 --- a/examples/docker_example.py +++ b/examples/docker_example.py @@ -1,12 +1,39 @@ from agents.search import DuckDuckGoSearchTool from agents.docker_alternative import DockerPythonInterpreter + +test = """ +from agents.tools import Tool + +class DummyTool(Tool): + name = "echo" + description = '''Perform a web search based on your query (think a Google search) then returns the top search results as a list of dict elements. + Each result has keys 'title', 'href' and 'body'.''' + inputs = { + "cmd": {"type": "string", "description": "The search query to perform."} + } + output_type = "any" + + def forward(self, cmd: str) -> str: + return cmd + +""" + container = DockerPythonInterpreter() -tools = [DuckDuckGoSearchTool] +output = container.execute("x = 5") +print(f"first output: {output}") +output = container.execute("print(x)") +print(f"second output: {output}") -output = container.execute("res = web_search(query='whats the capital of Cambodia?'); print(res)", tools=tools) +breakpoint() +print("---------") + +output = container.execute(test) +print(output) + +output = container.execute("res = DummyTool(cmd='echo this'); print(res)") print(output) container.stop() diff --git a/examples/dummytool.py b/examples/dummytool.py new file mode 100644 index 0000000..75edfd9 --- /dev/null +++ b/examples/dummytool.py @@ -0,0 +1,14 @@ +from agents.tools import Tool + + +class DummyTool(Tool): + name = "echo" + description = """Perform a web search based on your query (think a Google search) then returns the top search results as a list of dict elements. + Each result has keys 'title', 'href' and 'body'.""" + inputs = { + "cmd": {"type": "string", "description": "The search query to perform."} + } + output_type = "any" + + def forward(self, cmd: str) -> str: + return cmd \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..ebdc61d --- /dev/null +++ b/server.py @@ -0,0 +1,46 @@ +import socket +import sys +import traceback +import io + +exec_globals = {} +exec_locals = {} + +def execute_code(code): + stdout = io.StringIO() + stderr = io.StringIO() + sys.stdout = stdout + sys.stderr = stderr + + try: + exec(code, exec_globals, exec_locals) + except Exception as e: + traceback.print_exc(file=stderr) + + output = stdout.getvalue() + error = stderr.getvalue() + + # Restore original stdout and stderr + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + + return output + error + +def start_server(host='0.0.0.0', port=65432): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind((host, port)) + s.listen() + print(f"Server listening on {host}:{port}") + while True: + conn, addr = s.accept() + with conn: + print(f"Connected by {addr}") + data = conn.recv(1024) + if not data: + break + code = data.decode('utf-8') + output = execute_code(code) + conn.sendall(output.encode('utf-8')) + +if __name__ == "__main__": + start_server() \ No newline at end of file diff --git a/src/agents/docker_alternative.py b/src/agents/docker_alternative.py index 6fd11e8..b035c7e 100644 --- a/src/agents/docker_alternative.py +++ b/src/agents/docker_alternative.py @@ -1,6 +1,7 @@ import docker from typing import List, Optional import warnings +import socket from agents.tools import Tool @@ -18,7 +19,7 @@ class DockerPythonInterpreter: try: self.container = self.client.containers.run( "pyrunner:latest", - "tail -f /dev/null", + ports={'65432/tcp': 65432}, detach=True, remove=True, ) @@ -37,10 +38,11 @@ class DockerPythonInterpreter: """ Execute Python code in the container and return stdout and stderr """ - - tool_instance = tools[0]() - import_code = f""" + if tools != None: + tool_instance = tools[0]() + + import_code = f""" module_path = '{tool_instance.__class__.__module__}' class_name = '{tool_instance.__class__.__name__}' @@ -50,14 +52,14 @@ module = importlib.import_module(module_path) web_search = getattr(module, class_name)() """ - full_code = import_code + "\n" + code + code = import_code + "\n" + code try: - exec_command = self.container.exec_run( - cmd=["python", "-c", full_code], - ) - output = exec_command.output - return output.decode('utf-8') + # Connect to the server running inside the container + with socket.create_connection(('localhost', 65432)) as sock: + sock.sendall(code.encode('utf-8')) + output = sock.recv(4096) + return output.decode('utf-8') except Exception as e: return f"Error executing code: {str(e)}"