Skip to content

Commit e0b6927

Browse files
committed
Make it backward compatible + add execution count
1 parent 392aa9a commit e0b6927

File tree

9 files changed

+86
-46
lines changed

9 files changed

+86
-46
lines changed

js/src/code-interpreter.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import { ProcessMessage, Sandbox, SandboxOpts } from 'e2b'
22
import { Result, JupyterKernelWebSocket, Execution } from './messaging'
3-
import { createDeferredPromise } from './utils'
3+
import { createDeferredPromise, id } from './utils'
44

55
interface Kernels {
66
[kernelID: string]: JupyterKernelWebSocket
77
}
88

9-
export interface CreateKernelProps {
10-
path: string
11-
kernelName?: string
12-
}
13-
149
/**
1510
* E2B code interpreter sandbox extension.
1611
*/
@@ -101,11 +96,9 @@ export class JupyterExtension {
10196
resolve: (value: string) => void,
10297
opts?: { timeout?: number }
10398
) {
104-
const sessionInfo = (
105-
await this.sandbox.filesystem.read('/root/.jupyter/.session_info', opts)
106-
)
107-
const parsedSessionInfo = JSON.parse(sessionInfo)
108-
const kernelID = parsedSessionInfo.kernel.id
99+
const kernelID = (
100+
await this.sandbox.filesystem.read('/root/.jupyter/kernel_id', opts)
101+
).trim()
109102
await this.connectToKernelWS(kernelID)
110103
resolve(kernelID)
111104
}
@@ -119,13 +112,16 @@ export class JupyterExtension {
119112
* the retrieval of the necessary WebSocket URL from the kernel's information.
120113
*
121114
* @param kernelID The unique identifier of the kernel to connect to.
115+
* @param sessionID The unique identifier of the session to connect to.
122116
* @throws {Error} Throws an error if the connection to the kernel's WebSocket cannot be established.
123117
*/
124-
private async connectToKernelWS(kernelID: string) {
118+
private async connectToKernelWS(kernelID: string, sessionID?: string) {
125119
const url = `${this.sandbox.getProtocol('ws')}://${this.sandbox.getHostname(
126120
8888
127121
)}/api/kernels/${kernelID}/channels`
128-
const ws = new JupyterKernelWebSocket(url)
122+
123+
sessionID = sessionID || id(16)
124+
const ws = new JupyterKernelWebSocket(url, sessionID)
129125
await ws.connect()
130126
this.connectedKernels[kernelID] = ws
131127

@@ -149,15 +145,15 @@ export class JupyterExtension {
149145
cwd: string = '/home/user',
150146
kernelName?: string
151147
): Promise<string> {
152-
const data: CreateKernelProps = { path: cwd }
148+
const data = { path: cwd, kernel: {name: "python3"}, type: "notebook", name: id(16) }
153149
if (kernelName) {
154-
data.kernelName = kernelName
150+
data.kernel.name = kernelName
155151
}
156152

157153
const response = await fetch(
158154
`${this.sandbox.getProtocol()}://${this.sandbox.getHostname(
159155
8888
160-
)}/api/kernels`,
156+
)}/api/sessions`,
161157
{
162158
method: 'POST',
163159
body: JSON.stringify(data)
@@ -168,8 +164,10 @@ export class JupyterExtension {
168164
throw new Error(`Failed to create kernel: ${response.statusText}`)
169165
}
170166

171-
const kernelID = (await response.json()).id
172-
await this.connectToKernelWS(kernelID)
167+
const sessionInfo = await response.json()
168+
const kernelID = sessionInfo.kernel.id
169+
await this.connectToKernelWS(kernelID, sessionInfo.id)
170+
173171
return kernelID
174172
}
175173

js/src/messaging.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,11 @@ export class Execution {
226226
/**
227227
* An Error object if an error occurred, null otherwise.
228228
*/
229-
public error?: ExecutionError
229+
public error?: ExecutionError,
230+
/**
231+
* Execution count of the cell.
232+
*/
233+
public executionCount?: number
230234
) { }
231235

232236
/**
@@ -305,7 +309,7 @@ export class JupyterKernelWebSocket {
305309
* Does not start WebSocket connection!
306310
* You need to call connect() method first.
307311
*/
308-
constructor(private readonly url: string) { }
312+
constructor(private readonly url: string, private readonly sessionID: string) { }
309313

310314
// public
311315
/**
@@ -401,6 +405,7 @@ export class JupyterKernelWebSocket {
401405
}
402406
} else if (message.msg_type == 'execute_input') {
403407
cell.inputAccepted = true
408+
cell.execution.executionCount = message.content.execution_count
404409
} else {
405410
console.warn('[UNHANDLED MESSAGE TYPE]:', message.msg_type)
406411
}
@@ -486,12 +491,11 @@ export class JupyterKernelWebSocket {
486491
* @param code Code to be executed.
487492
*/
488493
private sendExecuteRequest(msg_id: string, code: string) {
489-
const session = id(16)
490494
return {
491495
header: {
492496
msg_id: msg_id,
493497
username: 'e2b',
494-
session: session,
498+
session: this.sessionID,
495499
msg_type: 'execute_request',
496500
version: '5.3'
497501
},
@@ -500,7 +504,7 @@ export class JupyterKernelWebSocket {
500504
content: {
501505
code: code,
502506
silent: false,
503-
store_history: false,
507+
store_history: true,
504508
user_expressions: {},
505509
allow_stdin: false
506510
}

js/tests/executionCount.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { CodeInterpreter } from '../src'
2+
3+
import { expect, test } from 'vitest'
4+
5+
test('execution count', async () => {
6+
const sandbox = await CodeInterpreter.create()
7+
8+
await sandbox.notebook.execCell('!pwd')
9+
const result = await sandbox.notebook.execCell('!pwd')
10+
11+
12+
await sandbox.close()
13+
14+
expect(result.executionCount).toEqual(2)
15+
})

python/e2b_code_interpreter/main.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from __future__ import annotations
22

3-
import json
43
import logging
54
import threading
5+
import uuid
6+
67
import requests
78

89
from concurrent.futures import Future
@@ -94,14 +95,14 @@ def exec_cell(
9495
logger.debug(f"Creating new websocket connection to kernel {kernel_id}")
9596
ws = self._connect_to_kernel_ws(kernel_id, timeout=timeout)
9697

97-
session_id = ws.send_execution_message(code, on_stdout, on_stderr, on_result)
98+
message_id = ws.send_execution_message(code, on_stdout, on_stderr, on_result)
9899
logger.debug(
99-
f"Sent execution message to kernel {kernel_id}, session_id: {session_id}"
100+
f"Sent execution message to kernel {kernel_id}, message_id: {message_id}"
100101
)
101102

102-
result = ws.get_result(session_id, timeout=timeout)
103+
result = ws.get_result(message_id, timeout=timeout)
103104
logger.debug(
104-
f"Received result from kernel {kernel_id}, session_id: {session_id}, result: {result}"
105+
f"Received result from kernel {kernel_id}, message_id: {message_id}, result: {result}"
105106
)
106107

107108
return result
@@ -140,24 +141,26 @@ def create_kernel(
140141
:param timeout: Timeout for the kernel creation request.
141142
:return: Kernel id of the created kernel
142143
"""
143-
data = {"path": cwd}
144+
data = {"path": cwd, "kernel": {"name": "python3"}, "type": "notebook", "name": str(uuid.uuid4())}
144145
if kernel_name:
145-
data["kernel_name"] = kernel_name
146+
data["kernel"]['name'] = kernel_name
146147
logger.debug(f"Creating kernel with data: {data}")
147148

148149
response = requests.post(
149-
f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/kernels",
150+
f"{self._sandbox.get_protocol()}://{self._sandbox.get_hostname(8888)}/api/sessions",
150151
json=data,
151152
timeout=timeout,
152153
)
153154
if not response.ok:
154155
raise KernelException(f"Failed to create kernel: {response.text}")
155156

156-
kernel_id = response.json()["id"]
157+
session_data = response.json()
158+
kernel_id = session_data["kernel"]["id"]
159+
session_id = session_data["id"]
157160
logger.debug(f"Created kernel {kernel_id}")
158161

159162
threading.Thread(
160-
target=self._connect_to_kernel_ws, args=(kernel_id, timeout)
163+
target=self._connect_to_kernel_ws, args=(kernel_id, session_id, timeout)
161164
).start()
162165
return kernel_id
163166

@@ -244,7 +247,7 @@ def close(self):
244247
ws.result().close()
245248

246249
def _connect_to_kernel_ws(
247-
self, kernel_id: str, timeout: Optional[float] = TIMEOUT
250+
self, kernel_id: str, session_id: Optional[str], timeout: Optional[float] = TIMEOUT
248251
) -> JupyterKernelWebSocket:
249252
"""
250253
Establishes a WebSocket connection to a specified Jupyter kernel.
@@ -258,9 +261,13 @@ def _connect_to_kernel_ws(
258261
future = Future()
259262
self._connected_kernels[kernel_id] = future
260263

264+
print(session_id)
265+
session_id = session_id or str(uuid.uuid4())
261266
ws = JupyterKernelWebSocket(
262267
url=f"{self._sandbox.get_protocol('ws')}://{self._sandbox.get_hostname(8888)}/api/kernels/{kernel_id}/channels",
268+
session_id=session_id
263269
)
270+
264271
ws.connect(timeout=timeout)
265272
logger.debug(f"Connected to kernel's ({kernel_id}) websocket.")
266273

@@ -277,16 +284,13 @@ def _start_connecting_to_default_kernel(
277284
logger.debug("Starting to connect to the default kernel")
278285

279286
def setup_default_kernel():
280-
session_info = self._sandbox.filesystem.read(
281-
"/root/.jupyter/.session_info", timeout=timeout
287+
kernel_id = self._sandbox.filesystem.read(
288+
"/root/.jupyter/kernel_id", timeout=timeout
282289
)
283-
if session_info is None and not self._sandbox.is_open:
284-
return
290+
kernel_id = kernel_id.strip()
285291

286-
session_info = json.loads(session_info)
287-
kernel_id = session_info["kernel"]["id"]
288292
logger.debug(f"Default kernel id: {kernel_id}")
289-
self._connect_to_kernel_ws(kernel_id, timeout=timeout)
293+
self._connect_to_kernel_ws(kernel_id, None, timeout=timeout)
290294
self._kernel_id_set.set_result(kernel_id)
291295

292296
threading.Thread(target=setup_default_kernel).start()

python/e2b_code_interpreter/messaging.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class CellExecution:
2525
"""
2626

2727
input_accepted: bool = False
28+
2829
on_stdout: Optional[Callable[[ProcessMessage], Any]] = None
2930
on_stderr: Optional[Callable[[ProcessMessage], Any]] = None
3031
on_result: Optional[Callable[[Result], Any]] = None
@@ -44,8 +45,9 @@ def __init__(
4445

4546
class JupyterKernelWebSocket:
4647

47-
def __init__(self, url: str):
48+
def __init__(self, url: str, session_id: str):
4849
self.url = url
50+
self.session_id = session_id
4951
self._cells: Dict[str, CellExecution] = {}
5052
self._waiting_for_replies: Dict[str, DeferredFuture] = {}
5153
self._queue_in = Queue()
@@ -101,14 +103,13 @@ def connect(self, timeout: float = TIMEOUT):
101103

102104
logger.debug("WebSocket started")
103105

104-
@staticmethod
105-
def _get_execute_request(msg_id: str, code: str) -> str:
106+
def _get_execute_request(self, msg_id: str, code: str) -> str:
106107
return json.dumps(
107108
{
108109
"header": {
109110
"msg_id": msg_id,
110111
"username": "e2b",
111-
"session": str(uuid.uuid4()),
112+
"session": self.session_id,
112113
"msg_type": "execute_request",
113114
"version": "5.3",
114115
},
@@ -117,7 +118,7 @@ def _get_execute_request(msg_id: str, code: str) -> str:
117118
"content": {
118119
"code": code,
119120
"silent": False,
120-
"store_history": False,
121+
"store_history": True,
121122
"user_expressions": {},
122123
"allow_stdin": False,
123124
},
@@ -237,6 +238,7 @@ def _receive_message(self, data: dict):
237238

238239
elif data["msg_type"] == "execute_input":
239240
logger.debug(f"Input accepted for {parent_msg_ig}")
241+
cell.partial_result.execution_count = data["content"]["execution_count"]
240242
cell.input_accepted = True
241243
else:
242244
logger.warning(f"[UNHANDLED MESSAGE TYPE]: {data['msg_type']}")

python/e2b_code_interpreter/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,8 @@ class Config:
222222
"Logs printed to stdout and stderr during execution."
223223
error: Optional[Error] = None
224224
"Error object if an error occurred, None otherwise."
225+
execution_count: Optional[int] = None
226+
"Execution count of the cell."
225227

226228
@property
227229
def text(self) -> Optional[str]:

python/example.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"""
2525

2626
with CodeInterpreter() as sandbox:
27+
print(sandbox.id)
2728
execution = sandbox.notebook.exec_cell(code)
2829

2930
print(execution.results[0].formats())
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from e2b_code_interpreter.main import CodeInterpreter
2+
3+
4+
def test_bash():
5+
with CodeInterpreter() as sandbox:
6+
result = sandbox.notebook.exec_cell("echo 'E2B is awesome!'")
7+
print(result.execution_count)
8+
result = sandbox.notebook.exec_cell("!pwd")
9+
print(result.execution_count)
10+
result = sandbox.notebook.exec_cell("!pwd")
11+
print(result.execution_count)
12+

template/start-up.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ function start_jupyter_server() {
2323
echo "Kernel created"
2424

2525
sudo mkdir -p /root/.jupyter
26+
kernel_id=$(echo "${response}" | jq -r '.kernel.id')
27+
sudo echo "${kernel_id}" | sudo tee /root/.jupyter/kernel_id >/dev/null
2628
sudo echo "${response}" | sudo tee /root/.jupyter/.session_info >/dev/null
2729
echo "Jupyter Server started"
2830
}

0 commit comments

Comments
 (0)