├── .github └── workflows │ └── python-test.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ └── index.rst ├── examples ├── adapter │ └── openai.ipynb ├── langchain │ └── langchain_with_local_jupyter_sandbox.ipynb └── sandbox │ ├── local_jupyter_async.py │ └── local_jupyter_sync.py ├── poetry.lock ├── poetry.toml ├── pyproject.toml ├── pytest.ini ├── src ├── codeinterpreter │ ├── __init__.py │ ├── adapter │ │ ├── __init__.py │ │ ├── adapter.py │ │ └── openai.py │ ├── agent │ │ ├── __init__.py │ │ └── openai_functions_agent │ │ │ ├── __init__.py │ │ │ └── base.py │ ├── core.py │ ├── sandbox │ │ ├── __init__.py │ │ ├── docker_jupyter │ │ │ └── __init__.py │ │ ├── factory.py │ │ ├── kubernetes_jupyter │ │ │ └── __init__.py │ │ ├── local_jupyter │ │ │ ├── __init__.py │ │ │ ├── manager.py │ │ │ └── sandbox.py │ │ ├── manager.py │ │ ├── sandbox.py │ │ └── schema.py │ ├── session.py │ └── utils │ │ ├── __init__.py │ │ ├── dependency.py │ │ ├── error.py │ │ └── log.py └── codeinterpreter_daemon │ └── __init__.py └── tests └── unit ├── sandbox └── test_local_jupyter.py └── utils ├── test_error.py └── test_log.py /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python Test 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: true 20 | matrix: 21 | python-version: ["3.8", "3.9", "3.10", "3.11"] 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Set up Python 25 | uses: actions/setup-python@v4 26 | id: setup-python 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Install Poetry 30 | uses: snok/install-poetry@v1 31 | with: 32 | virtualenvs-create: true 33 | virtualenvs-in-project: true 34 | - name: Load cached venv 35 | id: cached-poetry-dependencies 36 | uses: actions/cache@v3 37 | with: 38 | path: .venv 39 | key: venv-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 40 | - name: Install dependencies 41 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 42 | run: poetry install --no-interaction --no-root 43 | - name: Install library 44 | run: poetry install --no-interaction 45 | - name: Test with pytest 46 | run: | 47 | source .venv/bin/activate 48 | make pytest 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | .sandbox/ 163 | .gitpod.yml 164 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.python", 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "ms-python.black-formatter" 6 | }, 7 | "python.formatting.provider": "none", 8 | "python.analysis.extraPaths": [ 9 | "./src" 10 | ], 11 | "python.testing.pytestArgs": [ 12 | "--log-cli-level=DEBUG", 13 | "tests" 14 | ], 15 | "python.testing.unittestEnabled": false, 16 | "python.testing.pytestEnabled": true, 17 | "python.linting.pylintEnabled": true, 18 | "python.linting.enabled": true, 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | pytest: 2 | pytest tests/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeInterpreter: The Best Open Source LLM Code Interpreter. 2 | 3 | [![Python Test](https://github.com/arcosx/CodeInterpreter/actions/workflows/python-test.yml/badge.svg)](https://github.com/arcosx/CodeInterpreter/actions/workflows/python-test.yml) 4 | 5 | ```shell 6 | pip install codeinterpreter 7 | ``` -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'CodeInterpreter' 10 | copyright = '2023, arcosx' 11 | author = 'arcosx' 12 | 13 | # -- General configuration --------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 15 | 16 | extensions = [] 17 | 18 | templates_path = ['_templates'] 19 | exclude_patterns = [] 20 | 21 | 22 | 23 | # -- Options for HTML output ------------------------------------------------- 24 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 25 | 26 | html_theme = 'alabaster' 27 | html_static_path = ['_static'] 28 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. CodeInterpreter documentation master file, created by 2 | sphinx-quickstart on Sat Sep 2 16:24:11 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to CodeInterpreter's documentation! 7 | =========================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /examples/adapter/openai.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%pip install codeinterpreter" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from codeinterpreter.adapter.openai import OpenAI\n", 19 | "from codeinterpreter.sandbox.local_jupyter.manager import LocalJupyterManager\n", 20 | "manager = LocalJupyterManager()\n", 21 | "manager.init()\n", 22 | "sandbox = manager.start()" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 7, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "openai = OpenAI(sandbox=sandbox)" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 8, 37 | "metadata": {}, 38 | "outputs": [ 39 | { 40 | "data": { 41 | "text/plain": [ 42 | "'The two random numbers are 85 and 67.'" 43 | ] 44 | }, 45 | "execution_count": 8, 46 | "metadata": {}, 47 | "output_type": "execute_result" 48 | } 49 | ], 50 | "source": [ 51 | "result = openai.run(\"print two random number\")\n", 52 | "result" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 9, 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "data": { 62 | "text/plain": [ 63 | "'The computer name of this machine is \"codespaces-43cf0f\".'" 64 | ] 65 | }, 66 | "execution_count": 9, 67 | "metadata": {}, 68 | "output_type": "execute_result" 69 | } 70 | ], 71 | "source": [ 72 | "result = openai.run(\"get this computer name\")\n", 73 | "result" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 10, 79 | "metadata": {}, 80 | "outputs": [ 81 | { 82 | "data": { 83 | "text/plain": [ 84 | "'The title of the page is \"Evolution\".'" 85 | ] 86 | }, 87 | "execution_count": 10, 88 | "metadata": {}, 89 | "output_type": "execute_result" 90 | } 91 | ], 92 | "source": [ 93 | "result = openai.run(\"read this page https://simple.wikipedia.org/wiki/Evolution and get title for me\")\n", 94 | "result" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [] 103 | } 104 | ], 105 | "metadata": { 106 | "kernelspec": { 107 | "display_name": "codeinterpreter-e5HWGu79-py3.10", 108 | "language": "python", 109 | "name": "python3" 110 | }, 111 | "language_info": { 112 | "codemirror_mode": { 113 | "name": "ipython", 114 | "version": 3 115 | }, 116 | "file_extension": ".py", 117 | "mimetype": "text/x-python", 118 | "name": "python", 119 | "nbconvert_exporter": "python", 120 | "pygments_lexer": "ipython3", 121 | "version": "3.10.8" 122 | }, 123 | "orig_nbformat": 4 124 | }, 125 | "nbformat": 4, 126 | "nbformat_minor": 2 127 | } 128 | -------------------------------------------------------------------------------- /examples/langchain/langchain_with_local_jupyter_sandbox.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%pip install codeinterpreter\n" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from codeinterpreter.sandbox.local_jupyter.manager import LocalJupyterManager" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 2, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "manager = LocalJupyterManager()\n", 28 | "manager.init()\n", 29 | "sandbox = manager.start()" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 3, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "from pydantic import BaseModel\n", 39 | "from langchain.tools import StructuredTool\n", 40 | "\n", 41 | "def code_run(code:str):\n", 42 | " output = sandbox.run(code=code)\n", 43 | " return output.content\n", 44 | "\n", 45 | "class CodeInput(BaseModel):\n", 46 | " code: str\n", 47 | "\n", 48 | "tools = [\n", 49 | " StructuredTool(\n", 50 | " name=\"python\",\n", 51 | " description=\"Input a string of code to a ipython interpreter. \"\n", 52 | " \"Write the entire code in a single string. This string can \"\n", 53 | " \"be really long, so you can use the `;` character to split lines. \"\n", 54 | " \"Variables are preserved between runs. \",\n", 55 | " func=code_run,\n", 56 | " args_schema=CodeInput,\n", 57 | " )\n", 58 | "]\n", 59 | "\n" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 4, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "from langchain.chat_models import ChatOpenAI\n", 69 | "from langchain.agents import AgentExecutor\n", 70 | "from langchain.memory import ConversationBufferMemory\n", 71 | "from codeinterpreter.agent.openai_functions_agent.base import CustomOpenAIFunctionsAgent\n", 72 | "\n", 73 | "\n", 74 | "llm = ChatOpenAI(temperature=0, model=\"gpt-3.5-turbo-0613\")\n", 75 | "\n", 76 | "agent = CustomOpenAIFunctionsAgent.from_llm_and_tools(llm=llm,tools=tools)\n", 77 | "\n", 78 | "agent_executor = AgentExecutor(agent=agent, tools=tools, memory=ConversationBufferMemory(\n", 79 | " memory_key=\"chat_history\", return_messages=True\n", 80 | " ), verbose=True)" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 5, 86 | "metadata": {}, 87 | "outputs": [ 88 | { 89 | "name": "stdout", 90 | "output_type": "stream", 91 | "text": [ 92 | "\n", 93 | "\n", 94 | "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", 95 | "\u001b[32;1m\u001b[1;3m\n", 96 | "Invoking: `python` with `import random\n", 97 | "\n", 98 | "# Generate two random numbers\n", 99 | "random_number1 = random.randint(1, 100)\n", 100 | "random_number2 = random.randint(1, 100)\n", 101 | "\n", 102 | "# Print the random numbers\n", 103 | "print(random_number1, random_number2)`\n", 104 | "\n", 105 | "\n", 106 | "\u001b[0m\u001b[36;1m\u001b[1;3m93 27\u001b[0m\u001b[32;1m\u001b[1;3mThe two random numbers are 93 and 27.\u001b[0m\n", 107 | "\n", 108 | "\u001b[1m> Finished chain.\u001b[0m\n" 109 | ] 110 | }, 111 | { 112 | "data": { 113 | "text/plain": [ 114 | "{'input': 'print two random number',\n", 115 | " 'chat_history': [HumanMessage(content='print two random number', additional_kwargs={}, example=False),\n", 116 | " AIMessage(content='The two random numbers are 93 and 27.', additional_kwargs={}, example=False)],\n", 117 | " 'output': 'The two random numbers are 93 and 27.'}" 118 | ] 119 | }, 120 | "execution_count": 5, 121 | "metadata": {}, 122 | "output_type": "execute_result" 123 | } 124 | ], 125 | "source": [ 126 | "result = agent_executor({\"input\": \"print two random number\"})\n", 127 | "result" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 6, 133 | "metadata": {}, 134 | "outputs": [ 135 | { 136 | "name": "stdout", 137 | "output_type": "stream", 138 | "text": [ 139 | "\n", 140 | "\n", 141 | "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", 142 | "\u001b[32;1m\u001b[1;3m\n", 143 | "Invoking: `python` with `import socket\n", 144 | "\n", 145 | "# Get the computer name\n", 146 | "computer_name = socket.gethostname()\n", 147 | "\n", 148 | "computer_name`\n", 149 | "\n", 150 | "\n", 151 | "\u001b[0m\u001b[36;1m\u001b[1;3m'arcosx-win'\u001b[0m\u001b[32;1m\u001b[1;3mThe computer name of this machine is \"arcosx-win\".\u001b[0m\n", 152 | "\n", 153 | "\u001b[1m> Finished chain.\u001b[0m\n" 154 | ] 155 | }, 156 | { 157 | "data": { 158 | "text/plain": [ 159 | "{'input': 'get this computer name',\n", 160 | " 'chat_history': [HumanMessage(content='print two random number', additional_kwargs={}, example=False),\n", 161 | " AIMessage(content='The two random numbers are 93 and 27.', additional_kwargs={}, example=False),\n", 162 | " HumanMessage(content='get this computer name', additional_kwargs={}, example=False),\n", 163 | " AIMessage(content='The computer name of this machine is \"arcosx-win\".', additional_kwargs={}, example=False)],\n", 164 | " 'output': 'The computer name of this machine is \"arcosx-win\".'}" 165 | ] 166 | }, 167 | "execution_count": 6, 168 | "metadata": {}, 169 | "output_type": "execute_result" 170 | } 171 | ], 172 | "source": [ 173 | "result = agent_executor({\"input\": \"get this computer name\"})\n", 174 | "result" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": 7, 180 | "metadata": {}, 181 | "outputs": [ 182 | { 183 | "name": "stdout", 184 | "output_type": "stream", 185 | "text": [ 186 | "\n", 187 | "\n", 188 | "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", 189 | "\u001b[32;1m\u001b[1;3m\n", 190 | "Invoking: `python` with `{'code': \"import requests\\nfrom bs4 import BeautifulSoup\\n\\nurl = 'https://simple.wikipedia.org/wiki/Evolution'\\nresponse = requests.get(url)\\nsoup = BeautifulSoup(response.content, 'html.parser')\\ntitle = soup.find('h1', {'id': 'firstHeading'}).text\\n\\ntitle\"}`\n", 191 | "\n", 192 | "\n", 193 | "\u001b[0m\u001b[36;1m\u001b[1;3m'Evolution'\u001b[0m\u001b[32;1m\u001b[1;3mThe title of the page is \"Evolution\".\u001b[0m\n", 194 | "\n", 195 | "\u001b[1m> Finished chain.\u001b[0m\n" 196 | ] 197 | }, 198 | { 199 | "data": { 200 | "text/plain": [ 201 | "{'input': 'read this page https://simple.wikipedia.org/wiki/Evolution and get title for me',\n", 202 | " 'chat_history': [HumanMessage(content='print two random number', additional_kwargs={}, example=False),\n", 203 | " AIMessage(content='The two random numbers are 93 and 27.', additional_kwargs={}, example=False),\n", 204 | " HumanMessage(content='get this computer name', additional_kwargs={}, example=False),\n", 205 | " AIMessage(content='The computer name of this machine is \"arcosx-win\".', additional_kwargs={}, example=False),\n", 206 | " HumanMessage(content='read this page https://simple.wikipedia.org/wiki/Evolution and get title for me', additional_kwargs={}, example=False),\n", 207 | " AIMessage(content='The title of the page is \"Evolution\".', additional_kwargs={}, example=False)],\n", 208 | " 'output': 'The title of the page is \"Evolution\".'}" 209 | ] 210 | }, 211 | "execution_count": 7, 212 | "metadata": {}, 213 | "output_type": "execute_result" 214 | } 215 | ], 216 | "source": [ 217 | "result = agent_executor({\"input\": \"read this page https://simple.wikipedia.org/wiki/Evolution and get title for me\"})\n", 218 | "result" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": null, 224 | "metadata": {}, 225 | "outputs": [], 226 | "source": [] 227 | } 228 | ], 229 | "metadata": { 230 | "kernelspec": { 231 | "display_name": ".venv", 232 | "language": "python", 233 | "name": "python3" 234 | }, 235 | "language_info": { 236 | "codemirror_mode": { 237 | "name": "ipython", 238 | "version": 3 239 | }, 240 | "file_extension": ".py", 241 | "mimetype": "text/x-python", 242 | "name": "python", 243 | "nbconvert_exporter": "python", 244 | "pygments_lexer": "ipython3", 245 | "version": "3.10.12" 246 | }, 247 | "orig_nbformat": 4 248 | }, 249 | "nbformat": 4, 250 | "nbformat_minor": 2 251 | } 252 | -------------------------------------------------------------------------------- /examples/sandbox/local_jupyter_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from notebook.services.kernels.kernelmanager import MappingKernelManager 5 | 6 | import codeinterpreter 7 | from codeinterpreter.sandbox.local_jupyter.manager import LocalJupyterManager 8 | 9 | logger = logging.getLogger(f"codeinterpreter:{codeinterpreter.__version__}") 10 | logger.setLevel(logging.DEBUG) 11 | 12 | async def calculate_add(manager:LocalJupyterManager)->int: 13 | sandbox_add = await manager.astart() 14 | await sandbox_add.arun("a = 5") 15 | await sandbox_add.arun("b = 10") 16 | add_result = await sandbox_add.arun("a + b") 17 | return int(add_result.content) 18 | 19 | async def calculate_mul(manager:LocalJupyterManager)->int: 20 | sandbox_mul = await manager.astart() 21 | await sandbox_mul.arun("a = 5") 22 | await sandbox_mul.arun("b = 10") 23 | mul_result = await sandbox_mul.arun("a * b") 24 | return int(mul_result.content) 25 | 26 | async def main(): 27 | try: 28 | manager = LocalJupyterManager() 29 | await manager.ainit() 30 | add_result, mul_result = await asyncio.gather(calculate_add(manager), calculate_mul(manager)) 31 | print(int(add_result) * int(mul_result)) 32 | 33 | finally: 34 | await manager.astop() 35 | 36 | if __name__ == "__main__": 37 | asyncio.run(main()) 38 | -------------------------------------------------------------------------------- /examples/sandbox/local_jupyter_sync.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import codeinterpreter 4 | from codeinterpreter.sandbox.local_jupyter.manager import LocalJupyterManager 5 | 6 | logger = logging.getLogger(f"codeinterpreter:{codeinterpreter.__version__}") 7 | logger.setLevel(logging.DEBUG) 8 | 9 | def main(): 10 | try: 11 | manager = LocalJupyterManager() 12 | manager.init() 13 | sandbox_add = manager.start() 14 | sandbox_add.run("a = 5") 15 | sandbox_add.run("b = 10") 16 | 17 | add_result = sandbox_add.run("a + b") 18 | 19 | sandbox_mul = manager.start() 20 | sandbox_mul.run("a = 5") 21 | sandbox_mul.run("b = 10") 22 | mul_result = sandbox_add.run("a * b") 23 | 24 | print(int(add_result.content) * int(mul_result.content)) 25 | 26 | finally: 27 | manager.stop() 28 | 29 | if __name__ == "__main__": 30 | main() -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | create = true 3 | in-project = true -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "codeinterpreter" 3 | version = "0.0.1" 4 | description = "The Best Open Source LLM Code Interpreter." 5 | authors = ["arcosx "] 6 | readme = "README.md" 7 | license = "Apache-2.0" 8 | packages = [{ include = "codeinterpreter", from = "src" }] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.8.1" 12 | aiohttp = "3.8.5" 13 | pydantic = "1.10.9" 14 | requests = "2.31.0" 15 | websockets = "11.0.3" 16 | jupyter-kernel-gateway = "2.5.2" 17 | langchain = "^0.0.256" 18 | openai = "^0.27.8" 19 | 20 | [tool.poetry.group.test.dependencies] 21 | psutil = "5.9.5" 22 | pytest = "7.4.0" 23 | 24 | [build-system] 25 | requires = ["poetry-core"] 26 | build-backend = "poetry.core.masonry.api" 27 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = "src" 3 | addopts = "--import-mode=importlib" 4 | log_cli = true 5 | log_cli_level = DEBUG -------------------------------------------------------------------------------- /src/codeinterpreter/__init__.py: -------------------------------------------------------------------------------- 1 | """codeinterpreter version""" 2 | __version__ = "0.0.1" -------------------------------------------------------------------------------- /src/codeinterpreter/adapter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcosx/CodeInterpreter/30cf6e26e18ac4ff83a631484cc829b461dc731b/src/codeinterpreter/adapter/__init__.py -------------------------------------------------------------------------------- /src/codeinterpreter/adapter/adapter.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Adapter(ABC): 5 | type: str 6 | -------------------------------------------------------------------------------- /src/codeinterpreter/adapter/openai.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from codeinterpreter.adapter.adapter import Adapter 3 | from codeinterpreter.agent.openai_functions_agent.base import CustomOpenAIFunctionsAgent 4 | from codeinterpreter.utils.log import codeinterpreter_log 5 | from codeinterpreter.sandbox.schema import SandboxRunOutput 6 | 7 | from pydantic import BaseModel 8 | from langchain.schema.language_model import BaseLanguageModel 9 | from codeinterpreter.sandbox.sandbox import Sandbox 10 | 11 | from langchain.chat_models import ChatOpenAI 12 | 13 | from langchain.agents import AgentExecutor, BaseSingleActionAgent 14 | from langchain.tools import BaseTool, StructuredTool 15 | 16 | 17 | class OpenAI(Adapter): 18 | def __init__(self, sandbox: Sandbox, model: str = "gpt-3.5-turbo-0613") -> None: 19 | super().__init__() 20 | self.sandbox: Sandbox = sandbox 21 | self.llm: BaseLanguageModel = ChatOpenAI(temperature=0, model=model) 22 | self.agent_executor: AgentExecutor = self._agent_executor() 23 | 24 | def _tools(self) -> List[BaseTool]: 25 | class CodeInput(BaseModel): 26 | code: str 27 | 28 | return [ 29 | StructuredTool( 30 | name="python", 31 | description="Input a string of code to a ipython interpreter. " 32 | "Write the entire code in a single string. This string can " 33 | "be really long, so you can use the `;` character to split lines. " 34 | "Variables are preserved between runs. ", 35 | func=self._sandbox_run_handler, 36 | coroutine=self._asandbox_run_handler, 37 | args_schema=CodeInput, 38 | ) 39 | ] 40 | 41 | def _agent(self) -> BaseSingleActionAgent: 42 | return CustomOpenAIFunctionsAgent.from_llm_and_tools( 43 | llm=self.llm, tools=self._tools() 44 | ) 45 | 46 | def _agent_executor(self) -> AgentExecutor: 47 | return AgentExecutor(agent=self._agent(), tools=self._tools()) 48 | 49 | def _sandbox_run_handler(self, code: str): 50 | output: SandboxRunOutput = self.sandbox.run(code=code) 51 | return output.content 52 | 53 | async def _asandbox_run_handler(self, code: str): 54 | output: SandboxRunOutput = await self.sandbox.arun(code=code) 55 | return output.content 56 | 57 | def run(self, code: str): 58 | return self.agent_executor.run(code) 59 | 60 | async def arun(self, code: str): 61 | return self.agent_executor.arun(code) 62 | -------------------------------------------------------------------------------- /src/codeinterpreter/agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcosx/CodeInterpreter/30cf6e26e18ac4ff83a631484cc829b461dc731b/src/codeinterpreter/agent/__init__.py -------------------------------------------------------------------------------- /src/codeinterpreter/agent/openai_functions_agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcosx/CodeInterpreter/30cf6e26e18ac4ff83a631484cc829b461dc731b/src/codeinterpreter/agent/openai_functions_agent/__init__.py -------------------------------------------------------------------------------- /src/codeinterpreter/agent/openai_functions_agent/base.py: -------------------------------------------------------------------------------- 1 | from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent 2 | 3 | import json 4 | from json import JSONDecodeError 5 | from typing import Union 6 | 7 | 8 | from langchain.schema import ( 9 | AgentAction, 10 | AgentFinish, 11 | OutputParserException, 12 | ) 13 | from langchain.schema.messages import ( 14 | AIMessage, 15 | BaseMessage, 16 | ) 17 | 18 | from typing import Any, List, Tuple, Union, Dict 19 | 20 | 21 | from langchain.agents.openai_functions_agent.base import ( 22 | OpenAIFunctionsAgent, 23 | _FunctionsAgentAction, 24 | _format_intermediate_steps, 25 | ) 26 | from langchain.schema import ( 27 | AgentAction, 28 | AgentFinish, 29 | AIMessage, 30 | BaseMessage, 31 | ) 32 | from typing import Union 33 | from json import JSONDecodeError 34 | 35 | 36 | from langchain.callbacks.manager import Callbacks 37 | 38 | 39 | def _parse_ai_message(message: BaseMessage) -> Union[AgentAction, AgentFinish]: 40 | """Parse an AI message.""" 41 | if not isinstance(message, AIMessage): 42 | raise TypeError(f"Expected an AI message got {type(message)}") 43 | 44 | function_call = message.additional_kwargs.get("function_call", {}) 45 | 46 | if function_call: 47 | function_name = function_call["name"] 48 | try: 49 | _tool_input = json.loads(function_call["arguments"]) 50 | except JSONDecodeError: 51 | _tool_input = function_call["arguments"] 52 | # HACK HACK HACK: 53 | # The code that encodes tool input into Open AI uses a special variable 54 | # name called `__arg1` to handle old style tools that do not expose a 55 | # schema and expect a single string argument as an input. 56 | # We unpack the argument here if it exists. 57 | # Open AI does not support passing in a JSON array as an argument. 58 | if "__arg1" in _tool_input: 59 | tool_input = _tool_input["__arg1"] 60 | else: 61 | tool_input = _tool_input 62 | 63 | content_msg = "responded: {content}\n" if message.content else "\n" 64 | 65 | return _FunctionsAgentAction( 66 | tool=function_name, 67 | tool_input=tool_input, 68 | log=f"\nInvoking: `{function_name}` with `{tool_input}`\n{content_msg}\n", 69 | message_log=[message], 70 | ) 71 | 72 | return AgentFinish(return_values={"output": message.content}, log=message.content) 73 | 74 | 75 | class CustomOpenAIFunctionsAgent(OpenAIFunctionsAgent): 76 | """ 77 | https://github.com/langchain-ai/langchain/issues/6364 78 | """ 79 | 80 | def plan( 81 | self, 82 | intermediate_steps: List[Tuple[AgentAction, str]], 83 | callbacks: Callbacks = None, 84 | with_functions: bool = True, 85 | **kwargs: Any, 86 | ) -> Union[AgentAction, AgentFinish]: 87 | """Given input, decided what to do. 88 | 89 | Args: 90 | intermediate_steps: Steps the LLM has taken to date, along with observations 91 | **kwargs: User inputs. 92 | 93 | Returns: 94 | Action specifying what tool to use. 95 | """ 96 | agent_scratchpad = _format_intermediate_steps(intermediate_steps) 97 | selected_inputs = { 98 | k: kwargs[k] for k in self.prompt.input_variables if k != "agent_scratchpad" 99 | } 100 | full_inputs = dict(**selected_inputs, agent_scratchpad=agent_scratchpad) 101 | prompt = self.prompt.format_prompt(**full_inputs) 102 | messages = prompt.to_messages() 103 | if with_functions: 104 | predicted_message = self.llm.predict_messages( 105 | messages, 106 | functions=self.functions, 107 | callbacks=callbacks, 108 | ) 109 | else: 110 | predicted_message = self.llm.predict_messages( 111 | messages, 112 | callbacks=callbacks, 113 | ) 114 | agent_decision = _parse_ai_message(predicted_message) 115 | return agent_decision 116 | 117 | async def aplan( 118 | self, 119 | intermediate_steps: List[Tuple[AgentAction, str]], 120 | callbacks: Callbacks = None, 121 | **kwargs: Any, 122 | ) -> Union[AgentAction, AgentFinish]: 123 | """Given input, decided what to do. 124 | 125 | Args: 126 | intermediate_steps: Steps the LLM has taken to date, 127 | along with observations 128 | **kwargs: User inputs. 129 | 130 | Returns: 131 | Action specifying what tool to use. 132 | """ 133 | agent_scratchpad = _format_intermediate_steps(intermediate_steps) 134 | selected_inputs = { 135 | k: kwargs[k] for k in self.prompt.input_variables if k != "agent_scratchpad" 136 | } 137 | full_inputs = dict(**selected_inputs, agent_scratchpad=agent_scratchpad) 138 | prompt = self.prompt.format_prompt(**full_inputs) 139 | messages = prompt.to_messages() 140 | predicted_message = await self.llm.apredict_messages( 141 | messages, functions=self.functions, callbacks=callbacks 142 | ) 143 | agent_decision = _parse_ai_message(predicted_message) 144 | return agent_decision 145 | -------------------------------------------------------------------------------- /src/codeinterpreter/core.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from codeinterpreter.sandbox.manager import SandboxManager 3 | from codeinterpreter.sandbox.factory import get_sandbox_manager 4 | 5 | 6 | class CodeInterpreter: 7 | def __init__(self) -> None: 8 | self.sandbox_manager: Optional[SandboxManager] = None 9 | 10 | def init(self, sandbox_manager: SandboxManager = get_sandbox_manager()): 11 | self.sandbox_manager = sandbox_manager 12 | self.sandbox_manager.init() 13 | 14 | async def ainit(self, sandbox_manager: SandboxManager = get_sandbox_manager()): 15 | self.sandbox_manager = sandbox_manager 16 | await self.sandbox_manager.ainit() 17 | -------------------------------------------------------------------------------- /src/codeinterpreter/sandbox/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcosx/CodeInterpreter/30cf6e26e18ac4ff83a631484cc829b461dc731b/src/codeinterpreter/sandbox/__init__.py -------------------------------------------------------------------------------- /src/codeinterpreter/sandbox/docker_jupyter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcosx/CodeInterpreter/30cf6e26e18ac4ff83a631484cc829b461dc731b/src/codeinterpreter/sandbox/docker_jupyter/__init__.py -------------------------------------------------------------------------------- /src/codeinterpreter/sandbox/factory.py: -------------------------------------------------------------------------------- 1 | from codeinterpreter.sandbox.local_jupyter.manager import LocalJupyterManager 2 | from codeinterpreter.sandbox.manager import SandboxManager 3 | 4 | 5 | def get_sandbox_manager() -> SandboxManager: 6 | return LocalJupyterManager() 7 | -------------------------------------------------------------------------------- /src/codeinterpreter/sandbox/kubernetes_jupyter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcosx/CodeInterpreter/30cf6e26e18ac4ff83a631484cc829b461dc731b/src/codeinterpreter/sandbox/kubernetes_jupyter/__init__.py -------------------------------------------------------------------------------- /src/codeinterpreter/sandbox/local_jupyter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcosx/CodeInterpreter/30cf6e26e18ac4ff83a631484cc829b461dc731b/src/codeinterpreter/sandbox/local_jupyter/__init__.py -------------------------------------------------------------------------------- /src/codeinterpreter/sandbox/local_jupyter/manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import shutil 4 | import signal 5 | import subprocess 6 | import time 7 | from asyncio.subprocess import Process 8 | from typing import Dict, List, Union 9 | 10 | import aiohttp 11 | import requests 12 | 13 | from codeinterpreter.sandbox.local_jupyter.sandbox import LocalJupyterSandbox 14 | from codeinterpreter.sandbox.manager import SandboxManager 15 | from codeinterpreter.sandbox.schema import SandboxResponse 16 | from codeinterpreter.utils.log import codeinterpreter_log 17 | 18 | 19 | class LocalJupyterManager(SandboxManager): 20 | def __init__(self, workdir: str = ".sandbox", port: int = 8888) -> None: 21 | super().__init__() 22 | self.session: Union[requests.Session, aiohttp.ClientSession] = None 23 | self.subprocess: Union[Process, subprocess.Popen, None] = None 24 | self.workdir = workdir 25 | self.port = port 26 | self.sandboxs:Dict[str,LocalJupyterSandbox] = {} 27 | 28 | def init(self) -> None: 29 | self.session = requests.Session() 30 | 31 | workdir = os.path.abspath(self.workdir) 32 | os.makedirs(workdir, exist_ok=True) 33 | codeinterpreter_log.debug("Starting kernelgateway...") 34 | 35 | env = os.environ.copy() 36 | env["JUPYTER_DATA_DIR"] = workdir 37 | 38 | with open( 39 | os.path.join(workdir, "jupyter-kernelgateway.log"), "w", encoding="utf-8" 40 | ) as subprocess_log_file: 41 | self.subprocess = subprocess.Popen( 42 | [ 43 | "jupyter", 44 | "kernelgateway", 45 | "--KernelGatewayApp.ip='0.0.0.0'", 46 | f"--KernelGatewayApp.port={self.port}", 47 | "--JupyterWebsocketPersonality.list_kernels=true", 48 | "--KernelGatewayApp.log_level=DEBUG", 49 | "--JupyterWebsocketPersonality.env_whitelist JUPYTER_DATA_DIR", 50 | "--KernelGatewayApp.kernel_manager_class=notebook.services.kernels.kernelmanager.AsyncMappingKernelManager", 51 | ], 52 | stdout=subprocess_log_file, 53 | stderr=subprocess_log_file, 54 | cwd=workdir, 55 | env=env, 56 | ) 57 | codeinterpreter_log.debug("Starting jupyter kernelgateway pid %s",self.subprocess.pid) 58 | with open( 59 | os.path.join(workdir, f"{self.subprocess.pid}.pid"), 60 | "w", 61 | encoding="utf-8" 62 | ) as p: 63 | p.write("kernel") 64 | 65 | while True: 66 | try: 67 | response = self.session.get(self.base_http_url) 68 | if response.status_code == 200: 69 | codeinterpreter_log.debug("kernelgateway start success!") 70 | break 71 | except requests.exceptions.ConnectionError: 72 | pass 73 | codeinterpreter_log.debug("Waiting for kernelgateway to start...") 74 | time.sleep(1) 75 | 76 | 77 | 78 | async def ainit(self) -> None: 79 | self.session = aiohttp.ClientSession() 80 | 81 | workdir = os.path.abspath(self.workdir) 82 | os.makedirs(workdir, exist_ok=True) 83 | codeinterpreter_log.debug("Starting kernelgateway...") 84 | 85 | env = os.environ.copy() 86 | env["JUPYTER_DATA_DIR"] = workdir 87 | 88 | with open( 89 | os.path.join(workdir, "jupyter-kernelgateway.log"), "w", encoding="utf-8" 90 | ) as subprocess_log_file: 91 | self.subprocess = await asyncio.create_subprocess_exec( 92 | "jupyter", 93 | "kernelgateway", 94 | "--KernelGatewayApp.ip='0.0.0.0'", 95 | f"--KernelGatewayApp.port={self.port}", 96 | "--JupyterWebsocketPersonality.list_kernels=true", 97 | "--KernelGatewayApp.log_level=DEBUG", 98 | "--JupyterWebsocketPersonality.env_whitelist JUPYTER_DATA_DIR", 99 | "--KernelGatewayApp.kernel_manager_class=notebook.services.kernels.kernelmanager.AsyncMappingKernelManager", 100 | stdout=subprocess_log_file, 101 | stderr=subprocess_log_file, 102 | cwd=workdir, 103 | env=env, 104 | ) 105 | codeinterpreter_log.debug("Starting jupyter kernelgateway pid %s",self.subprocess.pid) 106 | with open( 107 | os.path.join(workdir, f"{self.subprocess.pid}.pid"), 108 | "w", 109 | encoding="utf-8" 110 | ) as p: 111 | p.write("kernel") 112 | 113 | while True: 114 | try: 115 | response = await self.session.get(self.base_http_url) 116 | if response.status == 200: 117 | break 118 | except aiohttp.ClientConnectorError: 119 | pass 120 | except aiohttp.ServerDisconnectedError: 121 | pass 122 | await asyncio.sleep(1) 123 | 124 | codeinterpreter_log.debug("kernelgateway start success!") 125 | 126 | def list(self) -> List[LocalJupyterSandbox]: 127 | return list(self.sandboxs.values()) 128 | 129 | async def alist(self) -> List[LocalJupyterSandbox]: 130 | return list(self.sandboxs.values()) 131 | 132 | def start(self) -> LocalJupyterSandbox: 133 | response = self.session.post(f"{self.base_http_url}/kernels", json={}) 134 | kernel_id = response.json()["id"] 135 | ws_url = f"{self.base_websocket_url}/kernels/{kernel_id}/channels" 136 | 137 | sandbox = LocalJupyterSandbox.sync_init( 138 | id=kernel_id, ws_url=ws_url, workdir=f"{self.workdir}/{kernel_id}" 139 | ) 140 | 141 | self.sandboxs[kernel_id] = sandbox 142 | 143 | return sandbox 144 | 145 | async def astart(self) -> LocalJupyterSandbox: 146 | response = await self.session.post(f"{self.base_http_url}/kernels", json={}) 147 | data = await response.json() 148 | kernel_id = data["id"] 149 | ws_url = f"{self.base_websocket_url}/kernels/{kernel_id}/channels" 150 | 151 | sandbox = await LocalJupyterSandbox.async_init( 152 | id=kernel_id, ws_url=ws_url, workdir=f"{self.workdir}/{kernel_id}" 153 | ) 154 | 155 | self.sandboxs[kernel_id] = sandbox 156 | 157 | return sandbox 158 | 159 | def get(self, id: str) -> LocalJupyterSandbox: 160 | return self.sandboxs[id] 161 | 162 | async def aget(self, id: str) -> LocalJupyterSandbox: 163 | return self.sandboxs[id] 164 | 165 | 166 | def restart(self, id: str) -> SandboxResponse: 167 | codeinterpreter_log.debug("restart kernel %s",id) 168 | response = self.session.post(f"{self.base_http_url}/kernels/{id}/restart", json={}) 169 | if response.status_code == 200: 170 | self.sandboxs[id].reconnect() 171 | return SandboxResponse(content="success") 172 | else: 173 | return SandboxResponse(content="restart failed") 174 | 175 | async def arestart(self, id: str) -> SandboxResponse: 176 | codeinterpreter_log.debug("restart kernel %s",id) 177 | response = await self.session.post(f"{self.base_http_url}/kernels/{id}/restart", json={}) 178 | if response.status_code == 200: 179 | await self.sandboxs[id].areconnect() 180 | return SandboxResponse(content="success") 181 | else: 182 | return SandboxResponse(content="restart failed") 183 | 184 | def delete(self, id: str): 185 | codeinterpreter_log.debug("delete kernel %s",id) 186 | self.sandboxs[id].close_websocket() 187 | response = self.session.delete(f"{self.base_http_url}/kernels/{id}", json={}) 188 | if response.status_code <= 400: 189 | self.sandboxs.pop(id) 190 | return SandboxResponse(content="success") 191 | 192 | async def adelete(self, id: str): 193 | codeinterpreter_log.debug("delete kernel %s",id) 194 | await self.sandboxs[id].aclose_websocket() 195 | response = await self.session.delete(f"{self.base_http_url}/kernels/{id}", json={}) 196 | if response.status <= 400: 197 | self.sandboxs.pop(id) 198 | return SandboxResponse(content="success") 199 | 200 | def stop(self) -> SandboxResponse: 201 | codeinterpreter_log.debug("Begin stop sandbox manager") 202 | for sandbox_id in list(self.sandboxs.keys()): 203 | self.delete(sandbox_id) 204 | if self.subprocess is not None: 205 | self.subprocess.send_signal(signal.SIGINT) 206 | try: 207 | self.subprocess.wait(30) 208 | except subprocess.TimeoutExpired: 209 | self.subprocess.kill() 210 | finally: 211 | self.subprocess = None 212 | if self.session is not None: 213 | self.session.close() 214 | self.session = None 215 | shutil.rmtree(self.workdir, ignore_errors=True, onerror=None) 216 | codeinterpreter_log.debug("sandbox manager stoped") 217 | 218 | async def astop(self) -> SandboxResponse: 219 | codeinterpreter_log.debug("Begin stop sandbox manager") 220 | if self.sandboxs: 221 | await asyncio.gather(*(self.adelete(id) for id in list(self.sandboxs.keys()))) 222 | if self.subprocess is not None: 223 | self.subprocess.send_signal(signal.SIGINT) 224 | try: 225 | await self.subprocess.wait() 226 | except asyncio.TimeoutError: 227 | self.subprocess.kill() 228 | finally: 229 | self.subprocess = None 230 | 231 | if self.session is not None: 232 | await self.session.close() 233 | self.session = None 234 | shutil.rmtree(self.workdir, ignore_errors=True, onerror=None) 235 | codeinterpreter_log.debug("sandbox manager stoped") 236 | 237 | @property 238 | def base_http_url(self) -> str: 239 | return f"http://0.0.0.0:{self.port}/api" 240 | 241 | @property 242 | def base_websocket_url(self) -> str: 243 | return f"ws://0.0.0.0:{self.port}/api" 244 | -------------------------------------------------------------------------------- /src/codeinterpreter/sandbox/local_jupyter/sandbox.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import os 4 | import time 5 | from typing import Union 6 | from uuid import uuid4 7 | 8 | from websockets.client import WebSocketClientProtocol 9 | from websockets.client import connect as ws_connect 10 | from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK 11 | from websockets.sync.client import ClientConnection 12 | from websockets.sync.client import connect as ws_sync_connect 13 | 14 | from codeinterpreter.sandbox.sandbox import Sandbox 15 | from codeinterpreter.sandbox.schema import ( 16 | SandboxFile, 17 | SandboxResponse, 18 | SandboxRunOutput, 19 | ) 20 | from codeinterpreter.utils.log import codeinterpreter_log 21 | 22 | WEBSOCKETS_RECONNECT_MAX_RETRIES = 3 23 | WEBSOCKETS_RECONNECT_RETRY_DELAY = 5 24 | 25 | 26 | class LocalJupyterSandbox(Sandbox): 27 | def __init__( 28 | self, 29 | id: str = None, 30 | ws_url: str = None, 31 | ws: Union[ClientConnection, WebSocketClientProtocol, None] = None, 32 | workdir: str = None, 33 | ) -> None: 34 | super().__init__() 35 | self.type = "local_jupyter" 36 | self.id = id 37 | self.ws_url = ws_url 38 | self.ws = ws 39 | self.workdir = workdir 40 | 41 | @classmethod 42 | def sync_init(cls, id: str = None, ws_url: str = None, workdir: str = None): 43 | ws = ws_sync_connect(ws_url) 44 | 45 | return cls(id=id, ws_url=ws_url, ws=ws, workdir=workdir) 46 | 47 | @classmethod 48 | async def async_init(cls, id: str = None, ws_url: str = None, workdir: str = None): 49 | ws = await ws_connect(ws_url) 50 | return cls(id=id, ws_url=ws_url, ws=ws, workdir=workdir) 51 | 52 | def run(self, code: Union[str, os.PathLike]) -> SandboxRunOutput: 53 | if not code: 54 | raise ValueError("Code or Code file path must be specified one.") 55 | if type(code) is os.PathLike: 56 | with open(code, "r") as f: 57 | code = f.read() 58 | codeinterpreter_log.debug(f"Running code:{code}") 59 | retries = 0 60 | while retries < WEBSOCKETS_RECONNECT_MAX_RETRIES: 61 | try: 62 | msg_id = self.send_code_to_sandbox(code=code) 63 | return self.get_sandbox_output(msg_id) 64 | except ConnectionClosedError or ConnectionClosedOK: 65 | codeinterpreter_log.warning("reconnect websocket...") 66 | self.reconnect() 67 | time.sleep(WEBSOCKETS_RECONNECT_RETRY_DELAY) 68 | retries += 1 69 | except Exception as exception: 70 | codeinterpreter_log.error("websocket get exception") 71 | raise exception 72 | 73 | return SandboxRunOutput( 74 | type="error", content="Failed to execute code after multiple retries." 75 | ) 76 | 77 | async def arun(self, code: Union[str, os.PathLike]) -> SandboxRunOutput: 78 | if not code: 79 | raise ValueError("Code or Code file path must be specified one.") 80 | if type(code) is os.PathLike: 81 | with open(code, "r") as f: 82 | code = await f.read() 83 | codeinterpreter_log.debug(f"Running code:{code}") 84 | retries = 0 85 | while retries < WEBSOCKETS_RECONNECT_MAX_RETRIES: 86 | try: 87 | msg_id = await self.asend_code_to_sandbox(code=code) 88 | return await self.aget_sandbox_output(msg_id) 89 | except ConnectionClosedError or ConnectionClosedOK: 90 | codeinterpreter_log.warning("reconnect websocket...") 91 | await self.areconnect() 92 | time.sleep(WEBSOCKETS_RECONNECT_RETRY_DELAY) 93 | retries += 1 94 | except Exception as exception: 95 | codeinterpreter_log.error("websocket get exception") 96 | raise exception 97 | 98 | return SandboxRunOutput( 99 | type="error", content="Failed to execute code after multiple retries." 100 | ) 101 | 102 | def upload(self, file_name: str, content: bytes) -> SandboxResponse: 103 | os.makedirs(self.workdir, exist_ok=True) 104 | with open(os.path.join(self.workdir, file_name), "wb") as file: 105 | file.write(content) 106 | 107 | return SandboxResponse(content=f"{file_name} uploaded successfully") 108 | 109 | async def aupload(self, file_name: str, content: bytes) -> SandboxResponse: 110 | return await asyncio.get_running_loop().run_in_executor( 111 | None, self.upload, file_name, content 112 | ) 113 | 114 | def download(self, file_name: str) -> SandboxFile: 115 | with open(os.path.join(self.workdir, file_name), "rb") as file: 116 | content = file.read() 117 | 118 | return SandboxFile(name=file_name, content=content) 119 | 120 | async def adownload(self, file_name: str) -> SandboxFile: 121 | return await asyncio.get_running_loop().run_in_executor( 122 | None, self.download, file_name 123 | ) 124 | 125 | def close_websocket(self) -> None: 126 | self.ws.close() 127 | 128 | async def aclose_websocket(self) -> None: 129 | await self.ws.close() 130 | 131 | def reconnect(self) -> None: 132 | self.ws = ws_sync_connect(self.ws_url) 133 | 134 | async def areconnect(self) -> None: 135 | self.ws = await ws_connect(self.ws_url) 136 | 137 | def send_code_to_sandbox(self, code: str) -> str: 138 | msg_id = uuid4().hex 139 | 140 | self.ws.send( 141 | json.dumps( 142 | { 143 | "header": { 144 | "msg_id": msg_id, 145 | "msg_type": "execute_request", 146 | }, 147 | "parent_header": {}, 148 | "metadata": {}, 149 | "content": { 150 | "code": code, 151 | "silent": False, 152 | "store_history": True, 153 | "user_expressions": {}, 154 | "allow_stdin": False, 155 | "stop_on_error": True, 156 | }, 157 | "channel": "shell", 158 | "buffers": [], 159 | } 160 | ) 161 | ) 162 | 163 | return msg_id 164 | 165 | async def asend_code_to_sandbox(self, code: str) -> str: 166 | msg_id = uuid4().hex 167 | 168 | await self.ws.send( 169 | json.dumps( 170 | { 171 | "header": { 172 | "msg_id": msg_id, 173 | "msg_type": "execute_request", 174 | }, 175 | "parent_header": {}, 176 | "metadata": {}, 177 | "content": { 178 | "code": code, 179 | "silent": False, 180 | "store_history": True, 181 | "user_expressions": {}, 182 | "allow_stdin": False, 183 | "stop_on_error": True, 184 | }, 185 | "channel": "shell", 186 | "buffers": [], 187 | } 188 | ) 189 | ) 190 | 191 | return msg_id 192 | 193 | def get_sandbox_output(self, msg_id: str) -> SandboxRunOutput: 194 | result = "" 195 | 196 | while True: 197 | received_msg = json.loads(self.ws.recv()) 198 | if ( 199 | received_msg["header"]["msg_type"] == "stream" 200 | and received_msg["parent_header"]["msg_id"] == msg_id 201 | ): 202 | msg = received_msg["content"]["text"].strip() 203 | codeinterpreter_log.debug("Received [stream] %s" % msg) 204 | result += msg 205 | elif ( 206 | received_msg["header"]["msg_type"] == "execute_result" 207 | and received_msg["parent_header"]["msg_id"] == msg_id 208 | ): 209 | msg = received_msg["content"]["data"]["text/plain"].strip() 210 | codeinterpreter_log.debug("Received [execute_result] output %s" % msg) 211 | result += msg 212 | elif received_msg["header"]["msg_type"] == "display_data": 213 | if "image/png" in received_msg["content"]["data"]: 214 | codeinterpreter_log.debug( 215 | "Received [display_data] image/png output" 216 | ) 217 | return SandboxRunOutput( 218 | type="image/png", 219 | content=received_msg["content"]["data"]["image/png"], 220 | ) 221 | 222 | elif "text/plain" in received_msg["content"]["data"]: 223 | codeinterpreter_log.debug( 224 | "Received [display_data] text/plain output" 225 | ) 226 | return SandboxRunOutput( 227 | type="text", 228 | content=received_msg["content"]["data"]["text/plain"], 229 | ) 230 | elif ( 231 | received_msg["header"]["msg_type"] == "status" 232 | and received_msg["parent_header"]["msg_id"] == msg_id 233 | and received_msg["content"]["execution_state"] == "idle" 234 | ): 235 | codeinterpreter_log.debug( 236 | "Received [status] idle, return the result %s" % result 237 | ) 238 | return SandboxRunOutput( 239 | type="text", content=result or "code run successfully (no output)" 240 | ) 241 | 242 | elif ( 243 | received_msg["header"]["msg_type"] == "error" 244 | and received_msg["parent_header"]["msg_id"] == msg_id 245 | ): 246 | error = f"{received_msg['content']['ename']}: {received_msg['content']['evalue']}" 247 | codeinterpreter_log.debug( 248 | "Received [error], return the error %s" % error 249 | ) 250 | return SandboxRunOutput(type="error", content=error) 251 | 252 | async def aget_sandbox_output(self, msg_id: str) -> SandboxRunOutput: 253 | result = "" 254 | 255 | while True: 256 | received_msg = json.loads(await self.ws.recv()) 257 | 258 | if ( 259 | received_msg["header"]["msg_type"] == "stream" 260 | and received_msg["parent_header"]["msg_id"] == msg_id 261 | ): 262 | msg = received_msg["content"]["text"].strip() 263 | codeinterpreter_log.debug("Received [stream] %s" % msg) 264 | result += msg 265 | elif ( 266 | received_msg["header"]["msg_type"] == "execute_result" 267 | and received_msg["parent_header"]["msg_id"] == msg_id 268 | ): 269 | msg = received_msg["content"]["data"]["text/plain"].strip() 270 | codeinterpreter_log.debug("Received [execute_result] output %s" % msg) 271 | result += msg 272 | elif received_msg["header"]["msg_type"] == "display_data": 273 | if "image/png" in received_msg["content"]["data"]: 274 | codeinterpreter_log.debug( 275 | "Received [display_data] image/png output" 276 | ) 277 | return SandboxRunOutput( 278 | type="image/png", 279 | content=received_msg["content"]["data"]["image/png"], 280 | ) 281 | 282 | elif "text/plain" in received_msg["content"]["data"]: 283 | codeinterpreter_log.debug( 284 | "Received [display_data] text/plain output" 285 | ) 286 | return SandboxRunOutput( 287 | type="text", 288 | content=received_msg["content"]["data"]["text/plain"], 289 | ) 290 | elif ( 291 | received_msg["header"]["msg_type"] == "status" 292 | and received_msg["parent_header"]["msg_id"] == msg_id 293 | and received_msg["content"]["execution_state"] == "idle" 294 | ): 295 | codeinterpreter_log.debug( 296 | "Received [status] idle, return the result %s" % result 297 | ) 298 | return SandboxRunOutput( 299 | type="text", content=result or "code run successfully (no output)" 300 | ) 301 | 302 | elif ( 303 | received_msg["header"]["msg_type"] == "error" 304 | and received_msg["parent_header"]["msg_id"] == msg_id 305 | ): 306 | error = f"{received_msg['content']['ename']}: {received_msg['content']['evalue']}" 307 | codeinterpreter_log.debug( 308 | "Received [error], return the error %s" % error 309 | ) 310 | return SandboxRunOutput(type="error", content=error) 311 | -------------------------------------------------------------------------------- /src/codeinterpreter/sandbox/manager.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List 3 | 4 | from codeinterpreter.sandbox.sandbox import Sandbox 5 | from codeinterpreter.sandbox.schema import SandboxResponse 6 | 7 | 8 | class SandboxManager(ABC): 9 | @abstractmethod 10 | def init(self): 11 | ... 12 | 13 | @abstractmethod 14 | async def ainit(self): 15 | ... 16 | 17 | @abstractmethod 18 | def list(self) -> SandboxResponse: 19 | ... 20 | 21 | @abstractmethod 22 | async def alist(self) -> List[SandboxResponse]: 23 | ... 24 | 25 | @abstractmethod 26 | def start(self) -> Sandbox: 27 | ... 28 | 29 | @abstractmethod 30 | async def astart(self) -> Sandbox: 31 | ... 32 | 33 | @abstractmethod 34 | def get(self, id: str) -> Sandbox: 35 | ... 36 | 37 | @abstractmethod 38 | async def aget(self, id: str) -> Sandbox: 39 | ... 40 | 41 | @abstractmethod 42 | def restart(self, id: str) -> SandboxResponse: 43 | ... 44 | 45 | @abstractmethod 46 | async def arestart(self, id: str) -> SandboxResponse: 47 | ... 48 | 49 | @abstractmethod 50 | def delete(self, id: str): 51 | ... 52 | 53 | @abstractmethod 54 | async def adelete(self, id: str): 55 | ... 56 | 57 | @abstractmethod 58 | def stop(self): 59 | ... 60 | 61 | @abstractmethod 62 | async def astop(self): 63 | ... 64 | -------------------------------------------------------------------------------- /src/codeinterpreter/sandbox/sandbox.py: -------------------------------------------------------------------------------- 1 | import os 2 | from abc import ABC, abstractmethod 3 | from typing import Union 4 | 5 | from codeinterpreter.sandbox.schema import ( 6 | SandboxFile, 7 | SandboxResponse, 8 | SandboxRunOutput, 9 | ) 10 | 11 | 12 | class Sandbox(ABC): 13 | id: str 14 | type: str 15 | 16 | @abstractmethod 17 | def run(self, code: Union[str, os.PathLike]) -> SandboxRunOutput: 18 | ... 19 | 20 | @abstractmethod 21 | async def arun(self, code: Union[str, os.PathLike]) -> SandboxRunOutput: 22 | ... 23 | 24 | @abstractmethod 25 | def upload(self, file_name: str, content: bytes) -> SandboxResponse: 26 | ... 27 | 28 | @abstractmethod 29 | async def aupload(self, file_name: str, content: bytes) -> SandboxResponse: 30 | ... 31 | 32 | @abstractmethod 33 | def download(self, file_name: str) -> SandboxFile: 34 | ... 35 | 36 | @abstractmethod 37 | async def adownload(self, file_name: str) -> SandboxFile: 38 | ... 39 | -------------------------------------------------------------------------------- /src/codeinterpreter/sandbox/schema.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class SandboxResponse(BaseModel): 7 | content: str 8 | 9 | 10 | class SandboxRunOutput(BaseModel): 11 | type: str 12 | content: str 13 | 14 | 15 | class SandboxFile(BaseModel): 16 | name: str 17 | content: Optional[bytes] = None 18 | 19 | 20 | class SandboxRunConfig(BaseModel): 21 | retry: int = 3 22 | -------------------------------------------------------------------------------- /src/codeinterpreter/session.py: -------------------------------------------------------------------------------- 1 | from codeinterpreter.sandbox.sandbox import Sandbox 2 | from codeinterpreter.adapter.adapter import Adapter 3 | 4 | 5 | class CodeInterpreterSession: 6 | def __init__(self) -> None: 7 | self.sandbox: Sandbox = None 8 | self.adapter: Adapter = None 9 | -------------------------------------------------------------------------------- /src/codeinterpreter/utils/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["import_openai"] 2 | 3 | import importlib.util 4 | from typing import Optional 5 | 6 | from codeinterpreter.utils.dependency import pip_install 7 | 8 | 9 | def _check_library(libname: str, prompt: bool = True, package: Optional[str] = None): 10 | is_avail = False 11 | if importlib.util.find_spec(libname): 12 | is_avail = True 13 | if not is_avail and prompt: 14 | pip_install(package if package else libname) 15 | return is_avail 16 | 17 | 18 | def import_openai(): 19 | _check_library("openai") 20 | -------------------------------------------------------------------------------- /src/codeinterpreter/utils/dependency.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from codeinterpreter.utils.error import PipInstallError 4 | from codeinterpreter.utils.log import codeinterpreter_log 5 | 6 | 7 | def pip_install(package: str, warn: bool = False): 8 | """ 9 | Use subprocess execute `pip install` 10 | """ 11 | cmd = f"pip install -q {package}" 12 | try: 13 | if warn and input(f"Install {package}? Y/n:") != "Y": 14 | raise ModuleNotFoundError(f"No module named {package}") 15 | print(f"start to install package: {package}") 16 | subprocess.check_call(cmd, shell=True) 17 | print(f"successfully installed package: {package}") 18 | codeinterpreter_log.info("%s installed successfully!", package) 19 | 20 | except subprocess.CalledProcessError as e: 21 | raise PipInstallError(package) from e 22 | -------------------------------------------------------------------------------- /src/codeinterpreter/utils/error.py: -------------------------------------------------------------------------------- 1 | class CodeInterpreterError(Exception): 2 | """CodeInterpreter base error""" 3 | 4 | 5 | class PipInstallError(CodeInterpreterError): 6 | """Raise when failed to install package.""" 7 | 8 | def __init__(self, package: str): 9 | super().__init__(f"Run into error installing {package}.") 10 | 11 | 12 | class SandboxRunMaxRetryError(CodeInterpreterError): 13 | def __init__(self): 14 | super().__init__("Sandbox run has reached the maximum number of attempts.") 15 | 16 | 17 | class PythonPackageNotFoundError(CodeInterpreterError): 18 | def __init__(self, package: str, hint: str): 19 | super().__init__(f"Python package {package} not found.{hint}") 20 | 21 | 22 | def wrap_error(e: Exception) -> Exception: 23 | """Add a type to exception `e` while ensuring that the original type is not changed""" 24 | 25 | e.__class__ = type(e.__class__.__name__, (CodeInterpreterError, e.__class__), {}) 26 | return e 27 | -------------------------------------------------------------------------------- /src/codeinterpreter/utils/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import codeinterpreter 4 | 5 | FORMAT = "%(asctime)s - %(thread)d - %(filename)s-%(module)s:%(lineno)s - %(levelname)s: %(message)s" 6 | logging.basicConfig(format=FORMAT) 7 | 8 | codeinterpreter_log = logging.getLogger(f"codeinterpreter:{codeinterpreter.__version__}") 9 | -------------------------------------------------------------------------------- /src/codeinterpreter_daemon/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcosx/CodeInterpreter/30cf6e26e18ac4ff83a631484cc829b461dc731b/src/codeinterpreter_daemon/__init__.py -------------------------------------------------------------------------------- /tests/unit/sandbox/test_local_jupyter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import tempfile 4 | from pathlib import Path 5 | 6 | import psutil 7 | 8 | from codeinterpreter.sandbox.local_jupyter.manager import LocalJupyterManager 9 | 10 | # tips: clear all jupyter thread 11 | # ps aux | grep jupyter | grep -v grep | awk '{ print $2 }' | xargs kill -9 12 | 13 | 14 | def test_manager_init(): 15 | manager = LocalJupyterManager() 16 | manager.init() 17 | 18 | assert os.path.exists(".sandbox") 19 | assert manager.subprocess is not None 20 | assert psutil.pid_exists(manager.subprocess.pid) 21 | assert os.path.exists(".sandbox/jupyter-kernelgateway.log") 22 | manager.stop() 23 | 24 | 25 | def test_manager_ainit(): 26 | manager = LocalJupyterManager() 27 | 28 | async def run_ainit(): 29 | await manager.ainit() 30 | assert os.path.exists(".sandbox") 31 | assert manager.subprocess is not None 32 | assert os.path.exists(".sandbox/jupyter-kernelgateway.log") 33 | await manager.astop() 34 | 35 | asyncio.run(run_ainit()) 36 | 37 | 38 | def test_sandbox_start(): 39 | manager = LocalJupyterManager() 40 | manager.init() 41 | sandbox = manager.start() 42 | sandbox.ws.recv() 43 | manager.stop() 44 | 45 | 46 | def test_sandbox_astart(): 47 | manager = LocalJupyterManager() 48 | 49 | async def run_astart(): 50 | await manager.ainit() 51 | sandbox = await manager.astart() 52 | await sandbox.ws.recv() 53 | await manager.astop() 54 | 55 | asyncio.run(run_astart()) 56 | 57 | 58 | def test_sandbox_run(): 59 | manager = LocalJupyterManager() 60 | manager.init() 61 | sandbox = manager.start() 62 | sandbox_output = sandbox.run('print("hello,world")') 63 | assert sandbox_output.content == "hello,world" 64 | assert sandbox_output.type == "text" 65 | 66 | sandbox_output = sandbox.run('print("to the world")') 67 | assert sandbox_output.content == "to the world" 68 | assert sandbox_output.type == "text" 69 | 70 | manager.stop() 71 | 72 | 73 | def test_sandbox_arun(): 74 | manager = LocalJupyterManager() 75 | 76 | async def run_arun(): 77 | await manager.ainit() 78 | sandbox = await manager.astart() 79 | sandbox_output = await sandbox.arun('print("hello,world")') 80 | assert sandbox_output.content == "hello,world" 81 | assert sandbox_output.type == "text" 82 | 83 | sandbox_output = await sandbox.arun('print("to the world")') 84 | assert sandbox_output.content == "to the world" 85 | assert sandbox_output.type == "text" 86 | 87 | await manager.astop() 88 | 89 | asyncio.run(run_arun()) 90 | 91 | 92 | def test_sandbox_upload_download(): 93 | test_file = tempfile.NamedTemporaryFile(delete=False) 94 | test_file.write(b"test_content") 95 | test_file.close() 96 | with open(test_file.name, "rb") as f: 97 | content = f.read() 98 | 99 | manager = LocalJupyterManager() 100 | manager.init() 101 | sandbox = manager.start() 102 | 103 | response = sandbox.upload(Path(test_file.name).name, content) 104 | assert response.content == f"{Path(test_file.name).name} uploaded successfully" 105 | 106 | with open(os.path.join(sandbox.workdir, Path(test_file.name).name), "rb") as file: 107 | assert file.read() == content 108 | 109 | downloaded_file = sandbox.download(Path(test_file.name).name) 110 | assert downloaded_file.name == Path(test_file.name).name 111 | assert downloaded_file.content == content 112 | 113 | manager.stop() 114 | 115 | 116 | def test_sandbox_aupload_adownload(): 117 | test_file = tempfile.NamedTemporaryFile(delete=False) 118 | test_file.write(b"test_content") 119 | test_file.close() 120 | with open(test_file.name, "rb") as f: 121 | content = f.read() 122 | 123 | manager = LocalJupyterManager() 124 | 125 | async def run_aupload(): 126 | await manager.ainit() 127 | sandbox = await manager.astart() 128 | response = await sandbox.aupload(Path(test_file.name).name, content) 129 | assert response.content == f"{Path(test_file.name).name} uploaded successfully" 130 | with open( 131 | os.path.join(sandbox.workdir, Path(test_file.name).name), "rb" 132 | ) as file: 133 | assert file.read() == content 134 | 135 | downloaded_file = await sandbox.adownload(Path(test_file.name).name) 136 | assert downloaded_file.name == Path(test_file.name).name 137 | assert downloaded_file.content == content 138 | 139 | await manager.astop() 140 | 141 | asyncio.run(run_aupload()) 142 | -------------------------------------------------------------------------------- /tests/unit/utils/test_error.py: -------------------------------------------------------------------------------- 1 | from codeinterpreter.utils.error import CodeInterpreterError, PipInstallError 2 | 3 | 4 | def test_error_type(): 5 | pip_install_error = PipInstallError("openai") 6 | assert issubclass(type(pip_install_error), CodeInterpreterError) 7 | -------------------------------------------------------------------------------- /tests/unit/utils/test_log.py: -------------------------------------------------------------------------------- 1 | from codeinterpreter.utils.log import codeinterpreter_log 2 | 3 | 4 | def test_error_type(): 5 | codeinterpreter_log.setLevel("INFO") 6 | codeinterpreter_log.error("codeinterpreter log error.") 7 | codeinterpreter_log.warning("codeinterpreter log warning.") 8 | codeinterpreter_log.info("codeinterpreter log info.") 9 | assert codeinterpreter_log.level == 20 10 | --------------------------------------------------------------------------------