diff --git a/.changeset/lazy-impalas-accept.md b/.changeset/lazy-impalas-accept.md new file mode 100644 index 0000000..cf0eb9f --- /dev/null +++ b/.changeset/lazy-impalas-accept.md @@ -0,0 +1,5 @@ +--- +'@e2b/code-interpreter-template': patch +--- + +Add [Deno kernel](https://docs.deno.com/runtime/reference/cli/jupyter/) diff --git a/js/tests/languages/deno.test.ts b/js/tests/languages/deno.test.ts new file mode 100644 index 0000000..91e4b3a --- /dev/null +++ b/js/tests/languages/deno.test.ts @@ -0,0 +1,80 @@ +import { expect } from 'vitest' + +import { sandboxTest } from '../setup' + +sandboxTest('js simple', async ({ sandbox }) => { + const result = await sandbox.runCode('console.log("Hello, World!")', {language: "deno"}) + + expect(result.logs.stdout.join().trim()).toEqual('Hello, World!') +}) + +sandboxTest('js import', async ({ sandbox }) => { + const result = await sandbox.runCode('import isOdd from "npm:is-odd"\nisOdd(3)', {language: "deno"}) + + expect(result.results[0].text).toEqual('true') +}) + +sandboxTest('js top level await', async ({ sandbox }) => { + const result = await sandbox.runCode(` + async function main() { + return 'Hello, World!' + } + + await main() + `, {language: "deno"}) + expect(result.results[0].text).toEqual('Hello, World!') +}) + +sandboxTest('js es6', async ({ sandbox }) => { + const result = await sandbox.runCode(` + const add = (x, y) => x + y; + add(1, 2)`, {language: "deno"}) + expect(result.results[0].text).toEqual('3') +}) + + +sandboxTest('js context', async ({ sandbox }) => { + await sandbox.runCode('const z = 1', {language: "deno"}) + const result = await sandbox.runCode('z', {language: "deno"}) + expect(result.results[0].text).toEqual('1') +}) + +sandboxTest('js cwd', async ({ sandbox }) => { + const result = await sandbox.runCode('process.cwd()', {language: "deno"}) + expect(result.results[0].text).toEqual('/home/user') + + const ctx = await sandbox.createCodeContext( {cwd: '/home', language: "deno"}) + const result2 = await sandbox.runCode('process.cwd()', {context: ctx}) + expect(result2.results[0].text).toEqual('/home') +}) + +sandboxTest('ts simple', async ({ sandbox }) => { + const result = await sandbox.runCode(` +function subtract(x: number, y: number): number { + return x - y; +} + +subtract(1, 2) +`, {language: "deno"}) + + expect(result.results[0].text).toEqual('-1') +}) + +sandboxTest('test display', async ({ sandbox }) => { + const result = await sandbox.runCode(` + { + [Symbol.for("Jupyter.display")]() { + return { + // Plain text content + "text/plain": "Hello world!", + + // HTML output + "text/html": "

Hello world!

", + } + } +} +`, {language: "deno"}) + + expect(result.results[0].html).toBe('

Hello world!

') + expect(result.results[0].text).toBe('Hello world!') +}) diff --git a/python/tests/languages/test_deno.py b/python/tests/languages/test_deno.py new file mode 100644 index 0000000..0a0e58e --- /dev/null +++ b/python/tests/languages/test_deno.py @@ -0,0 +1,87 @@ +from e2b_code_interpreter import AsyncSandbox + + +async def test_javascript(async_sandbox: AsyncSandbox): + code = """ + console.log('Hello, World!') + """ + execution = await async_sandbox.run_code(code, language="deno") + assert execution.logs.stdout == ["Hello, World!\n"] + + +async def test_import(async_sandbox: AsyncSandbox): + code = """ + import isOdd from 'npm:is-odd' + isOdd(3) + """ + execution = await async_sandbox.run_code(code, language="deno") + assert execution.results[0].text == "true" + + +async def test_toplevel_await(async_sandbox: AsyncSandbox): + code = """ + async function main() { + return 'Hello, World!' + } + + await main() + """ + execution = await async_sandbox.run_code(code, language="deno") + assert execution.results[0].text == "Hello, World!" + + +async def test_es6(async_sandbox: AsyncSandbox): + code = """ +const add = (x, y) => x + y; +add(1, 2); + """ + execution = await async_sandbox.run_code(code, language="deno") + assert execution.results[0].text == "3" + + +async def test_context(async_sandbox: AsyncSandbox): + await async_sandbox.run_code("const x = 1", language="deno") + execution = await async_sandbox.run_code("x", language="deno") + assert execution.results[0].text == "1" + + +async def test_cwd(async_sandbox: AsyncSandbox): + execution = await async_sandbox.run_code("process.cwd()", language="deno") + assert execution.results[0].text == "/home/user" + + ctx = await async_sandbox.create_code_context("/home", language="deno") + execution = await async_sandbox.run_code("process.cwd()", context=ctx) + assert execution.results[0].text == "/home" + + +async def test_typescript(async_sandbox: AsyncSandbox): + execution = await async_sandbox.run_code( + """ +function subtract(x: number, y: number): number { + return x - y; +} + +subtract(1, 2); +""", + language="deno", + ) + assert execution.results[0].text == "-1" + + +async def test_display(async_sandbox: AsyncSandbox): + code = """ +{ + [Symbol.for("Jupyter.display")]() { + return { + // Plain text content + "text/plain": "Hello world!", + + // HTML output + "text/html": "

Hello world!

", + } + } +} + """ + execution = await async_sandbox.run_code(code, language="deno") + assert execution.results[0].text == "Hello world!" + assert execution.results[0].html == "

Hello world!

" diff --git a/python/tests/sync/test_default_kernels.py b/python/tests/sync/test_default_kernels.py index cb21cdc..d0daf82 100644 --- a/python/tests/sync/test_default_kernels.py +++ b/python/tests/sync/test_default_kernels.py @@ -1,6 +1,20 @@ +import pytest + from e2b_code_interpreter.code_interpreter_sync import Sandbox def test_js_kernel(sandbox: Sandbox): execution = sandbox.run_code("console.log('Hello, World!')", language="js") assert execution.logs.stdout == ["Hello, World!\n"] + + +@pytest.mark.skip_debug() +def test_r_kernel(sandbox: Sandbox): + execution = sandbox.run_code('print("Hello, World!")', language="r") + assert execution.logs.stdout == ['[1] "Hello, World!"\n'] + + +@pytest.mark.skip_debug() +def test_java_kernel(sandbox: Sandbox): + execution = sandbox.run_code('System.out.println("Hello, World!")', language="java") + assert execution.logs.stdout[0] == "Hello, World!" diff --git a/template/Dockerfile b/template/Dockerfile index 5717108..5a2e1af 100644 --- a/template/Dockerfile +++ b/template/Dockerfile @@ -27,6 +27,12 @@ RUN npm install -g node-gyp RUN npm install -g --unsafe-perm ijavascript RUN ijsinstall --install=global +# Deno Kernel +COPY --from=denoland/deno:bin-2.0.4 /deno /usr/bin/deno +RUN chmod +x /usr/bin/deno +RUN deno jupyter --unstable --install +COPY ./deno.json /root/.local/share/jupyter/kernels/deno/kernel.json + # Bash Kernel RUN pip install bash_kernel RUN python -m bash_kernel.install diff --git a/template/deno.json b/template/deno.json new file mode 100644 index 0000000..3c491e4 --- /dev/null +++ b/template/deno.json @@ -0,0 +1,14 @@ +{ + "argv": [ + "/usr/bin/deno", + "jupyter", + "--kernel", + "--conn", + "{connection_file}" + ], + "display_name": "Deno", + "env": { + "NO_COLOR": "1" + }, + "language": "typescript" +} diff --git a/template/server/api/models/result.py b/template/server/api/models/result.py index e63cdd0..90e24a9 100644 --- a/template/server/api/models/result.py +++ b/template/server/api/models/result.py @@ -45,7 +45,10 @@ def __init__(self, is_main_result: bool, data: [str, str]): self.is_main_result = is_main_result self.text = data.pop("text/plain", None) - if self.text and self.text.startswith("'") and self.text.endswith("'"): + if self.text and ( + (self.text.startswith("'") and self.text.endswith("'")) + or (self.text.startswith('"') and self.text.endswith('"')) + ): self.text = self.text[1:-1] self.html = data.pop("text/html", None) diff --git a/template/server/contexts.py b/template/server/contexts.py index 87b8cad..3af317d 100644 --- a/template/server/contexts.py +++ b/template/server/contexts.py @@ -36,9 +36,8 @@ async def create_context(client, websockets: dict, language: str, cwd: str) -> C response = await client.post(f"{JUPYTER_BASE_URL}/api/sessions", json=data) if not response.is_success: - return PlainTextResponse( + raise Exception( f"Failed to create context: {response.text}", - status_code=500, ) session_data = response.json() @@ -53,7 +52,7 @@ async def create_context(client, websockets: dict, language: str, cwd: str) -> C logger.info(f"Setting working directory to {cwd}") try: - await ws.change_current_directory(cwd) + await ws.change_current_directory(cwd, language) except ExecutionError as e: return PlainTextResponse( "Failed to set working directory", diff --git a/template/server/main.py b/template/server/main.py index ba35abf..fa7760a 100644 --- a/template/server/main.py +++ b/template/server/main.py @@ -89,9 +89,13 @@ async def post_execute(request: ExecutionRequest): context_id = default_websockets.get(language) if not context_id: - context = await create_context( - client, websockets, language, "/home/user" - ) + try: + context = await create_context( + client, websockets, language, "/home/user" + ) + except Exception as e: + return PlainTextResponse(str(e), status_code=500) + context_id = context.id default_websockets[language] = context_id @@ -110,7 +114,10 @@ async def post_execute(request: ExecutionRequest): ) return StreamingListJsonResponse( - ws.execute(request.code, env_vars=request.env_vars) + ws.execute( + request.code, + env_vars=request.env_vars, + ) ) @@ -121,7 +128,10 @@ async def post_contexts(request: CreateContext) -> Context: language = normalize_language(request.language) cwd = request.cwd or "/home/user" - return await create_context(client, websockets, language, cwd) + try: + return await create_context(client, websockets, language, cwd) + except Exception as e: + return PlainTextResponse(str(e), status_code=500) @app.get("/contexts") diff --git a/template/server/messaging.py b/template/server/messaging.py index 6d4635c..4f537aa 100644 --- a/template/server/messaging.py +++ b/template/server/messaging.py @@ -1,3 +1,4 @@ +import datetime import json import logging import uuid @@ -95,14 +96,21 @@ def _get_execute_request( "session": self.session_id, "msg_type": "execute_request", "version": "5.3", + "date": datetime.datetime.now(datetime.timezone.utc).isoformat(), }, "parent_header": {}, - "metadata": {}, + "metadata": { + "trusted": True, + "deletedCells": [], + "recordTiming": False, + "cellId": str(uuid.uuid4()), + }, "content": { "code": code, "silent": background, "store_history": True, "user_expressions": {}, + "stop_on_error": True, "allow_stdin": False, }, } @@ -127,10 +135,29 @@ async def _wait_for_result(self, message_id: str): yield output.model_dump(exclude_none=True) - async def change_current_directory(self, path: Union[str, StrictStr]): + async def change_current_directory( + self, path: Union[str, StrictStr], language: str + ): message_id = str(uuid.uuid4()) self._executions[message_id] = Execution(in_background=True) - request = self._get_execute_request(message_id, f"%cd {path}", True) + if language == "python": + request = self._get_execute_request(message_id, f"%cd {path}", True) + elif language == "deno": + request = self._get_execute_request( + message_id, f"Deno.chdir('{path}')", True + ) + elif language == "js": + request = self._get_execute_request( + message_id, f"process.chdir('{path}')", True + ) + elif language == "r": + request = self._get_execute_request(message_id, f"setwd('{path}')", True) + elif language == "java": + request = self._get_execute_request( + message_id, f"System.setProperty('user.dir', '{path}')", True + ) + else: + return await self._ws.send(request) @@ -165,11 +192,12 @@ async def execute( indent = len(line) - len(line.lstrip()) break - code = ( - indent * " " - + f"os.environ.set_envs_for_execution({vars_to_set})\n" - + code - ) + if self.language == "python": + code = ( + indent * " " + + f"os.environ.set_envs_for_execution({vars_to_set})\n" + + code + ) logger.info(code) request = self._get_execute_request(message_id, code, False) @@ -192,7 +220,7 @@ async def _receive_message(self): async for message in self._ws: await self._process_message(json.loads(message)) except Exception as e: - logger.error(f"WebSocket received error while receiving messages: {e}") + logger.error(f"WebSocket received error while receiving messages: {str(e)}") async def _process_message(self, data: dict): """ @@ -308,11 +336,21 @@ async def _process_message(self, data: dict): execution.errored = True await queue.put( Error( - name=data["content"]["ename"], - value=data["content"]["evalue"], - traceback="".join(data["content"]["traceback"]), + name=data["content"].get("ename", ""), + value=data["content"].get("evalue", ""), + traceback="".join(data["content"].get("traceback", [])), ) ) + elif data["content"]["status"] == "abort": + logger.debug(f"Execution {parent_msg_ig} was aborted") + await queue.put( + Error( + name="ExecutionAborted", + value="Execution was aborted", + traceback="", + ) + ) + await queue.put(EndOfExecution()) elif data["content"]["status"] == "ok": pass diff --git a/template/test.Dockerfile b/template/test.Dockerfile index 195f3ab..6dea866 100644 --- a/template/test.Dockerfile +++ b/template/test.Dockerfile @@ -23,6 +23,12 @@ RUN npm install -g node-gyp RUN npm install -g --unsafe-perm ijavascript RUN ijsinstall --install=global +# Deno Kernel +COPY --from=denoland/deno:bin-2.0.4 /deno /usr/bin/deno +RUN chmod +x /usr/bin/deno +RUN deno jupyter --unstable --install +COPY ./template/deno.json /root/.local/share/jupyter/kernels/deno/kernel.json + # Create separate virtual environment for server RUN python -m venv $SERVER_PATH/.venv