Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental - Stateful code interpreter #326

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
aed6370
First version of stateful code interpreter
jakubno Mar 5, 2024
37a9489
Add prerelease pipeline
jakubno Mar 5, 2024
c91fa10
[skip ci] Release new versions
github-actions[bot] Mar 5, 2024
fded067
Change method name to `exec_python`
jakubno Mar 5, 2024
6d130be
[skip ci] Release new versions
github-actions[bot] Mar 5, 2024
9379395
Add on stdout and on stderr for code execution streaming, fix websocket
jakubno Mar 6, 2024
4a99814
[skip ci] Release new versions
github-actions[bot] Mar 6, 2024
66182c8
Add possibility to pass template in python
jakubno Mar 6, 2024
d7aa184
Add error info
jakubno Mar 6, 2024
0fc18ea
Implement code interpreter in js
jakubno Mar 6, 2024
8766860
Add release pipeline
jakubno Mar 6, 2024
ea8b13a
Release new versions
github-actions[bot] Mar 6, 2024
2f3ef86
[skip ci] Release new versions
github-actions[bot] Mar 6, 2024
3e5e928
Fix pipelines
jakubno Mar 7, 2024
c370f9d
Fix js version
jakubno Mar 7, 2024
604845b
[skip ci] Release new versions
github-actions[bot] Mar 7, 2024
0befb64
Fix reconnect in js
jakubno Mar 7, 2024
2a0de7b
Fix reconnect
jakubno Mar 7, 2024
13500a9
[skip ci] Release new versions
github-actions[bot] Mar 7, 2024
a160308
Update installed packages in template
jakubno Mar 7, 2024
325a33d
[skip ci] Release new versions
github-actions[bot] Mar 8, 2024
3dd2e92
Allow all origins in jupyter server
jakubno Mar 8, 2024
e9cfdd2
[skip ci] Release new versions
github-actions[bot] Mar 8, 2024
d252f2d
Fix python reconnect
jakubno Mar 8, 2024
d9eac38
[skip ci] Release new versions
github-actions[bot] Mar 8, 2024
4142a5e
Switch to start cmd
jakubno Mar 8, 2024
72bb5b9
[skip ci] Release new versions
github-actions[bot] Mar 8, 2024
7f0a8f8
Remove auth token
jakubno Mar 8, 2024
67efd48
[skip ci] Release new versions
github-actions[bot] Mar 8, 2024
a39cbac
Fix python package versions
jakubno Mar 8, 2024
1491f5a
[skip ci] Release new versions
github-actions[bot] Mar 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/release_candidates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: SDK Release Candidate

on:
pull_request:

permissions:
contents: write

jobs:
release:
name: Release Candidate
runs-on: ubuntu-latest

steps:
- name: Checkout Repo
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}

- uses: pnpm/action-setup@v3
if: ${{ contains( github.event.pull_request.labels.*.name, 'js-rc') }}


- name: Setup Node.js 20
uses: actions/setup-node@v4
if: ${{ contains( github.event.pull_request.labels.*.name, 'js-rc') || contains( github.event.pull_request.labels.*.name, 'python-rc') }}
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
cache: pnpm

- name: Configure pnpm
working-directory: packages/js-sdk
if: ${{ contains( github.event.pull_request.labels.*.name, 'js-rc') }}
run: |
pnpm config set auto-install-peers true
pnpm config set exclude-links-from-lockfile true

- name: Install dependencies
working-directory: packages/js-sdk
if: ${{ contains( github.event.pull_request.labels.*.name, 'js-rc') }}
run: pnpm install --frozen-lockfile

- name: Release Candidate
working-directory: packages/js-sdk
if: ${{ contains( github.event.pull_request.labels.*.name, 'js-rc') }}
run: |
npm version prerelease --preid=${{ github.head_ref }}
npm publish --tag rc
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Set up Python
uses: actions/setup-python@v4
if: ${{ contains( github.event.pull_request.labels.*.name, 'python-rc') }}
with:
python-version: "3.10"

- name: Install and configure Poetry
uses: snok/install-poetry@v1
if: ${{ contains( github.event.pull_request.labels.*.name, 'python-rc') }}
with:
version: 1.5.1
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true

- name: Release Candidate
if: ${{ contains( github.event.pull_request.labels.*.name, 'python-rc') }}
working-directory: packages/python-sdk
run: |
poetry version prerelease
poetry build
poetry config pypi-token.pypi ${PYPI_TOKEN} && poetry publish --skip-existing
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}

- name: Commit new versions
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -am "[skip ci] Release new versions" || exit 0
git push
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion packages/js-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "e2b",
"version": "0.12.5",
"version": "0.12.6-stateful-code-interpreter.8",
"description": "E2B SDK that give agents cloud environments",
"homepage": "https://e2b.dev",
"license": "MIT",
Expand Down
1 change: 1 addition & 0 deletions packages/js-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { runCode, CodeRuntime } from './runCode' // Export CodeRuntime enum as v
import { Sandbox } from './sandbox/index'

import { DataAnalysis } from './templates/dataAnalysis'
export { CodeInterpreterV2 } from './templates/statefulCodeInterpreter'
export { DataAnalysis as CodeInterpreter }

export { Artifact, DataAnalysis } from './templates/dataAnalysis'
Expand Down
268 changes: 268 additions & 0 deletions packages/js-sdk/src/templates/statefulCodeInterpreter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import { Sandbox, SandboxOpts } from '../sandbox'
import { ProcessMessage } from '../sandbox/process'
import { randomBytes } from 'crypto'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use web crypto for edge environment support (cf workers)

import IWebSocket from 'isomorphic-ws'

interface ExecutionError {
name: string
value: string
traceback: string[]
}

interface Result {
output?: string
stdout: string[]
stderr: string[]
error?: ExecutionError
display_data: object[]
}

export class CodeInterpreterV2 extends Sandbox {
private static template = 'code-interpreter-stateful'
private jupyterKernelID?: string

/**
* Use `DataAnalysis.create()` instead.
*
* @hidden
* @hide
* @internal
* @access protected
*/
constructor(opts: SandboxOpts) {
super({ template: opts.template || CodeInterpreterV2.template, ...opts })
}

/**
* Creates a new Sandbox from the template.
* @returns New Sandbox
*/
static override async create(): Promise<CodeInterpreterV2>
/**
* Creates a new Sandbox from the specified options.
* @param opts Sandbox options
* @returns New Sandbox
*/
static override async create(opts: SandboxOpts): Promise<CodeInterpreterV2>
static override async create(opts?: SandboxOpts) {
const sandbox = new CodeInterpreterV2({ ...(opts ? opts : {}) })
await sandbox._open({ timeout: opts?.timeout })
sandbox.jupyterKernelID = await sandbox.getKernelID()

return sandbox
}


/**
* Reconnects to an existing Sandbox.
* @param sandboxID Sandbox ID
* @returns Existing Sandbox
*
* @example
* ```ts
* const sandbox = await Sandbox.create()
* const sandboxID = sandbox.id
*
* await sandbox.keepAlive(300 * 1000)
* await sandbox.close()
*
* const reconnectedSandbox = await Sandbox.reconnect(sandboxID)
* ```
*/
static async reconnect<S extends typeof CodeInterpreterV2>(this: S, sandboxID: string): Promise<InstanceType<S>>
/**
* Reconnects to an existing Sandbox.
* @param opts Sandbox options
* @returns Existing Sandbox
*
* @example
* ```ts
* const sandbox = await Sandbox.create()
* const sandboxID = sandbox.id
*
* await sandbox.keepAlive(300 * 1000)
* await sandbox.close()
*
* const reconnectedSandbox = await Sandbox.reconnect({
* sandboxID,
* })
* ```
*/
static async reconnect<S extends typeof CodeInterpreterV2>(this: S, opts: Omit<SandboxOpts, 'id' | 'template'> & { sandboxID: string }): Promise<InstanceType<S>>
static async reconnect<S extends typeof CodeInterpreterV2>(this: S, sandboxIDorOpts: string | Omit<SandboxOpts, 'id' | 'template'> & { sandboxID: string }): Promise<InstanceType<S>> {
let id: string
let opts: SandboxOpts
if (typeof sandboxIDorOpts === 'string') {
id = sandboxIDorOpts
opts = {}
} else {
id = sandboxIDorOpts.sandboxID
opts = sandboxIDorOpts
}

const sandboxIDAndClientID = id.split('-')
const sandboxID = sandboxIDAndClientID[0]
const clientID = sandboxIDAndClientID[1]
opts.__sandbox = { sandboxID, clientID, templateID: 'unknown' }

const sandbox = new this(opts) as InstanceType<S>
await sandbox._open({ timeout: opts?.timeout })

sandbox.jupyterKernelID = await sandbox.getKernelID()
return sandbox
}

private static sendExecuteRequest(code: string) {
const msg_id = randomBytes(16).toString('hex')
const session = randomBytes(16).toString('hex')
return {
header: {
msg_id: msg_id,
username: 'e2b',
session: session,
msg_type: 'execute_request',
version: '5.3',
},
parent_header: {},
metadata: {},
content: {
code: code,
silent: false,
store_history: false,
user_expressions: {},
allow_stdin: false,
},
}
}

async execPython(
code: string,
onStdout?: (out: ProcessMessage) => Promise<void> | void,
onStderr?: (out: ProcessMessage) => Promise<void> | void,
) {
let resolve: () => void

const finished = new Promise<void>((r) => {
resolve = () => r()
})
const result: Result = {
stdout: [],
stderr: [],
display_data: [],
}

// @ts-ignore
const ws = await this._connectKernel(result, resolve, onStdout, onStderr)
ws.send(JSON.stringify(CodeInterpreterV2.sendExecuteRequest(code)))
await finished

ws.close()
return result
}

private async getKernelID() {
return await this.filesystem.read('/root/.jupyter/kernel_id')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return await this.filesystem.read('/root/.jupyter/kernel_id')
return (await this.filesystem.read('/root/.jupyter/kernel_id')).trim();

There was an extra newline at the end on cloudflare workers durable objects.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @lawrencecchen , all the fixes will be incorporated in this repo https://github.com/e2b-dev/code-interpreter

}

private async _connectKernel(
result: Result,
finish: () => void,
onStdout?: (out: ProcessMessage) => Promise<void> | void,
onStderr?: (out: ProcessMessage) => Promise<void> | void,
) {
const ws = new IWebSocket(
`${this.getProtocol('ws')}://${this.getHostname(8888)}/api/kernels/${
this.jupyterKernelID
}/channels`,
)

const opened = new Promise<void>((resolve) => {
ws.onopen = () => resolve()
})
await opened

const helperData = {
input_accepted: false,
finish,
}

ws.onmessage = (e) => {
this.onMessage(result, helperData, e.data.toString(), {
onStdout,
onStderr,
})
}

return ws
}

private onMessage(
result: Result,
helperData: { input_accepted: boolean; finish: () => void },
data: string,
opts?: {
onStdout?: (out: ProcessMessage) => Promise<void> | void
onStderr?: (out: ProcessMessage) => Promise<void> | void
},
) {
const message = JSON.parse(data)
if (message.msg_type == 'error') {
result.error = {
name: message.content.ename,
value: message.content.evalue,
traceback: message.content.traceback,
}
} else if (message.msg_type == 'stream') {
if (message.content.name == 'stdout') {
result.stdout.push(message.content.text)
if (opts?.onStdout) {
opts.onStdout({
line: message.content.text,
timestamp: new Date().getTime() * 1_000_000,
error: false,
})
}
} else if (message.content.name == 'stderr') {
result.stderr.push(message.content.text)
if (opts?.onStderr) {
opts.onStderr({
line: message.content.text,
timestamp: new Date().getTime() * 1_000_000,
error: true,
})
}
}
} else if (message.msg_type == 'display_data') {
result.display_data.push(message.content.data)
} else if (message.msg_type == 'execute_result') {
result.output = message.content.data['text/plain']
} else if (message.msg_type == 'status') {
if (message.content.execution_state == 'idle') {
if (helperData.input_accepted) {
helperData.finish()
}
} else if (message.content.execution_state == 'error') {
result.error = {
name: message.content.ename,
value: message.content.evalue,
traceback: message.content.traceback,
}
helperData.finish()
}
} else if (message.msg_type == 'execute_reply') {
if (message.content.status == 'error') {
result.error = {
name: message.content.ename,
value: message.content.evalue,
traceback: message.content.traceback,
}
} else if (message.content.status == 'ok') {
return
}
} else if (message.msg_type == 'execute_input') {
helperData.input_accepted = true
} else {
console.log('[UNHANDLED MESSAGE TYPE]:', message.msg_type)
}
}
}
Loading