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..321f455 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# 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 . + +COPY server.py /app/server.py + +CMD ["python", "/app/server.py"] diff --git a/examples/docker_example.py b/examples/docker_example.py new file mode 100644 index 0000000..f229bb9 --- /dev/null +++ b/examples/docker_example.py @@ -0,0 +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() + +output = container.execute("x = 5") +print(f"first output: {output}") +output = container.execute("print(x)") +print(f"second output: {output}") + +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 new file mode 100644 index 0000000..b035c7e --- /dev/null +++ b/src/agents/docker_alternative.py @@ -0,0 +1,68 @@ +import docker +from typing import List, Optional +import warnings +import socket + +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", + ports={'65432/tcp': 65432}, + 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 + """ + + if tools != None: + 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)() +""" + + code = import_code + "\n" + code + + try: + # 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)}" + + +__all__ = ["DockerPythonInterpreter"] \ No newline at end of file