diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/e2b/ConnectionConfig.java b/k8s/java/src/main/java/io/openkruise/agents/client/e2b/ConnectionConfig.java index 2f8c30d..9fbf81d 100644 --- a/k8s/java/src/main/java/io/openkruise/agents/client/e2b/ConnectionConfig.java +++ b/k8s/java/src/main/java/io/openkruise/agents/client/e2b/ConnectionConfig.java @@ -2,6 +2,9 @@ import io.openkruise.agents.client.e2b.api.SandboxesApi; import io.openkruise.agents.client.e2b.api.invoker.ApiClient; +import io.openkruise.agents.client.runtime.RuntimeConfig; +import io.openkruise.agents.client.url.E2BURLBuilder; +import io.openkruise.agents.client.url.URLBuilder; import java.util.HashMap; import java.util.Map; @@ -30,7 +33,6 @@ public String getValue() { private static final String DEFAULT_SCHEME = "https"; private static final long DEFAULT_REQUEST_TIMEOUT_MS = 60_000L; static final int DEFAULT_SANDBOX_TIMEOUT = 300; - static final int DEFAULT_RUNTIME_PORT = 49983; private String apiKey; private String accessToken; @@ -42,19 +44,24 @@ public String getValue() { private boolean debug; private long requestTimeoutMs; private int port; + private int codeInterpreterPort; private Map headers; private volatile ApiClient apiClient; private volatile SandboxesApi sandboxesApi; private final Object lock = new Object(); + + private URLBuilder urlBuilder; private ConnectionConfig() { this.domain = DEFAULT_DOMAIN; this.scheme = DEFAULT_SCHEME; this.protocol = Protocol.NATIVE; this.requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS; - this.port = DEFAULT_RUNTIME_PORT; + this.port = RuntimeConfig.DEFAULT_RUNTIME_PORT; + this.codeInterpreterPort = RuntimeConfig.DEFAULT_CODE_INTERPRETER_PORT; this.headers = new HashMap<>(); + this.urlBuilder = buildURLBuilder(); } private ConnectionConfig(ConnectionConfig config) { @@ -68,35 +75,82 @@ private ConnectionConfig(ConnectionConfig config) { this.debug = config.debug; this.requestTimeoutMs = config.requestTimeoutMs; this.port = config.port; + this.codeInterpreterPort = config.codeInterpreterPort; this.headers = new HashMap<>(config.headers); + this.urlBuilder = buildURLBuilder(); + } + + private URLBuilder buildURLBuilder() { + return new E2BURLBuilder.Builder() + .scheme(scheme) + .domain(domain) + .protocol(protocol) + .runtimePort(port) + .codeInterpreterPort(codeInterpreterPort) + .customApiURL(apiURL) + .customSandboxBaseURL(sandboxBaseURL) + .build(); } public static ConnectionConfig create() { return new Builder().build(); } - /** API base URL, automatically selects NATIVE/PRIVATE format based on protocol. */ - public String getAPIURL() { - if (apiURL != null && !apiURL.isEmpty()) { - return apiURL; + /** + * Convert ConnectionConfig to RuntimeConfig for data plane operations. + */ + public static RuntimeConfig toRuntimeConfig(ConnectionConfig connectionConfig, String envdAccessToken) { + if (connectionConfig == null) { + throw new IllegalArgumentException("connectionConfig cannot be null"); } - String s = getSchemeOrDefault(); - if (protocol == Protocol.PRIVATE) { - return String.format("%s://%s/kruise/api", s, domain); + + RuntimeConfig.Builder builder = new RuntimeConfig.Builder(); + builder.domain(connectionConfig.getDomain()); + builder.scheme(connectionConfig.getScheme()); + builder.requestTimeoutMs(connectionConfig.getRequestTimeoutMs()); + + if (connectionConfig.getApiKey() != null) { + builder.apiKey(connectionConfig.getApiKey()); } - return String.format("%s://api.%s", s, domain); + if (connectionConfig.getHeaders() != null) { + builder.headers(connectionConfig.getHeaders()); + } + if (envdAccessToken != null && !envdAccessToken.isEmpty()) { + builder.runtimeToken(envdAccessToken); + } + + builder.sandboxPort(connectionConfig.getPort()); + builder.codeInterpreterPort(connectionConfig.getCodeInterpreterPort()); + + // Build URLBuilder for E2B + URLBuilder urlBuilder = new E2BURLBuilder.Builder() + .scheme(connectionConfig.getScheme()) + .domain(connectionConfig.getDomain()) + .protocol(connectionConfig.getProtocol()) + .runtimePort(connectionConfig.getPort()) + .codeInterpreterPort(connectionConfig.getCodeInterpreterPort()) + .customApiURL(connectionConfig.getApiURL()) + .customSandboxBaseURL(connectionConfig.getSandboxBaseURL()) + .build(); + + builder.urlBuilder(urlBuilder); + + return builder.build(); + } + + /** API base URL, automatically selects NATIVE/PRIVATE format based on protocol. */ + public String getAPIURL() { + return urlBuilder.buildAPIURL(); } /** Envd URL for a specific sandbox, automatically selects NATIVE/PRIVATE format based on protocol. */ public String getSandboxURL(String sandboxID) { - if (sandboxBaseURL != null && !sandboxBaseURL.isEmpty()) { - return sandboxBaseURL; - } - String s = getSchemeOrDefault(); - if (protocol == Protocol.PRIVATE) { - return String.format("%s://%s/kruise/%s/%d", s, domain, sandboxID, port); - } - return String.format("%s://%d-%s.%s", s, port, sandboxID, domain); + return urlBuilder.buildSandboxURL(sandboxID); + } + + /** Code Interpreter URL for a specific sandbox, uses codeInterpreterPort (49999). */ + public String getCodeInterpreterURL(String sandboxID) { + return urlBuilder.buildCodeInterpreterURL(sandboxID); } /** Shared ApiClient with double-checked locking lazy initialization. */ @@ -156,6 +210,8 @@ private String getSchemeOrDefault() { public int getPort() {return port;} + public int getCodeInterpreterPort() {return codeInterpreterPort;} + public Map getHeaders() {return headers;} public static class Builder { @@ -223,6 +279,11 @@ public Builder port(int port) { return this; } + public Builder codeInterpreterPort(int codeInterpreterPort) { + config.codeInterpreterPort = codeInterpreterPort; + return this; + } + public Builder headers(Map headers) { config.headers.putAll(headers); return this; @@ -234,6 +295,7 @@ public Builder addHeader(String key, String value) { } public ConnectionConfig build() { + config.urlBuilder = config.buildURLBuilder(); return new ConnectionConfig(config); } } diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/e2b/E2bRuntimeConfig.java b/k8s/java/src/main/java/io/openkruise/agents/client/e2b/E2bRuntimeConfig.java deleted file mode 100644 index ebf3f4d..0000000 --- a/k8s/java/src/main/java/io/openkruise/agents/client/e2b/E2bRuntimeConfig.java +++ /dev/null @@ -1,146 +0,0 @@ -package io.openkruise.agents.client.e2b; - -import io.openkruise.agents.client.runtime.RuntimeConfig; - -import java.util.Map; - -/** - * RuntimeConfig subclass that carries E2B-specific URL construction and request header logic. - */ -public class E2bRuntimeConfig extends RuntimeConfig { - - private final String sandboxBaseURL; - private final int e2bRuntimePort; - - private E2bRuntimeConfig(Builder builder) { - super(builder); - this.sandboxBaseURL = builder.sandboxBaseURL; - this.e2bRuntimePort = builder.e2bRuntimePort; - } - - /** - * PRIVATE mode uses sandboxBaseURL, NATIVE mode uses {@code ://-.}. - */ - @Override - public String getSandboxURL(String sandboxID) { - if (sandboxBaseURL != null && !sandboxBaseURL.isEmpty()) { - // PRIVATE mode: sandboxBaseURL already contains full path prefix - return sandboxBaseURL; - } - return String.format("%s://%d-%s.%s", getScheme(), e2bRuntimePort, sandboxID, getDomain()); - } - - /** Adds e2b-sandbox-port to the base request headers. */ - @Override - public Map getSandboxHeaders(String sandboxID) { - Map result = super.getSandboxHeaders(sandboxID); - result.put("e2b-sandbox-port", String.valueOf(e2bRuntimePort)); - return result; - } - - public static E2bRuntimeConfig fromConnectionConfig(ConnectionConfig connectionConfig, String envdAccessToken) { - Builder builder = new Builder(); - builder.domain(connectionConfig.getDomain()); - builder.scheme(connectionConfig.getScheme()); - builder.requestTimeoutMs(connectionConfig.getRequestTimeoutMs()); - - if (connectionConfig.getApiKey() != null) { - builder.apiKey(connectionConfig.getApiKey()); - } - if (connectionConfig.getHeaders() != null) { - builder.headers(connectionConfig.getHeaders()); - } - if (envdAccessToken != null && !envdAccessToken.isEmpty()) { - builder.runtimeToken(envdAccessToken); - } - - builder.e2bRuntimePort(connectionConfig.getPort()); - - if (connectionConfig.getProtocol() == ConnectionConfig.Protocol.PRIVATE) { - String scheme = connectionConfig.getScheme() != null ? connectionConfig.getScheme() : "https"; - builder.e2bSandboxBaseURL(String.format("%s://%s", scheme, connectionConfig.getDomain())); - } - - return builder.buildE2b(); - } - - public static class Builder extends RuntimeConfig.Builder { - private String sandboxBaseURL; - private int e2bRuntimePort = ConnectionConfig.DEFAULT_RUNTIME_PORT; - - public Builder() { - super(); - } - - /** - * Sets sandboxBaseURL for PRIVATE mode. - */ - public Builder e2bSandboxBaseURL(String sandboxBaseURL) { - this.sandboxBaseURL = sandboxBaseURL; - return this; - } - - public Builder e2bRuntimePort(int port) { - this.e2bRuntimePort = port; - return this; - } - - public E2bRuntimeConfig buildE2b() { - return new E2bRuntimeConfig(this); - } - - @Override - public Builder domain(String domain) { - super.domain(domain); - return this; - } - - @Override - public Builder scheme(String scheme) { - super.scheme(scheme); - return this; - } - - @Override - public Builder runtimeToken(String runtimeToken) { - super.runtimeToken(runtimeToken); - return this; - } - - @Override - public Builder authHeader(String authHeader) { - super.authHeader(authHeader); - return this; - } - - @Override - public Builder apiKey(String apiKey) { - super.apiKey(apiKey); - return this; - } - - @Override - public Builder headers(Map headers) { - super.headers(headers); - return this; - } - - @Override - public Builder addHeader(String key, String value) { - super.addHeader(key, value); - return this; - } - - @Override - public Builder requestTimeoutMs(long requestTimeoutMs) { - super.requestTimeoutMs(requestTimeoutMs); - return this; - } - - @Override - public Builder runtimeUrl(String runtimeUrl) { - super.runtimeUrl(runtimeUrl); - return this; - } - } -} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/e2b/README.md b/k8s/java/src/main/java/io/openkruise/agents/client/e2b/README.md index 2d9f1e7..4674233 100644 --- a/k8s/java/src/main/java/io/openkruise/agents/client/e2b/README.md +++ b/k8s/java/src/main/java/io/openkruise/agents/client/e2b/README.md @@ -10,10 +10,8 @@ ```java import io.openkruise.agents.client.e2b.*; -ConnectionConfig config = new ConnectionConfig.Builder() - .apiKey("your-api-key") - .domain("your.domain.com") - .build(); +// Reads E2B_API_KEY and E2B_DOMAIN from environment variables +ConnectionConfig config = new ConnectionConfig.Builder().build(); SandboxApi api = new SandboxApi(config); @@ -27,6 +25,12 @@ try (sandbox) { } ``` +**Environment Variables Setup**: +```bash +export E2B_API_KEY="your-api-key" +export E2B_DOMAIN="your.domain.com" +``` + Full examples: [Lifecycle Management](../examples/e2b/SandboxApiManagerExample.java) | [Commands](../examples/e2b/SandboxCommandsExample.java) | [Files](../examples/e2b/SandboxFilesExample.java) @@ -39,10 +43,8 @@ examples: [Lifecycle Management](../examples/e2b/SandboxApiManagerExample.java) ### Initialization ```java -ConnectionConfig config = new ConnectionConfig.Builder() - .apiKey("your-api-key") - .domain("your.domain.com") - .build(); +// Reads E2B_API_KEY and E2B_DOMAIN from environment variables +ConnectionConfig config = new ConnectionConfig.Builder().build(); SandboxApi api = new SandboxApi(config); ``` @@ -100,6 +102,7 @@ api.kill(id); | `sandboxID` | Sandbox ID | | `commands` | Command execution module (`Commands`) | | `files` | Filesystem module (`Filesystem`) | +| `codeInterpreter` | Code interpreter module (`CodeInterpreter`) | | `getSandboxURL()` | Sandbox envd URL | | `getConfig()` | Connection config | | `getRuntimeClient()` | Underlying `RuntimeClient` | @@ -263,7 +266,136 @@ wh.stop(); --- -## 4. SandboxInfo (Immutable) +## 4. Code Interpreter (CodeInterpreter) + +Execute code in the sandbox via `sandbox.codeInterpreter`. Supports Python, JavaScript, TypeScript, R, Java, Bash, and +more. + +### Methods + +| Method | Description | +|---------------------------------------------------------|--------------------------------------------------| +| `runCode(String code)` | Execute Python code (default language) | +| `runCode(String code, String language)` | Execute code in specified language | +| `runCode(String code, String language, RunCodeOptions)` | Execute code with options (cwd, env vars, etc.) | +| `runCode(RunCodeRequest request)` | Execute code (full request object) | +| `createCodeContext(String cwd, String language)` | Create code execution context (set cwd/language) | +| `removeCodeContext(String contextId)` | Remove specified code execution context | +| `listCodeContexts()` | List all code execution contexts | + +### RunCodeOptions + +```java +RunCodeOptions opts = new RunCodeOptions() + .setCwd("/tmp") // Working directory + .setEnvVars(Map.of("DEBUG", "true")) // Environment variables + .setTimeoutMs(30000L) // Timeout (milliseconds) + .setContextId("context-id"); // Use specified context (mutually exclusive with language) +``` + +### Context (Code Execution Context) + +Context maintains an independent code execution environment. Each Context has its own working directory and language +environment. + +| Field | Type | Description | +|------------|----------|-------------| +| `id` | `String` | Context ID | +| `language` | `String` | Language | +| `cwd` | `String` | Working dir | + +**Note**: When using Context, the `language` parameter in `runCode()` is ignored (server requires `context_id` and +`language` to be mutually exclusive). + +### Execution (Execution Result) + +| Field | Type | Description | +|------------------|------------------|---------------------------------------------| +| `results` | `List` | Execution results (text/html/image formats) | +| `logs` | `Logs` | Log output (stdout/stderr) | +| `error` | `ExecutionError` | Execution error (if any) | +| `executionCount` | `Integer` | Execution count | + +### Result (Result Format) + +Supports multiple output formats, similar to Jupyter notebook: + +| Field | Type | Description | +|--------------|-----------------------|----------------| +| `text` | `String` | Plain text | +| `html` | `String` | HTML output | +| `markdown` | `String` | Markdown | +| `png` | `String` (base64) | PNG image | +| `jpeg` | `String` (base64) | JPEG image | +| `svg` | `String` | SVG graphic | +| `json` | `Map` | JSON data | +| `mainResult` | `boolean` | Is main result | + +### Examples + +```java +// Execute Python code +Execution result = sandbox.codeInterpreter.runCode("print('Hello from Python!')"); +for (String line : result.getLogs().getStdout()) { + System.out.println(line); +} + +// Execute JavaScript code +Execution jsResult = sandbox.codeInterpreter.runCode( + "console.log('Hello from JS!');", + RunCodeLanguage.JAVASCRIPT.getValue() +); + +// Execute with options (environment variables) +RunCodeOptions opts = new RunCodeOptions() + .setEnvVars(Map.of("API_KEY", "secret")); +Execution result2 = sandbox.codeInterpreter.runCode( + "import os; print(os.environ.get('API_KEY'))", + RunCodeLanguage.PYTHON.getValue(), + opts +); + +// Use Context to set working directory +Context ctx = sandbox.codeInterpreter.createCodeContext("/tmp", "python"); +System.out.println("Context created: " + ctx); + +RunCodeOptions ctxOpts = new RunCodeOptions() + .setContextId(ctx.getId()); +Execution ctxResult = sandbox.codeInterpreter.runCode( + "import os; print('CWD:', os.getcwd())", + RunCodeLanguage.PYTHON.getValue(), + ctxOpts +); + +// Clean up Context +sandbox.codeInterpreter.removeCodeContext(ctx.getId()); + +// List all Contexts +List contexts = sandbox.codeInterpreter.listCodeContexts(); +for (Context c : contexts) { + System.out.println(c); +} + +// Get main result text +String mainText = result2.getText(); +System.out.println("Main result: " + mainText); + +// Streaming execution (event-by-event processing) +RunCodeRequest request = new RunCodeRequest("for i in range(5): print(i)", "python"); +sandbox.codeInterpreter.runCodeStreaming(request, event -> { + if (event instanceof StdoutEvent) { + System.out.print(((StdoutEvent) event).getText()); + } else if (event instanceof ErrorEvent) { + System.err.println("Error: " + ((ErrorEvent) event).getError()); + } +}); +``` + +Full example: [SandboxCodeInterpreterExample.java](../examples/e2b/SandboxCodeInterpreterExample.java) + +--- + +## 5. SandboxInfo (Immutable) `SandboxInfo` is the return type of `list()` / `getInfo()`, an **immutable object** (all fields `final`, no setters, `metadata` and `volumeMounts` are unmodifiable collections). @@ -293,7 +425,7 @@ wh.stop(); --- -## 5. Connection Configuration (ConnectionConfig) +## 6. Connection Configuration (ConnectionConfig) ### Scheme & Protocol @@ -322,6 +454,7 @@ Connection behavior is determined by two orthogonal dimensions: **Scheme** and * | `.sandboxBaseURL(String)` | **Highest priority**: directly overrides sandbox envd base URL | | `.requestTimeoutMs(long)` | HTTP request timeout (ms), default 60000 | | `.port(int)` | envd port, default 49983 | +| `.codeInterpreterPort(int)` | Code interpreter port, default 49999 | | `.debug(boolean)` | Debug mode; kill/setTimeout skip actual calls | | `.headers(Map)` | Custom request headers | | `.addHeader(String, String)` | Add a single custom header | @@ -343,7 +476,7 @@ Builder methods: --- -## 6. K8s Direct Connect Mode +## 7. K8s Direct Connect Mode Connect directly to a sandbox in a K8s cluster, bypassing the E2B control plane: diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/e2b/README_zh-CH.md b/k8s/java/src/main/java/io/openkruise/agents/client/e2b/README_zh-CH.md index bcb9bfa..d11efbe 100644 --- a/k8s/java/src/main/java/io/openkruise/agents/client/e2b/README_zh-CH.md +++ b/k8s/java/src/main/java/io/openkruise/agents/client/e2b/README_zh-CH.md @@ -9,10 +9,8 @@ ```java import io.openkruise.agents.client.e2b.*; -ConnectionConfig config = new ConnectionConfig.Builder() - .apiKey("your-api-key") - .domain("your.domain.com") - .build(); +// 从环境变量读取 E2B_API_KEY 和 E2B_DOMAIN +ConnectionConfig config = new ConnectionConfig.Builder().build(); SandboxApi api = new SandboxApi(config); @@ -26,6 +24,12 @@ try (sandbox) { } ``` +**环境变量设置**: +```bash +export E2B_API_KEY="your-api-key" +export E2B_DOMAIN="your.domain.com" +``` + 完整示例:[生命周期管理](../examples/e2b/SandboxApiManagerExample.java) | [命令操作](../examples/e2b/SandboxCommandsExample.java) | [文件操作](../examples/e2b/SandboxFilesExample.java) --- @@ -37,10 +41,8 @@ try (sandbox) { ### 初始化 ```java -ConnectionConfig config = new ConnectionConfig.Builder() - .apiKey("your-api-key") - .domain("your.domain.com") - .build(); +// 从环境变量读取 E2B_API_KEY 和 E2B_DOMAIN +ConnectionConfig config = new ConnectionConfig.Builder().build(); SandboxApi api = new SandboxApi(config); ``` @@ -98,6 +100,7 @@ api.kill(id); | `sandboxID` | Sandbox ID | | `commands` | 命令执行模块(`Commands`) | | `files` | 文件系统模块(`Filesystem`) | +| `codeInterpreter` | 代码解释器模块(`CodeInterpreter` | | `getSandboxURL()` | Sandbox envd URL | | `getConfig()` | 连接配置 | | `getRuntimeClient()` | 底层 `RuntimeClient` | @@ -259,7 +262,133 @@ wh.stop(); --- -## 四、SandboxInfo(不可变) +## 四、代码解释器(CodeInterpreter) + +通过 `sandbox.codeInterpreter` 在沙箱内执行代码。支持 Python、JavaScript、TypeScript、R、Java、Bash 等多种语言。 + +### 方法一览 + +| 方法 | 说明 | +|---------------------------------------------------------|-----------------------| +| `runCode(String code)` | 执行 Python 代码(默认语言) | +| `runCode(String code, String language)` | 执行指定语言的代码 | +| `runCode(String code, String language, RunCodeOptions)` | 执行代码(带选项:工作目录、环境变量等) | +| `runCode(RunCodeRequest request)` | 执行代码(完整请求对象) | +| `createCodeContext(String cwd, String language)` | 创建代码执行上下文(可设置工作目录和语言) | +| `removeCodeContext(String contextId)` | 删除指定的代码执行上下文 | +| `listCodeContexts()` | 列出所有代码执行上下文 | + +### RunCodeOptions + +```java +RunCodeOptions opts = new RunCodeOptions() + .setCwd("/tmp") // 工作目录 + .setEnvVars(Map.of("DEBUG", "true")) // 环境变量 + .setTimeoutMs(30000L) // 超时时间(毫秒) + .setContextId("context-id"); // 使用指定的上下文(与 language 互斥) +``` + +### Context(代码执行上下文) + +Context 用于维护独立的代码执行环境,每个 Context 有自己的工作目录和语言环境。 + +| 字段 | 类型 | 说明 | +|------------|----------|--------| +| `id` | `String` | 上下文 ID | +| `language` | `String` | 编程语言 | +| `cwd` | `String` | 工作目录 | + +**注意**:使用 Context 时,`runCode()` 的 `language` 参数会被忽略(服务端要求 `context_id` 和 `language` 互斥)。 + +### Execution(执行结果) + +| 字段 | 类型 | 说明 | +|------------------|------------------|-----------------------------| +| `results` | `List` | 执行结果列表(text/html/image 等格式) | +| `logs` | `Logs` | 日志输出(stdout/stderr) | +| `error` | `ExecutionError` | 执行错误(如有) | +| `executionCount` | `Integer` | 执行次数 | + +### Result(结果格式) + +支持多种输出格式,类似 Jupyter notebook: + +| 字段 | 类型 | 说明 | +|--------------|-----------------------|----------| +| `text` | `String` | 纯文本输出 | +| `html` | `String` | HTML 输出 | +| `markdown` | `String` | Markdown | +| `png` | `String` (base64) | PNG 图片 | +| `jpeg` | `String` (base64) | JPEG 图片 | +| `svg` | `String` | SVG 图形 | +| `json` | `Map` | JSON 数据 | +| `mainResult` | `boolean` | 是否为主结果 | + +### 示例 + +```java +// 执行 Python 代码 +Execution result = sandbox.codeInterpreter.runCode("print('Hello from Python!')"); +for (String line : result.getLogs().getStdout()) { + System.out.println(line); +} + +// 执行 JavaScript 代码 +Execution jsResult = sandbox.codeInterpreter.runCode( + "console.log('Hello from JS!');", + RunCodeLanguage.JAVASCRIPT.getValue() +); + +// 带选项执行(环境变量) +RunCodeOptions opts = new RunCodeOptions() + .setEnvVars(Map.of("API_KEY", "secret")); +Execution result2 = sandbox.codeInterpreter.runCode( + "import os; print(os.environ.get('API_KEY'))", + RunCodeLanguage.PYTHON.getValue(), + opts +); + +// 使用 Context 设置工作目录 +Context ctx = sandbox.codeInterpreter.createCodeContext("/tmp", "python"); +System.out.println("Context created: " + ctx); + +RunCodeOptions ctxOpts = new RunCodeOptions() + .setContextId(ctx.getId()); +Execution ctxResult = sandbox.codeInterpreter.runCode( + "import os; print('CWD:', os.getcwd())", + RunCodeLanguage.PYTHON.getValue(), + ctxOpts +); + +// 清理 Context +sandbox.codeInterpreter.removeCodeContext(ctx.getId()); + +// 列出所有 Context +List contexts = sandbox.codeInterpreter.listCodeContexts(); +for (Context c : contexts) { + System.out.println(c); +} + +// 获取主结果文本 +String mainText = result2.getText(); +System.out.println("Main result: " + mainText); + +// 流式执行(逐事件处理) +RunCodeRequest request = new RunCodeRequest("for i in range(5): print(i)", "python"); +sandbox.codeInterpreter.runCodeStreaming(request, event -> { + if (event instanceof StdoutEvent) { + System.out.print(((StdoutEvent) event).getText()); + } else if (event instanceof ErrorEvent) { + System.err.println("Error: " + ((ErrorEvent) event).getError()); + } +}); +``` + +完整示例:[SandboxCodeInterpreterExample.java](../examples/e2b/SandboxCodeInterpreterExample.java) + +--- + +## 五、SandboxInfo(不可变) `SandboxInfo` 是 `list()` / `getInfo()` 的返回类型,**不可变对象**(所有字段 `final`,无 setters,`metadata` 和 `volumeMounts` 为 unmodifiable 集合)。 @@ -289,7 +418,7 @@ wh.stop(); --- -## 五、连接配置(ConnectionConfig) +## 六、连接配置(ConnectionConfig) ### Scheme 与 Protocol @@ -318,6 +447,7 @@ wh.stop(); | `.sandboxBaseURL(String)` | **最高优先级**:直接覆盖 sandbox envd base URL | | `.requestTimeoutMs(long)` | HTTP 请求超时(毫秒),默认 60000 | | `.port(int)` | envd 端口,默认 49983 | +| `.codeInterpreterPort(int)` | 代码解释器端口,默认 49999 | | `.debug(boolean)` | 调试模式,kill/setTimeout 跳过实际调用 | | `.headers(Map)` | 自定义请求头 | | `.addHeader(String, String)` | 添加单个自定义请求头 | @@ -337,7 +467,7 @@ Builder 构造时自动读取以下环境变量作为默认值,之后可通过 --- -## 六、K8s 直连模式 +## 七、K8s 直连模式 不通过 E2B 控制面,直接连接 K8s 集群中的 sandbox: diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/e2b/Sandbox.java b/k8s/java/src/main/java/io/openkruise/agents/client/e2b/Sandbox.java index 8c1aea5..3ad491c 100644 --- a/k8s/java/src/main/java/io/openkruise/agents/client/e2b/Sandbox.java +++ b/k8s/java/src/main/java/io/openkruise/agents/client/e2b/Sandbox.java @@ -1,6 +1,8 @@ package io.openkruise.agents.client.e2b; +import io.openkruise.agents.client.runtime.codeinterpreter.CodeInterpreter; import io.openkruise.agents.client.runtime.RuntimeClient; +import io.openkruise.agents.client.runtime.RuntimeConfig; import io.openkruise.agents.client.runtime.commands.Commands; import io.openkruise.agents.client.runtime.filesystem.Filesystem; @@ -15,6 +17,8 @@ public class Sandbox implements AutoCloseable { public final Commands commands; /** File operations within the sandbox */ public final Filesystem files; + /** Code interpreter for executing code in the sandbox */ + public final CodeInterpreter codeInterpreter; private final String sandboxID; private final ConnectionConfig config; @@ -24,10 +28,11 @@ public class Sandbox implements AutoCloseable { this.sandboxID = sandboxID; this.config = config; - E2bRuntimeConfig runtimeConfig = E2bRuntimeConfig.fromConnectionConfig(config, envdAccessToken); + RuntimeConfig runtimeConfig = ConnectionConfig.toRuntimeConfig(config, envdAccessToken); this.runtimeClient = RuntimeClient.create(sandboxID, runtimeConfig); this.commands = runtimeClient.commands; this.files = runtimeClient.files; + this.codeInterpreter = runtimeClient.codeInterpreter; } public String getSandboxID() { diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxApiManagerExample.java b/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxApiManagerExample.java index 0553494..b213b0b 100644 --- a/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxApiManagerExample.java +++ b/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxApiManagerExample.java @@ -15,16 +15,12 @@ * including: create, list, pause, get info, reconnect, set timeout, destroy. */ public class SandboxApiManagerExample { - private static final String API_KEY = "your-api-key"; - private static final String SANDBOX_DOMAIN = "your.domain.com"; private static final String TEMPLATE = "code-interpreter"; public static void main(String[] args) throws ApiException { // 1. Build connection configuration - ConnectionConfig config = new ConnectionConfig.Builder() - .apiKey(API_KEY) - .domain(SANDBOX_DOMAIN) - .build(); + // Reads E2B_API_KEY and E2B_DOMAIN from environment variables as defaults + ConnectionConfig config = new ConnectionConfig.Builder().build(); SandboxApi api = new SandboxApi(config); String sandboxId = null; diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxCodeInterpreterExample.java b/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxCodeInterpreterExample.java new file mode 100644 index 0000000..1cb1eb0 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxCodeInterpreterExample.java @@ -0,0 +1,118 @@ +package io.openkruise.agents.client.examples.e2b; + +import io.openkruise.agents.client.e2b.ConnectionConfig; +import io.openkruise.agents.client.e2b.Sandbox; +import io.openkruise.agents.client.e2b.SandboxApi; +import io.openkruise.agents.client.runtime.codeinterpreter.CodeInterpreter; +import io.openkruise.agents.client.runtime.codeinterpreter.Context; +import io.openkruise.agents.client.runtime.codeinterpreter.Execution; +import io.openkruise.agents.client.runtime.codeinterpreter.RunCodeLanguage; +import io.openkruise.agents.client.runtime.codeinterpreter.RunCodeOptions; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * E2B Sandbox Code Interpreter Example + * Demonstrates basic code execution with Python and JavaScript. + */ +public class SandboxCodeInterpreterExample { + private static final String TEMPLATE = "code-interpreter"; + + public static void main(String[] args) { + System.out.println("========== E2B Sandbox Code Interpreter Example ==========\n"); + + // Reads E2B_API_KEY and E2B_DOMAIN from environment variables as defaults + ConnectionConfig config = new ConnectionConfig.Builder().build(); + + SandboxApi api = new SandboxApi(config); + String sandboxId = null; + + try { + Sandbox sandbox = api.create(TEMPLATE); + sandboxId = sandbox.getSandboxID(); + System.out.printf("✓ Sandbox created: %s%n%n", sandboxId); + + CodeInterpreter interpreter = sandbox.codeInterpreter; + + // Python example + System.out.println("--- Python Execution ---"); + String pythonCode = "print('Hello from Python!')\nresult = 2 + 3\nprint(f'Result: {result}')"; + Execution pythonResult = interpreter.runCode(pythonCode); + + System.out.printf("Code: %s%n", pythonCode.replace("\n", "\\n")); + System.out.printf("Output:%n"); + for (String line : pythonResult.getLogs().getStdout()) { + System.out.printf(" %s%n", line); + } + System.out.printf("Execution count: %d%n%n", pythonResult.getExecutionCount()); + + // JavaScript example + System.out.println("--- JavaScript Execution ---"); + String jsCode + = "console.log('Hello from JavaScript!');\nconst result = 5 * 7;\nconsole.log(`Result: ${result}`);"; + Execution jsResult = interpreter.runCode(jsCode, RunCodeLanguage.JAVASCRIPT.getValue()); + + System.out.printf("Code: %s%n", jsCode.replace("\n", "\\n")); + System.out.printf("Output:%n"); + for (String line : jsResult.getLogs().getStdout()) { + System.out.printf(" %s%n", line); + } + System.out.printf("Execution count: %d%n", jsResult.getExecutionCount()); + + // Python with options example + System.out.println("\n--- Python with Options ---"); + Map envVars = new HashMap<>(); + envVars.put("MY_VAR", "Hello from env!"); + + RunCodeOptions opts = new RunCodeOptions() + .setEnvVars(envVars) + .setTimeoutMs(10000L) + .setOnStdout(event -> System.out.print("[REALTIME] " + event.getText())) + .setOnStderr(event -> System.err.print("[ERROR] " + event.getText())); + + String codeWithEnv = "import os\nprint('ENV:', os.environ.get('MY_VAR'))"; + Execution envResult = interpreter.runCode(codeWithEnv, RunCodeLanguage.PYTHON.getValue(), opts); + + System.out.printf("%nFinal execution count: %d%n", envResult.getExecutionCount()); + + // Context API example + System.out.println("\n--- Context API Example ---"); + Context ctx = interpreter.createCodeContext("/tmp", RunCodeLanguage.PYTHON.getValue()); + System.out.printf("✓ Context created: %s%n", ctx); + + // List all contexts + List contexts = interpreter.listCodeContexts(); + System.out.printf("✓ Active contexts: %d%n", contexts.size()); + for (Context c : contexts) { + System.out.printf(" - %s%n", c); + } + + RunCodeOptions ctxOpts = new RunCodeOptions() + .setContextId(ctx.getId()) + .setOnStdout(event -> System.out.print("[CTX] " + event.getText())); + + String ctxCode = "import os\nprint('CWD:', os.getcwd())"; + interpreter.runCode(ctxCode, RunCodeLanguage.PYTHON.getValue(), ctxOpts); + + interpreter.removeCodeContext(ctx.getId()); + System.out.printf("✓ Context removed: %s%n", ctx.getId()); + + System.out.println("\n========== Example Completed Successfully =========="); + + } catch (Exception e) { + System.err.println("✗ Error: " + e.getMessage()); + e.printStackTrace(); + } finally { + if (sandboxId != null) { + try { + api.kill(sandboxId); + System.out.println("✓ Sandbox cleaned up: " + sandboxId); + } catch (Exception e) { + System.err.println("✗ Cleanup failed: " + e.getMessage()); + } + } + } + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxCommandsExample.java b/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxCommandsExample.java index 42890fb..d5b61a4 100644 --- a/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxCommandsExample.java +++ b/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxCommandsExample.java @@ -16,18 +16,14 @@ * E2B mode Commands complete usage example */ public class SandboxCommandsExample { - private static final String API_KEY = "your-api-key"; - private static final String SANDBOX_DOMAIN = "your.domain.com"; private static final String TEMPLATE = "code-interpreter"; public static void main(String[] args) { System.out.println("========== E2B Sandbox Commands Java SDK Example =========="); // ========== 1. Connection configuration ========== - ConnectionConfig config = new ConnectionConfig.Builder() - .apiKey(API_KEY) - .domain(SANDBOX_DOMAIN) - .build(); + // Reads E2B_API_KEY and E2B_DOMAIN from environment variables as defaults + ConnectionConfig config = new ConnectionConfig.Builder().build(); SandboxApi api = new SandboxApi(config); String sandboxId = null; diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxFilesExample.java b/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxFilesExample.java index baeb00c..efb0f92 100644 --- a/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxFilesExample.java +++ b/k8s/java/src/main/java/io/openkruise/agents/client/examples/e2b/SandboxFilesExample.java @@ -13,18 +13,14 @@ * E2B mode Files complete usage example */ public class SandboxFilesExample { - private static final String API_KEY = "your-api-key"; - private static final String SANDBOX_DOMAIN = "your.domain.com"; private static final String TEMPLATE = "code-interpreter"; public static void main(String[] args) { System.out.println("========== E2B Sandbox Files Java SDK Example =========="); // ========== 1. Connection configuration ========== - ConnectionConfig config = new ConnectionConfig.Builder() - .apiKey(API_KEY) - .domain(SANDBOX_DOMAIN) - .build(); + // Reads E2B_API_KEY and E2B_DOMAIN from environment variables as defaults + ConnectionConfig config = new ConnectionConfig.Builder().build(); SandboxApi api = new SandboxApi(config); String sandboxId = null; diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/examples/runtime/K8sDirectConnectExample.java b/k8s/java/src/main/java/io/openkruise/agents/client/examples/runtime/K8sDirectConnectExample.java index c9e657a..721c0f0 100644 --- a/k8s/java/src/main/java/io/openkruise/agents/client/examples/runtime/K8sDirectConnectExample.java +++ b/k8s/java/src/main/java/io/openkruise/agents/client/examples/runtime/K8sDirectConnectExample.java @@ -2,6 +2,7 @@ import io.openkruise.agents.client.runtime.RuntimeConfig; import io.openkruise.agents.client.runtime.RuntimeClient; +import io.openkruise.agents.client.runtime.codeinterpreter.Execution; import io.openkruise.agents.client.runtime.commands.CommandResult; import io.openkruise.agents.client.runtime.filesystem.Filesystem.EntryInfo; @@ -39,11 +40,12 @@ public static void main(String[] args) { } // Create directory - client.files.makeDir("/tmp/test-dir"); + String testDir = "/tmp/test-dir" + System.nanoTime(); + client.files.makeDir(testDir); // Check if file exists - boolean exists = client.files.exists("/tmp/test-dir"); - System.out.println("/tmp/test-dir exists: " + exists); + boolean exists = client.files.exists(testDir); + System.out.println("exists: " + testDir + " -> " + exists); // Execute multiple commands CommandResult whoami = client.commands.run("whoami"); @@ -52,6 +54,16 @@ public static void main(String[] args) { CommandResult pwd = client.commands.run("pwd"); System.out.println("Working directory: " + pwd.getStdout().trim()); + // run code + String pythonCode = "print('Hello from Python!')\nresult = 2 + 3\nprint(f'Result: {result}')"; + Execution pythonResult = client.codeInterpreter.runCode(pythonCode); + + System.out.printf("Code: %s%n", pythonCode.replace("\n", "\\n")); + System.out.printf("Output:%n"); + for (String line : pythonResult.getLogs().getStdout()) { + System.out.printf(" %s%n", line); + } + System.out.printf("Execution count: %d%n%n", pythonResult.getExecutionCount()); } catch (Exception e) { System.err.println("K8s direct connection error: " + e.getMessage()); e.printStackTrace(); diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/README.md b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/README.md index 4df337f..84c530d 100644 --- a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/README.md +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/README.md @@ -20,6 +20,7 @@ runtime/ ├── filesystem/ # Filesystem │ ├── Filesystem.java # listDir / read / write / makeDir / remove / watchDir / move │ └── WatchHandle.java # Directory watch handle: stop +├── codeinterpreter/ # Code interpreter ├── utils/ # Utilities │ ├── ConnectStreamReader.java # Connect Protocol streaming response parser │ └── MessageStream.java # Streaming message interface (hasNext / next / close) @@ -96,10 +97,11 @@ Full example: [K8sDirectConnectExample.java](../examples/runtime/K8sDirectConnec ### Fields -| Field | Type | Description | -|------------|--------------|--------------------------| -| `commands` | `Commands` | Command execution module | -| `files` | `Filesystem` | Filesystem module | +| Field | Type | Description | +|-------------------|-------------------|--------------------------| +| `commands` | `Commands` | Command execution module | +| `files` | `Filesystem` | Filesystem module | +| `codeInterpreter` | `CodeInterpreter` | Code interpreter module | ### Methods @@ -132,6 +134,9 @@ routing is involved. | `.headers(Map)` | Merge multiple custom headers | | `.addHeader(String, String)` | Add a single custom header | | `.requestTimeoutMs(long)` | HTTP timeout (ms), default 60000 | +| `.sandboxPort(int)` | envd port, default 49983 (for `e2b-sandbox-port` header) | +| `.codeInterpreterPort(int)` | Code interpreter port, default 49999 | +| `.urlBuilder(URLBuilder)` | URL builder, supports E2B and Runtime modes | ### Priority @@ -295,7 +300,135 @@ wh.stop(); --- -## 5. Exception Hierarchy +## 5. Code Interpreter (CodeInterpreter) + +Execute code in the sandbox via `client.codeInterpreter`. Supports Python, JavaScript, TypeScript, R, Java, Bash, and +more. + +### Methods + +| Method | Description | +|--------------------------------------------------------------|-----------------------------------------------------------| +| `runCode(String code)` | Execute Python code (default language) | +| `runCode(String code, String language)` | Execute code in specified language | +| `runCode(String code, String language, RunCodeOptions)` | Execute code with options (cwd, env vars, etc.) | +| `runCode(RunCodeRequest request)` | Execute code (full request object) | +| `runCodeStreaming(RunCodeRequest, Consumer)` | Streaming execution, event-by-event callback (low memory) | +| `createCodeContext(String cwd, String language)` | Create code execution context (set cwd/language) | +| `removeCodeContext(String contextId)` | Remove specified code execution context | +| `listCodeContexts()` | List all code execution contexts | + +### RunCodeOptions + +```java +RunCodeOptions opts = new RunCodeOptions() + .setCwd("/tmp") // Working directory + .setEnvVars(Map.of("DEBUG", "true")) // Environment variables + .setTimeoutMs(30000L) // Timeout (milliseconds) + .setContextId("context-id"); // Use specified context (mutually exclusive with language) +``` + +### Context (Code Execution Context) + +Context maintains an independent code execution environment. Each Context has its own working directory and language +environment. + +| Field | Type | Description | +|------------|----------|-------------| +| `id` | `String` | Context ID | +| `language` | `String` | Language | +| `cwd` | `String` | Working dir | + +**Note**: When using Context, the `language` parameter in `runCode()` is ignored (server requires `context_id` and +`language` to be mutually exclusive). + +### Execution (Execution Result) + +| Field | Type | Description | +|------------------|------------------|---------------------------------------------| +| `results` | `List` | Execution results (text/html/image formats) | +| `logs` | `Logs` | Log output (stdout/stderr) | +| `error` | `ExecutionError` | Execution error (if any) | +| `executionCount` | `Integer` | Execution count | + +### Result (Result Format) + +Supports multiple output formats, similar to Jupyter notebook: + +| Field | Type | Description | +|--------------|-----------------------|----------------| +| `text` | `String` | Plain text | +| `html` | `String` | HTML output | +| `markdown` | `String` | Markdown | +| `png` | `String` (base64) | PNG image | +| `jpeg` | `String` (base64) | JPEG image | +| `svg` | `String` | SVG graphic | +| `json` | `Map` | JSON data | +| `mainResult` | `boolean` | Is main result | + +### Examples + +```java +// Execute Python code +Execution result = client.codeInterpreter.runCode("print('Hello from Python!')"); +for (String line : result.getLogs().getStdout()) { + System.out.println(line); +} + +// Execute JavaScript code +Execution jsResult = client.codeInterpreter.runCode( + "console.log('Hello from JS!');", + RunCodeLanguage.JAVASCRIPT.getValue() +); + +// Execute with options (environment variables) +RunCodeOptions opts = new RunCodeOptions() + .setEnvVars(Map.of("API_KEY", "secret")); +Execution result2 = client.codeInterpreter.runCode( + "import os; print(os.environ.get('API_KEY'))", + RunCodeLanguage.PYTHON.getValue(), + opts +); + +// Use Context to set working directory +Context ctx = client.codeInterpreter.createCodeContext("/tmp", "python"); +System.out.println("Context created: " + ctx); + +RunCodeOptions ctxOpts = new RunCodeOptions() + .setContextId(ctx.getId()); +Execution ctxResult = client.codeInterpreter.runCode( + "import os; print('CWD:', os.getcwd())", + RunCodeLanguage.PYTHON.getValue(), + ctxOpts +); + +// Clean up Context +client.codeInterpreter.removeCodeContext(ctx.getId()); + +// List all Contexts +List contexts = client.codeInterpreter.listCodeContexts(); +for (Context c : contexts) { + System.out.println(c); +} + +// Get main result text +String mainText = result2.getText(); +System.out.println("Main result: " + mainText); + +// Streaming execution (event-by-event processing) +RunCodeRequest request = new RunCodeRequest("for i in range(5): print(i)", "python"); +client.codeInterpreter.runCodeStreaming(request, event -> { + if (event instanceof StdoutEvent) { + System.out.print(((StdoutEvent) event).getText()); + } else if (event instanceof ErrorEvent) { + System.err.println("Error: " + ((ErrorEvent) event).getError()); + } +}); +``` + +--- + +## 6. Exception Hierarchy | Exception Class | Description | |-------------------------|--------------------------------------------------------------------------------| @@ -304,7 +437,7 @@ wh.stop(); --- -## 6. Resource Management +## 7. Resource Management `RuntimeClient` implements `AutoCloseable`; `close()` releases: diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/README_zh-CH.md b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/README_zh-CH.md index e1f8688..92dd18f 100644 --- a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/README_zh-CH.md +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/README_zh-CH.md @@ -19,6 +19,11 @@ runtime/ ├── filesystem/ # 文件系统 │ ├── Filesystem.java # listDir / read / write / makeDir / remove / watchDir / move │ └── WatchHandle.java # 目录监听句柄:stop +├── codeinterpreter/ # 代码解释器 +│ ├── CodeInterpreter.java # runCode / runCodeStreaming +│ ├── Execution.java # 执行结果:results / logs / error +│ ├── RunCodeRequest.java # 执行请求:code / language / options +│ └── ... # 事件类型:StdoutEvent / StderrEvent / ResultEvent 等 ├── utils/ # 工具类 │ ├── ConnectStreamReader.java # Connect Protocol 流式响应解析 │ └── MessageStream.java # 流式消息接口(hasNext / next / close) @@ -93,10 +98,11 @@ try (RuntimeClient client = RuntimeClient.newFromK8s("default", "your-sandbox-na ### 字段 -| 字段 | 类型 | 说明 | -|------------|--------------|--------| -| `commands` | `Commands` | 命令执行模块 | -| `files` | `Filesystem` | 文件系统模块 | +| 字段 | 类型 | 说明 | +|-------------------|-------------------|---------| +| `commands` | `Commands` | 命令执行模块 | +| `files` | `Filesystem` | 文件系统模块 | +| `codeInterpreter` | `CodeInterpreter` | 代码解释器模块 | ### 方法 @@ -128,6 +134,9 @@ try (RuntimeClient client = RuntimeClient.newFromK8s("default", "your-sandbox-na | `.headers(Map)` | 合并多个自定义 headers | | `.addHeader(String, String)` | 添加单个自定义 header | | `.requestTimeoutMs(long)` | HTTP 超时(毫秒),默认 60000 | +| `.sandboxPort(int)` | envd 端口,默认 49983(用于请求头 `e2b-sandbox-port`) | +| `.codeInterpreterPort(int)` | 代码解释器端口,默认 49999 | +| `.urlBuilder(URLBuilder)` | URL 构建器,支持 E2B 和 Runtime 模式 | ### 优先级 @@ -289,7 +298,132 @@ wh.stop(); --- -## 五、异常体系 +## 五、代码解释器(CodeInterpreter) + +通过 `client.codeInterpreter` 在沙箱内执行代码。支持 Python、JavaScript、TypeScript、R、Java、Bash 等多种语言。 + +### 方法一览 + +| 方法 | 说明 | +|--------------------------------------------------------------|-----------------------| +| `runCode(String code)` | 执行 Python 代码(默认语言) | +| `runCode(String code, String language)` | 执行指定语言的代码 | +| `runCode(String code, String language, RunCodeOptions)` | 执行代码(带选项:工作目录、环境变量等) | +| `runCode(RunCodeRequest request)` | 执行代码(完整请求对象) | +| `runCodeStreaming(RunCodeRequest, Consumer)` | 流式执行代码,逐事件回调(低内存占用) | +| `createCodeContext(String cwd, String language)` | 创建代码执行上下文(可设置工作目录和语言) | +| `removeCodeContext(String contextId)` | 删除指定的代码执行上下文 | +| `listCodeContexts()` | 列出所有代码执行上下文 | + +### RunCodeOptions + +```java +RunCodeOptions opts = new RunCodeOptions() + .setCwd("/tmp") // 工作目录 + .setEnvVars(Map.of("DEBUG", "true")) // 环境变量 + .setTimeoutMs(30000L) // 超时时间(毫秒) + .setContextId("context-id"); // 使用指定的上下文(与 language 互斥) +``` + +### Context(代码执行上下文) + +Context 用于维护独立的代码执行环境,每个 Context 有自己的工作目录和语言环境。 + +| 字段 | 类型 | 说明 | +|------------|----------|--------| +| `id` | `String` | 上下文 ID | +| `language` | `String` | 编程语言 | +| `cwd` | `String` | 工作目录 | + +**注意**:使用 Context 时,`runCode()` 的 `language` 参数会被忽略(服务端要求 `context_id` 和 `language` 互斥)。 + +### Execution(执行结果) + +| 字段 | 类型 | 说明 | +|------------------|------------------|-----------------------------| +| `results` | `List` | 执行结果列表(text/html/image 等格式) | +| `logs` | `Logs` | 日志输出(stdout/stderr) | +| `error` | `ExecutionError` | 执行错误(如有) | +| `executionCount` | `Integer` | 执行次数 | + +### Result(结果格式) + +支持多种输出格式,类似 Jupyter notebook: + +| 字段 | 类型 | 说明 | +|--------------|-----------------------|----------| +| `text` | `String` | 纯文本输出 | +| `html` | `String` | HTML 输出 | +| `markdown` | `String` | Markdown | +| `png` | `String` (base64) | PNG 图片 | +| `jpeg` | `String` (base64) | JPEG 图片 | +| `svg` | `String` | SVG 图形 | +| `json` | `Map` | JSON 数据 | +| `mainResult` | `boolean` | 是否为主结果 | + +### 示例 + +```java +// 执行 Python 代码 +Execution result = client.codeInterpreter.runCode("print('Hello from Python!')"); +for (String line : result.getLogs().getStdout()) { + System.out.println(line); +} + +// 执行 JavaScript 代码 +Execution jsResult = client.codeInterpreter.runCode( + "console.log('Hello from JS!');", + RunCodeLanguage.JAVASCRIPT.getValue() +); + +// 带选项执行(环境变量) +RunCodeOptions opts = new RunCodeOptions() + .setEnvVars(Map.of("API_KEY", "secret")); +Execution result2 = client.codeInterpreter.runCode( + "import os; print(os.environ.get('API_KEY'))", + RunCodeLanguage.PYTHON.getValue(), + opts +); + +// 使用 Context 设置工作目录 +Context ctx = client.codeInterpreter.createCodeContext("/tmp", "python"); +System.out.println("Context created: " + ctx); + +RunCodeOptions ctxOpts = new RunCodeOptions() + .setContextId(ctx.getId()); +Execution ctxResult = client.codeInterpreter.runCode( + "import os; print('CWD:', os.getcwd())", + RunCodeLanguage.PYTHON.getValue(), + ctxOpts +); + +// 清理 Context +client.codeInterpreter.removeCodeContext(ctx.getId()); + +// 列出所有 Context +List contexts = client.codeInterpreter.listCodeContexts(); +for (Context c : contexts) { + System.out.println(c); +} + +// 获取主结果文本 +String mainText = result2.getText(); +System.out.println("Main result: " + mainText); + +// 流式执行(逐事件处理) +RunCodeRequest request = new RunCodeRequest("for i in range(5): print(i)", "python"); +client.codeInterpreter.runCodeStreaming(request, event -> { + if (event instanceof StdoutEvent) { + System.out.print(((StdoutEvent) event).getText()); + } else if (event instanceof ErrorEvent) { + System.err.println("Error: " + ((ErrorEvent) event).getError()); + } +}); +``` + +--- + +## 六、异常体系 | 异常类 | 说明 | |-------------------------|-------------------------------| @@ -298,7 +432,7 @@ wh.stop(); --- -## 六、资源管理 +## 七、资源管理 `RuntimeClient` 实现 `AutoCloseable`,`close()` 会释放: diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/RuntimeClient.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/RuntimeClient.java index 1b818ec..29b2603 100644 --- a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/RuntimeClient.java +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/RuntimeClient.java @@ -1,5 +1,7 @@ package io.openkruise.agents.client.runtime; +import io.openkruise.agents.client.runtime.RuntimeConfig.Builder; +import io.openkruise.agents.client.runtime.codeinterpreter.CodeInterpreter; import io.openkruise.agents.client.runtime.commands.Commands; import io.openkruise.agents.client.runtime.exceptions.K8sOperationException; import io.openkruise.agents.client.runtime.filesystem.Filesystem; @@ -14,6 +16,7 @@ public class RuntimeClient implements AutoCloseable { public final Commands commands; public final Filesystem files; + public final CodeInterpreter codeInterpreter; private final String sandboxID; private final RuntimeConfig config; @@ -27,6 +30,7 @@ private RuntimeClient(String sandboxID, RuntimeConfig config, this.commands = new Commands(sandboxID, config, httpClient, streamingClient); this.files = new Filesystem(sandboxID, config, httpClient, streamingClient); + this.codeInterpreter = new CodeInterpreter(sandboxID, config); } public static RuntimeClient create(String sandboxID, RuntimeConfig config) { @@ -46,7 +50,7 @@ public static RuntimeClient newFromK8s(String namespace, String sandboxName, Run // Rebuild config if runtimeToken is obtained if (runtimeToken != null && !runtimeToken.isEmpty()) { - config = new RuntimeConfig.Builder() + config = new Builder() .domain(config.getDomain()) .scheme(config.getScheme()) .runtimeUrl(config.getRuntimeUrl()) @@ -55,6 +59,9 @@ public static RuntimeClient newFromK8s(String namespace, String sandboxName, Run .headers(config.getHeaders()) .requestTimeoutMs(config.getRequestTimeoutMs()) .runtimeToken(runtimeToken) + .urlBuilder(config.getUrlBuilder()) + .sandboxPort(config.getSandboxPort()) + .codeInterpreterPort(config.getCodeInterpreterPort()) .build(); } diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/RuntimeConfig.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/RuntimeConfig.java index 641b4eb..6ccfc7b 100644 --- a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/RuntimeConfig.java +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/RuntimeConfig.java @@ -3,6 +3,7 @@ import com.google.protobuf.Message; import com.google.protobuf.MessageOrBuilder; import com.google.protobuf.util.JsonFormat; +import io.openkruise.agents.client.url.URLBuilder; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -14,7 +15,8 @@ import java.util.concurrent.TimeUnit; /** - * Runtime direct connection configuration with Builder pattern. Use subclass {@code E2bRuntimeConfig} for E2B mode. + * Runtime connection configuration with Builder pattern. + * Supports both direct runtime connection and E2B mode (via URLBuilder). */ public class RuntimeConfig { @@ -22,6 +24,8 @@ public class RuntimeConfig { private static final String DEFAULT_SCHEME = "http"; private static final long DEFAULT_REQUEST_TIMEOUT_MS = 60_000L; static final String DEFAULT_AUTH_HEADER = "Basic cm9vdDo="; + public static final int DEFAULT_RUNTIME_PORT = 49983; + public static final int DEFAULT_CODE_INTERPRETER_PORT = 49999; private final String domain; private final String scheme; @@ -31,6 +35,9 @@ public class RuntimeConfig { private final String apiKey; private final Map headers; private final long requestTimeoutMs; + private final URLBuilder urlBuilder; + private final int sandboxPort; + private final int codeInterpreterPort; protected RuntimeConfig(Builder builder) { this.domain = builder.domain; @@ -41,12 +48,18 @@ protected RuntimeConfig(Builder builder) { this.apiKey = builder.apiKey; this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers)); this.requestTimeoutMs = builder.requestTimeoutMs; + this.urlBuilder = builder.urlBuilder; + this.sandboxPort = builder.sandboxPort; + this.codeInterpreterPort = builder.codeInterpreterPort; } /** - * Runtime base URL, overridable by subclasses. Returns runtimeUrl if set, otherwise combines scheme+domain. + * Runtime base URL, uses URLBuilder if available, otherwise returns runtimeUrl or scheme://domain. */ public String getSandboxURL(String sandboxID) { + if (urlBuilder != null) { + return urlBuilder.buildSandboxURL(sandboxID); + } if (runtimeUrl != null && !runtimeUrl.isEmpty()) { return runtimeUrl; } @@ -55,7 +68,18 @@ public String getSandboxURL(String sandboxID) { } /** - * Builds common authentication headers (Authorization, X-Access-Token, X-API-Key, e2b-sandbox-id, etc.), extensible by subclasses. + * Code Interpreter URL for a specific sandbox, uses codeInterpreterPort (49999). + */ + public String getCodeInterpreterURL(String sandboxID) { + if (urlBuilder != null) { + return urlBuilder.buildCodeInterpreterURL(sandboxID); + } + // Fallback: use same base URL as sandbox + return getSandboxURL(sandboxID); + } + + /** + * Builds common authentication headers (Authorization, X-Access-Token, X-API-Key, e2b-sandbox-id, e2b-sandbox-port, etc.), extensible by subclasses. */ public Map getSandboxHeaders(String sandboxID) { Map result = new HashMap<>(5 + headers.size()); @@ -75,10 +99,26 @@ public Map getSandboxHeaders(String sandboxID) { result.put("e2b-sandbox-id", sandboxID); } + if (sandboxPort > 0) { + result.put("e2b-sandbox-port", String.valueOf(sandboxPort)); + } + result.putAll(headers); return result; } + /** + * Get headers for code interpreter operations. + * Overrides e2b-sandbox-port with codeInterpreterPort. + */ + public Map getCodeInterpreterHeaders(String sandboxID) { + Map result = new HashMap<>(getSandboxHeaders(sandboxID)); + if (codeInterpreterPort > 0) { + result.put("e2b-sandbox-port", String.valueOf(codeInterpreterPort)); + } + return result; + } + private static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8"); private static final MediaType CONNECT_PROTO_MEDIA_TYPE = MediaType.get("application/connect+proto"); private static final JsonFormat.Printer PROTO_PRINTER = JsonFormat.printer().omittingInsignificantWhitespace(); @@ -237,6 +277,18 @@ public long getRequestTimeoutMs() { return requestTimeoutMs; } + public int getSandboxPort() { + return sandboxPort; + } + + public int getCodeInterpreterPort() { + return codeInterpreterPort; + } + + public URLBuilder getUrlBuilder() { + return urlBuilder; + } + /** * Builder pattern, extensible by subclasses. */ @@ -249,6 +301,9 @@ public static class Builder { protected String apiKey; protected Map headers = new HashMap<>(); protected long requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS; + protected URLBuilder urlBuilder; + protected int sandboxPort = DEFAULT_RUNTIME_PORT; + protected int codeInterpreterPort = DEFAULT_CODE_INTERPRETER_PORT; public Builder() {} @@ -300,6 +355,21 @@ public Builder requestTimeoutMs(long requestTimeoutMs) { return this; } + public Builder urlBuilder(URLBuilder urlBuilder) { + this.urlBuilder = urlBuilder; + return this; + } + + public Builder sandboxPort(int sandboxPort) { + this.sandboxPort = sandboxPort; + return this; + } + + public Builder codeInterpreterPort(int codeInterpreterPort) { + this.codeInterpreterPort = codeInterpreterPort; + return this; + } + public RuntimeConfig build() { return new RuntimeConfig(this); } diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/CodeInterpreter.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/CodeInterpreter.java new file mode 100644 index 0000000..c01e912 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/CodeInterpreter.java @@ -0,0 +1,424 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +import io.openkruise.agents.client.runtime.RuntimeConfig; +import io.openkruise.agents.client.runtime.exceptions.SandboxException; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import okhttp3.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Code Interpreter for executing code in sandbox. + * Uses port 49999 (vs envd port 49983) for code execution. + * Response is NDJSON stream: each line is a separate JSON event. + */ +public class CodeInterpreter { + private static final Logger log = LoggerFactory.getLogger(CodeInterpreter.class); + private static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8"); + + private final String sandboxID; + private final RuntimeConfig runtimeConfig; + private final OkHttpClient httpClient; + + public CodeInterpreter(String sandboxID, RuntimeConfig runtimeConfig) { + this.sandboxID = sandboxID; + this.runtimeConfig = runtimeConfig; + this.httpClient = runtimeConfig.getOrCreateStreamingHttpClient(); + } + + /** + * Execute Python code in the sandbox (default language). + * + * @param code The Python code to execute + * @return Execution result containing stdout, stderr, results, and error + * @throws SandboxException if execution fails + */ + public Execution runCode(String code) throws SandboxException { + return runCode(code, RunCodeLanguage.PYTHON.getValue()); + } + + /** + * Execute code in the sandbox (blocking, collects all events into Execution). + * + * @param code The code to execute + * @param language The programming language (python, javascript, typescript, r, java, bash) + * @return Execution result containing stdout, stderr, results, and error + * @throws SandboxException if execution fails + */ + public Execution runCode(String code, String language) throws SandboxException { + return runCode(new RunCodeRequest(code, language)); + } + + /** + * Execute code in the sandbox with options (blocking, collects all events into Execution). + * If options contain callbacks (onStdout/onStderr/onResult/onError), they will be invoked in real-time. + * + * @param code The code to execute + * @param language The programming language + * @param options Execution options (working directory, environment variables, callbacks, etc.) + * @return Execution result + * @throws SandboxException if execution fails + */ + public Execution runCode(String code, String language, RunCodeOptions options) throws SandboxException { + RunCodeRequest request = new RunCodeRequest(code, language, options); + Execution execution = new Execution(); + + Consumer handler = event -> { + applyEventToExecution(execution, event); + if (options != null) { + invokeUserCallbacks(event, options); + } + }; + + runCodeStreaming(request, handler); + log.debug("Code execution completed, results: {}, error: {}", + execution.getResults().size(), execution.getError() != null); + return execution; + } + + /** + * Execute code using a RunCodeRequest (blocking, collects all events into Execution). + */ + public Execution runCode(RunCodeRequest request) throws SandboxException { + log.debug("Executing code in sandbox {}, language: {}", sandboxID, request.getLanguage()); + Execution execution = new Execution(); + runCodeStreaming(request, event -> applyEventToExecution(execution, event)); + log.debug("Code execution completed, results: {}, error: {}", + execution.getResults().size(), execution.getError() != null); + return execution; + } + + /** + * Invoke user-defined callbacks from RunCodeOptions based on event type. + */ + private void invokeUserCallbacks(ExecutionEvent event, RunCodeOptions options) { + if (event instanceof StdoutEvent && options.getOnStdout() != null) { + options.getOnStdout().accept((StdoutEvent) event); + } else if (event instanceof StderrEvent && options.getOnStderr() != null) { + options.getOnStderr().accept((StderrEvent) event); + } else if (event instanceof ResultEvent && options.getOnResult() != null) { + options.getOnResult().accept(((ResultEvent) event).getResult()); + } else if (event instanceof ErrorEvent && options.getOnError() != null) { + options.getOnError().accept(((ErrorEvent) event).getError()); + } + } + + /** + * Execute code and process events as they arrive (streaming, low memory footprint). + * + * @param request The code execution request + * @param eventHandler Callback to handle each event + * @throws SandboxException if execution fails + */ + public void runCodeStreaming(RunCodeRequest request, Consumer eventHandler) + throws SandboxException { + String url = runtimeConfig.getCodeInterpreterURL(sandboxID) + "/execute"; + log.debug("Sending streaming request to: {}", url); + + okhttp3.Request httpRequest = new okhttp3.Request.Builder() + .url(url) + .post(RequestBody.create(request.toJson(), JSON_MEDIA_TYPE)) + .build(); + httpRequest = addCodeInterpreterHeaders(httpRequest); + + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : "Unknown error"; + throw new SandboxException("Code execution failed: " + response.code() + " - " + errorBody); + } + + ResponseBody body = response.body(); + if (body == null) { + return; + } + + // Parse NDJSON stream + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(body.byteStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.trim().isEmpty()) {continue;} + ExecutionEvent event = parseNDJSONLine(line); + if (event != null) { + eventHandler.accept(event); + } + } + } + + } catch (IOException e) { + throw new SandboxException("Failed to execute code", e); + } + } + + /** + * Apply a stream event to the Execution object. + */ + private void applyEventToExecution(Execution execution, ExecutionEvent event) { + if (event instanceof NumberOfExecutionsEvent) { + execution.setExecutionCount(((NumberOfExecutionsEvent)event).getExecutionCount()); + } else if (event instanceof StdoutEvent) { + execution.getLogs().getStdout().add(((StdoutEvent)event).getText()); + } else if (event instanceof StderrEvent) { + execution.getLogs().getStderr().add(((StderrEvent)event).getText()); + } else if (event instanceof ResultEvent) { + execution.getResults().add(((ResultEvent)event).getResult()); + } else if (event instanceof ErrorEvent) { + execution.setError(((ErrorEvent)event).getError()); + } + } + + /** + * Parse a single NDJSON line into an ExecutionEvent. + */ + private ExecutionEvent parseNDJSONLine(String line) { + try { + JsonObject json = JsonParser.parseString(line).getAsJsonObject(); + String type = json.has("type") ? json.get("type").getAsString() : ""; + + switch (type) { + case "number_of_executions": + int count = json.has("execution_count") ? json.get("execution_count").getAsInt() : 0; + return new NumberOfExecutionsEvent(count); + + case "stdout": + String stdoutText = json.has("text") ? json.get("text").getAsString() : ""; + String stdoutTimestamp = json.has("timestamp") ? json.get("timestamp").getAsString() : ""; + return new StdoutEvent(stdoutText, stdoutTimestamp); + + case "stderr": + String stderrText = json.has("text") ? json.get("text").getAsString() : ""; + String stderrTimestamp = json.has("timestamp") ? json.get("timestamp").getAsString() : ""; + return new StderrEvent(stderrText, stderrTimestamp); + + case "result": + return new ResultEvent(parseResult(json)); + + case "error": + String name = json.has("name") ? json.get("name").getAsString() : ""; + String value = json.has("value") ? json.get("value").getAsString() : ""; + String traceback = json.has("traceback") ? json.get("traceback").getAsString() : ""; + return new ErrorEvent(new ExecutionError(name, value, traceback)); + + case "end_of_execution": + return new EndOfExecutionEvent(); + + default: + return null; + } + } catch (Exception e) { + log.warn("Failed to parse NDJSON line: {}, error: {}", line, e.getMessage()); + return null; + } + } + + /** + * Parse a result object from JSON fields. + */ + private Result parseResult(JsonObject json) { + Result result = new Result(); + + if (json.has("text")) {result.setText(json.get("text").getAsString());} + if (json.has("html")) {result.setHtml(json.get("html").getAsString());} + if (json.has("markdown")) {result.setMarkdown(json.get("markdown").getAsString());} + if (json.has("svg")) {result.setSvg(json.get("svg").getAsString());} + if (json.has("png")) {result.setPng(json.get("png").getAsString());} + if (json.has("jpeg")) {result.setJpeg(json.get("jpeg").getAsString());} + if (json.has("pdf")) {result.setPdf(json.get("pdf").getAsString());} + if (json.has("latex")) {result.setLatex(json.get("latex").getAsString());} + if (json.has("javascript")) {result.setJavascript(json.get("javascript").getAsString());} + + if (json.has("json") && json.get("json").isJsonObject()) { + result.setJson(parseJsonObject(json.getAsJsonObject("json"))); + } + if (json.has("data") && json.get("data").isJsonObject()) { + result.setData(parseJsonObject(json.getAsJsonObject("data"))); + } + if (json.has("extra") && json.get("extra").isJsonObject()) { + result.setExtra(parseJsonObject(json.getAsJsonObject("extra"))); + } + if (json.has("is_main_result")) { + result.setMainResult(json.get("is_main_result").getAsBoolean()); + } + + return result; + } + + /** + * Parse a JSON object into a Map. + */ + private Map parseJsonObject(JsonObject jsonObject) { + Map map = new HashMap<>(); + for (Map.Entry entry : jsonObject.entrySet()) { + JsonElement value = entry.getValue(); + if (value.isJsonPrimitive()) { + if (value.getAsJsonPrimitive().isString()) { + map.put(entry.getKey(), value.getAsString()); + } else if (value.getAsJsonPrimitive().isNumber()) { + map.put(entry.getKey(), value.getAsNumber()); + } else if (value.getAsJsonPrimitive().isBoolean()) { + map.put(entry.getKey(), value.getAsBoolean()); + } + } else if (value.isJsonObject()) { + map.put(entry.getKey(), parseJsonObject(value.getAsJsonObject())); + } else if (value.isJsonArray()) { + List list = new ArrayList<>(); + for (JsonElement element : value.getAsJsonArray()) { + if (element.isJsonPrimitive()) { + if (element.getAsJsonPrimitive().isString()) { + list.add(element.getAsString()); + } else if (element.getAsJsonPrimitive().isNumber()) { + list.add(element.getAsNumber()); + } else if (element.getAsJsonPrimitive().isBoolean()) { + list.add(element.getAsBoolean()); + } + } else if (element.isJsonObject()) { + list.add(parseJsonObject(element.getAsJsonObject())); + } else if (element.isJsonNull()) { + list.add(null); + } + } + map.put(entry.getKey(), list); + } else if (value.isJsonNull()) { + map.put(entry.getKey(), null); + } + } + return map; + } + + private Request addCodeInterpreterHeaders(Request original) { + Map hdrs = runtimeConfig.getCodeInterpreterHeaders(sandboxID); + Request.Builder builder = original.newBuilder(); + for (Map.Entry entry : hdrs.entrySet()) { + builder.addHeader(entry.getKey(), entry.getValue()); + } + return builder.build(); + } + + /** + * Create a new code context with specified working directory and language. + * + * @param cwd Working directory for the context (optional, defaults to /home/user) + * @param language Programming language for the context (optional, defaults to python) + * @return Context object with ID, language, and cwd + * @throws SandboxException if creation fails + */ + public Context createCodeContext(String cwd, String language) throws SandboxException { + String url = runtimeConfig.getCodeInterpreterURL(sandboxID) + "/contexts"; + log.debug("Creating code context at: {}, cwd: {}, language: {}", url, cwd, language); + + JsonObject json = new JsonObject(); + if (cwd != null) { + json.addProperty("cwd", cwd); + } + if (language != null) { + json.addProperty("language", language); + } + + okhttp3.Request httpRequest = new okhttp3.Request.Builder() + .url(url) + .post(RequestBody.create(json.toString(), JSON_MEDIA_TYPE)) + .build(); + httpRequest = addCodeInterpreterHeaders(httpRequest); + + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : "Unknown error"; + throw new SandboxException("Create context failed: " + response.code() + " - " + errorBody); + } + + ResponseBody body = response.body(); + if (body == null) { + throw new SandboxException("Create context failed: empty response"); + } + + JsonObject respJson = JsonParser.parseString(body.string()).getAsJsonObject(); + String id = respJson.has("id") ? respJson.get("id").getAsString() : ""; + String lang = respJson.has("language") ? respJson.get("language").getAsString() : "python"; + String contextCwd = respJson.has("cwd") ? respJson.get("cwd").getAsString() : "/home/user"; + + return new Context(id, lang, contextCwd); + } catch (IOException e) { + throw new SandboxException("Failed to create context", e); + } + } + + /** + * Remove a code context by ID. + * + * @param contextId Context ID to remove + * @throws SandboxException if removal fails + */ + public void removeCodeContext(String contextId) throws SandboxException { + String url = runtimeConfig.getCodeInterpreterURL(sandboxID) + "/contexts/" + contextId; + log.debug("Removing code context: {}", url); + + okhttp3.Request httpRequest = new okhttp3.Request.Builder() + .url(url) + .delete() + .build(); + httpRequest = addCodeInterpreterHeaders(httpRequest); + + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : "Unknown error"; + throw new SandboxException("Remove context failed: " + response.code() + " - " + errorBody); + } + } catch (IOException e) { + throw new SandboxException("Failed to remove context", e); + } + } + + /** + * List all code contexts. + * + * @return List of Context objects + * @throws SandboxException if listing fails + */ + public List listCodeContexts() throws SandboxException { + String url = runtimeConfig.getCodeInterpreterURL(sandboxID) + "/contexts"; + log.debug("Listing code contexts: {}", url); + + okhttp3.Request httpRequest = new okhttp3.Request.Builder() + .url(url) + .get() + .build(); + httpRequest = addCodeInterpreterHeaders(httpRequest); + + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : "Unknown error"; + throw new SandboxException("List contexts failed: " + response.code() + " - " + errorBody); + } + + ResponseBody body = response.body(); + if (body == null) { + return new ArrayList<>(); + } + + List contexts = new ArrayList<>(); + com.google.gson.JsonArray arr = JsonParser.parseString(body.string()).getAsJsonArray(); + for (JsonElement elem : arr) { + JsonObject obj = elem.getAsJsonObject(); + String id = obj.has("id") ? obj.get("id").getAsString() : ""; + String lang = obj.has("language") ? obj.get("language").getAsString() : "python"; + String cwd = obj.has("cwd") ? obj.get("cwd").getAsString() : "/home/user"; + contexts.add(new Context(id, lang, cwd)); + } + return contexts; + } catch (IOException e) { + throw new SandboxException("Failed to list contexts", e); + } + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/Context.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/Context.java new file mode 100644 index 0000000..0d83138 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/Context.java @@ -0,0 +1,34 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +/** + * Represents a context for code execution. + * A context maintains its own working directory and language environment. + */ +public class Context { + private final String id; + private final String language; + private final String cwd; + + public Context(String id, String language, String cwd) { + this.id = id; + this.language = language; + this.cwd = cwd; + } + + public String getId() { + return id; + } + + public String getLanguage() { + return language; + } + + public String getCwd() { + return cwd; + } + + @Override + public String toString() { + return String.format("Context{id='%s', language='%s', cwd='%s'}", id, language, cwd); + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/EndOfExecutionEvent.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/EndOfExecutionEvent.java new file mode 100644 index 0000000..6fc8cff --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/EndOfExecutionEvent.java @@ -0,0 +1,15 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +/** + * End of execution stream event. + */ +public class EndOfExecutionEvent extends ExecutionEvent { + public EndOfExecutionEvent() { + super("end_of_execution"); + } + + @Override + public String toString() { + return "EndOfExecutionEvent{}"; + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/ErrorEvent.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/ErrorEvent.java new file mode 100644 index 0000000..9e5af1c --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/ErrorEvent.java @@ -0,0 +1,20 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +/** + * Execution error event. + */ +public class ErrorEvent extends ExecutionEvent { + private final ExecutionError error; + + public ErrorEvent(ExecutionError error) { + super("error"); + this.error = error; + } + + public ExecutionError getError() { return error; } + + @Override + public String toString() { + return "ErrorEvent{error=" + error + "}"; + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/Execution.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/Execution.java new file mode 100644 index 0000000..7cb6f91 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/Execution.java @@ -0,0 +1,85 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Represents the result of a code execution. + */ +public class Execution { + private List results; + private Logs logs; + private ExecutionError error; + /** Execution count, null if no number_of_executions event was received. */ + private Integer executionCount; + + public Execution() { + this.results = new ArrayList<>(); + this.logs = new Logs(); + } + + public List getResults() { + return results; + } + + public void setResults(List results) { + this.results = results; + } + + public Logs getLogs() { + return logs; + } + + public void setLogs(Logs logs) { + this.logs = logs; + } + + public ExecutionError getError() { + return error; + } + + public void setError(ExecutionError error) { + this.error = error; + } + + public Integer getExecutionCount() { + return executionCount; + } + + public void setExecutionCount(Integer executionCount) { + this.executionCount = executionCount; + } + + /** + * Returns the text representation of the main result. + * @return the text or null if not found + */ + public String getText() { + for (Result result : results) { + if (result.isMainResult() && result.getText() != null) { + return result.getText(); + } + } + return null; + } + + /** + * Returns the text representation of the main result as Optional. + * @return Optional containing the text if found, empty otherwise + */ + public Optional findText() { + return results.stream() + .filter(Result::isMainResult) + .map(Result::getText) + .filter(Objects::nonNull) + .findFirst(); + } + + @Override + public String toString() { + return String.format("Execution(results: %d, logs: %s, error: %s)", + results.size(), logs, error); + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/ExecutionError.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/ExecutionError.java new file mode 100644 index 0000000..1948867 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/ExecutionError.java @@ -0,0 +1,33 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +/** + * Represents an error that occurred during the execution of code. + */ +public class ExecutionError { + private final String name; + private final String value; + private final String traceback; + + public ExecutionError(String name, String value, String traceback) { + this.name = name; + this.value = value; + this.traceback = traceback; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public String getTraceback() { + return traceback; + } + + @Override + public String toString() { + return String.format("%s: %s\n%s", name, value, traceback); + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/ExecutionEvent.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/ExecutionEvent.java new file mode 100644 index 0000000..9887306 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/ExecutionEvent.java @@ -0,0 +1,20 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +/** + * Base class for NDJSON stream events from the /execute endpoint. + * Each line in the response is a JSON object with a "type" field. + */ +public abstract class ExecutionEvent { + private final String type; + + protected ExecutionEvent(String type) { + this.type = type; + } + + public String getType() { return type; } + + @Override + public String toString() { + return "ExecutionEvent{type='" + type + "'}"; + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/Logs.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/Logs.java new file mode 100644 index 0000000..a703caf --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/Logs.java @@ -0,0 +1,41 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Data printed to stdout and stderr during execution. + *

+ * NOT thread-safe. The caller must ensure that events are applied + * from a single thread only (e.g., the streaming reader thread in CodeInterpreter). + */ +public class Logs { + private List stdout; + private List stderr; + + public Logs() { + this.stdout = new ArrayList<>(); + this.stderr = new ArrayList<>(); + } + + public List getStdout() { + return stdout; + } + + public void setStdout(List stdout) { + this.stdout = stdout != null ? stdout : new ArrayList<>(); + } + + public List getStderr() { + return stderr; + } + + public void setStderr(List stderr) { + this.stderr = stderr != null ? stderr : new ArrayList<>(); + } + + @Override + public String toString() { + return String.format("Logs(stdout: %s, stderr: %s)", stdout, stderr); + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/NumberOfExecutionsEvent.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/NumberOfExecutionsEvent.java new file mode 100644 index 0000000..b85d6ca --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/NumberOfExecutionsEvent.java @@ -0,0 +1,20 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +/** + * Execution count update event. + */ +public class NumberOfExecutionsEvent extends ExecutionEvent { + private final int executionCount; + + public NumberOfExecutionsEvent(int executionCount) { + super("number_of_executions"); + this.executionCount = executionCount; + } + + public int getExecutionCount() { return executionCount; } + + @Override + public String toString() { + return "NumberOfExecutionsEvent{executionCount=" + executionCount + "}"; + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/OutputEvent.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/OutputEvent.java new file mode 100644 index 0000000..94d4616 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/OutputEvent.java @@ -0,0 +1,18 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +/** + * Base class for output events (stdout/stderr). + */ +public abstract class OutputEvent extends ExecutionEvent { + private final String text; + private final String timestamp; + + protected OutputEvent(String type, String text, String timestamp) { + super(type); + this.text = text; + this.timestamp = timestamp; + } + + public String getText() { return text; } + public String getTimestamp() { return timestamp; } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/Result.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/Result.java new file mode 100644 index 0000000..8b31ded --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/Result.java @@ -0,0 +1,156 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Represents the data to be displayed as a result of executing code. + * Similar to Jupyter notebook output structure. + */ +public class Result { + private String text; + private String html; + private String markdown; + private String svg; + private String png; + private String jpeg; + private String pdf; + private String latex; + private Map json; + private String javascript; + private Map data; + private boolean mainResult; + private Map extra; + + public Result() { + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getHtml() { + return html; + } + + public void setHtml(String html) { + this.html = html; + } + + public String getMarkdown() { + return markdown; + } + + public void setMarkdown(String markdown) { + this.markdown = markdown; + } + + public String getSvg() { + return svg; + } + + public void setSvg(String svg) { + this.svg = svg; + } + + public String getPng() { + return png; + } + + public void setPng(String png) { + this.png = png; + } + + public String getJpeg() { + return jpeg; + } + + public void setJpeg(String jpeg) { + this.jpeg = jpeg; + } + + public String getPdf() { + return pdf; + } + + public void setPdf(String pdf) { + this.pdf = pdf; + } + + public String getLatex() { + return latex; + } + + public void setLatex(String latex) { + this.latex = latex; + } + + public Map getJson() { + return json; + } + + public void setJson(Map json) { + this.json = json; + } + + public String getJavascript() { + return javascript; + } + + public void setJavascript(String javascript) { + this.javascript = javascript; + } + + public Map getData() { + return data; + } + + public void setData(Map data) { + this.data = data; + } + + public boolean isMainResult() { + return mainResult; + } + + public void setMainResult(boolean mainResult) { + this.mainResult = mainResult; + } + + public Map getExtra() { + return extra; + } + + public void setExtra(Map extra) { + this.extra = extra; + } + + @Override + public String toString() { + if (text != null) { + return text; + } + return "Result(formats: " + getFormats() + ")"; + } + + public List getFormats() { + List formats = new ArrayList<>(); + if (text != null) {formats.add("text");} + if (html != null) {formats.add("html");} + if (markdown != null) {formats.add("markdown");} + if (svg != null) {formats.add("svg");} + if (png != null) {formats.add("png");} + if (jpeg != null) {formats.add("jpeg");} + if (pdf != null) {formats.add("pdf");} + if (latex != null) {formats.add("latex");} + if (json != null) {formats.add("json");} + if (javascript != null) {formats.add("javascript");} + if (data != null) {formats.add("data");} + return formats; + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/ResultEvent.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/ResultEvent.java new file mode 100644 index 0000000..c8fd572 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/ResultEvent.java @@ -0,0 +1,20 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +/** + * Execution result event (text, html, image, etc.). + */ +public class ResultEvent extends ExecutionEvent { + private final Result result; + + public ResultEvent(Result result) { + super("result"); + this.result = result; + } + + public Result getResult() { return result; } + + @Override + public String toString() { + return "ResultEvent{result=" + result + "}"; + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/RunCodeLanguage.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/RunCodeLanguage.java new file mode 100644 index 0000000..ca251f3 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/RunCodeLanguage.java @@ -0,0 +1,28 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +/** + * Supported programming languages for code execution. + */ +public enum RunCodeLanguage { + PYTHON("python"), + JAVASCRIPT("javascript"), + TYPESCRIPT("typescript"), + R("r"), + JAVA("java"), + BASH("bash"); + + private final String value; + + RunCodeLanguage(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/RunCodeOptions.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/RunCodeOptions.java new file mode 100644 index 0000000..8da33ff --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/RunCodeOptions.java @@ -0,0 +1,106 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +import java.util.Map; +import java.util.function.Consumer; + +/** + * Options for code execution. + */ +public class RunCodeOptions { + private String cwd; + private Map envVars; + private Long timeoutMs; + private String contextId; + private Consumer onStdout; + private Consumer onStderr; + private Consumer onResult; + private Consumer onError; + + public RunCodeOptions() { + } + + public String getCwd() { + return cwd; + } + + public RunCodeOptions setCwd(String cwd) { + this.cwd = cwd; + return this; + } + + public Map getEnvVars() { + return envVars; + } + + public RunCodeOptions setEnvVars(Map envVars) { + this.envVars = envVars; + return this; + } + + public Long getTimeoutMs() { + return timeoutMs; + } + + public RunCodeOptions setTimeoutMs(Long timeoutMs) { + this.timeoutMs = timeoutMs; + return this; + } + + public String getContextId() { + return contextId; + } + + public RunCodeOptions setContextId(String contextId) { + this.contextId = contextId; + return this; + } + + public Consumer getOnStdout() { + return onStdout; + } + + public RunCodeOptions setOnStdout(Consumer onStdout) { + this.onStdout = onStdout; + return this; + } + + public Consumer getOnStderr() { + return onStderr; + } + + public RunCodeOptions setOnStderr(Consumer onStderr) { + this.onStderr = onStderr; + return this; + } + + public Consumer getOnResult() { + return onResult; + } + + public RunCodeOptions setOnResult(Consumer onResult) { + this.onResult = onResult; + return this; + } + + public Consumer getOnError() { + return onError; + } + + public RunCodeOptions setOnError(Consumer onError) { + this.onError = onError; + return this; + } + + /** + * Returns true if any callback is set. + */ + public boolean hasCallbacks() { + return onStdout != null || onStderr != null || onResult != null || onError != null; + } + + @Override + public String toString() { + return String.format("RunCodeOptions{cwd='%s', envVars=%s, timeoutMs=%s, contextId='%s'}", + cwd, envVars, timeoutMs, contextId); + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/RunCodeRequest.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/RunCodeRequest.java new file mode 100644 index 0000000..2a26d4f --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/RunCodeRequest.java @@ -0,0 +1,103 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +import com.google.gson.JsonObject; + +import java.util.HashMap; +import java.util.Map; + +/** + * Request object for code execution. + * Encapsulates all parameters needed for the /execute endpoint. + */ +public class RunCodeRequest { + private final String code; + private final String language; + private final String cwd; + private final Map envVars; + private final Long timeoutMs; + private final String contextId; + + public RunCodeRequest(String code, String language) { + this(code, language, null, null, null); + } + + public RunCodeRequest(String code, String language, RunCodeOptions options) { + this(code, language, + options != null ? options.getCwd() : null, + options != null ? options.getEnvVars() : null, + options != null ? options.getTimeoutMs() : null, + options != null ? options.getContextId() : null); + } + + public RunCodeRequest(String code, String language, String cwd, Map envVars) { + this(code, language, cwd, envVars, null, null); + } + + public RunCodeRequest(String code, String language, String cwd, Map envVars, Long timeoutMs) { + this(code, language, cwd, envVars, timeoutMs, null); + } + + public RunCodeRequest(String code, String language, String cwd, Map envVars, Long timeoutMs, String contextId) { + if (code == null || code.trim().isEmpty()) { + throw new IllegalArgumentException("Code cannot be null or empty"); + } + if (language == null || language.trim().isEmpty()) { + throw new IllegalArgumentException("Language cannot be null or empty"); + } + this.code = code; + this.language = language; + this.cwd = cwd; + this.envVars = envVars != null ? validateEnvVars(envVars) : new HashMap<>(); + this.timeoutMs = timeoutMs; + this.contextId = contextId; + } + + private Map validateEnvVars(Map envVars) { + for (Map.Entry entry : envVars.entrySet()) { + if (entry.getKey() == null || entry.getKey().trim().isEmpty()) { + throw new IllegalArgumentException("Environment variable key cannot be null or empty"); + } + if (entry.getValue() == null) { + throw new IllegalArgumentException("Environment variable value cannot be null for key: " + entry.getKey()); + } + } + return new HashMap<>(envVars); + } + + public String getCode() { return code; } + public String getLanguage() { return language; } + public String getCwd() { return cwd; } + public Map getEnvVars() { return envVars; } + public Long getTimeoutMs() { return timeoutMs; } + public String getContextId() { return contextId; } + + /** + * Serialize to JSON string for the /execute endpoint using Gson. + */ + public String toJson() { + JsonObject json = new JsonObject(); + json.addProperty("code", code); + + // Only one of context_id or language can be provided + if (contextId != null) { + json.addProperty("context_id", contextId); + } else { + json.addProperty("language", language); + } + + if (cwd != null) { + json.addProperty("cwd", cwd); + } + if (!envVars.isEmpty()) { + JsonObject envs = new JsonObject(); + for (Map.Entry entry : envVars.entrySet()) { + envs.addProperty(entry.getKey(), entry.getValue()); + } + json.add("env_vars", envs); + } + if (timeoutMs != null) { + json.addProperty("timeout", timeoutMs / 1000.0); + } + return json.toString(); + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/StderrEvent.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/StderrEvent.java new file mode 100644 index 0000000..72348c1 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/StderrEvent.java @@ -0,0 +1,15 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +/** + * Stderr output event. + */ +public class StderrEvent extends OutputEvent { + public StderrEvent(String text, String timestamp) { + super("stderr", text, timestamp); + } + + @Override + public String toString() { + return "StderrEvent{text='" + getText() + "', timestamp='" + getTimestamp() + "'}"; + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/StdoutEvent.java b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/StdoutEvent.java new file mode 100644 index 0000000..030f1b7 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/runtime/codeinterpreter/StdoutEvent.java @@ -0,0 +1,15 @@ +package io.openkruise.agents.client.runtime.codeinterpreter; + +/** + * Stdout output event. + */ +public class StdoutEvent extends OutputEvent { + public StdoutEvent(String text, String timestamp) { + super("stdout", text, timestamp); + } + + @Override + public String toString() { + return "StdoutEvent{text='" + getText() + "', timestamp='" + getTimestamp() + "'}"; + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/url/E2BURLBuilder.java b/k8s/java/src/main/java/io/openkruise/agents/client/url/E2BURLBuilder.java new file mode 100644 index 0000000..cc85db4 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/url/E2BURLBuilder.java @@ -0,0 +1,128 @@ +package io.openkruise.agents.client.url; + +import io.openkruise.agents.client.e2b.ConnectionConfig; +import io.openkruise.agents.client.runtime.RuntimeConfig; + +/** + * URL builder for E2B platform supporting NATIVE and PRIVATE protocols. + * + * NATIVE mode: + * - API URL: ://api. + * - Sandbox URL: ://-. + * - Code URL: ://-. + * + * PRIVATE mode: + * - API URL: :///kruise/api + * - Sandbox URL: :///kruise// + * - Code URL: :///kruise// + */ +public class E2BURLBuilder implements URLBuilder { + + private final String scheme; + private final String domain; + private final ConnectionConfig.Protocol protocol; + private final int runtimePort; + private final int codeInterpreterPort; + private final String customApiURL; + private final String customSandboxBaseURL; + + public E2BURLBuilder(String scheme, String domain, ConnectionConfig.Protocol protocol, + int runtimePort, int codeInterpreterPort, + String customApiURL, String customSandboxBaseURL) { + this.scheme = (scheme != null && !scheme.isEmpty()) ? scheme : "https"; + this.domain = domain; + this.protocol = protocol; + this.runtimePort = runtimePort; + this.codeInterpreterPort = codeInterpreterPort; + this.customApiURL = customApiURL; + this.customSandboxBaseURL = customSandboxBaseURL; + } + + @Override + public String buildAPIURL() { + if (customApiURL != null && !customApiURL.isEmpty()) { + return customApiURL; + } + + if (protocol == ConnectionConfig.Protocol.PRIVATE) { + return String.format("%s://%s/kruise/api", scheme, domain); + } + return String.format("%s://api.%s", scheme, domain); + } + + @Override + public String buildSandboxURL(String sandboxID) { + return buildURLWithPort(sandboxID, runtimePort); + } + + @Override + public String buildCodeInterpreterURL(String sandboxID) { + return buildURLWithPort(sandboxID, codeInterpreterPort); + } + + @Override + public String buildURLWithPort(String sandboxID, int port) { + // Priority 1: customSandboxBaseURL (explicit override) — return directly + if (customSandboxBaseURL != null && !customSandboxBaseURL.isEmpty()) { + return customSandboxBaseURL.replaceAll("/+$", ""); + } + + // Priority 2: protocol + domain assembly + return (protocol == ConnectionConfig.Protocol.PRIVATE) + ? String.format("%s://%s/kruise/%s/%d", scheme, domain, sandboxID, port) + : String.format("%s://%d-%s.%s", scheme, port, sandboxID, domain); + } + + /** + * Builder for E2BURLBuilder. + */ + public static class Builder { + private String scheme = "https"; + private String domain; + private ConnectionConfig.Protocol protocol = ConnectionConfig.Protocol.NATIVE; + private int runtimePort = RuntimeConfig.DEFAULT_RUNTIME_PORT; + private int codeInterpreterPort = RuntimeConfig.DEFAULT_CODE_INTERPRETER_PORT; + private String customApiURL; + private String customSandboxBaseURL; + + public Builder scheme(String scheme) { + this.scheme = scheme; + return this; + } + + public Builder domain(String domain) { + this.domain = domain; + return this; + } + + public Builder protocol(ConnectionConfig.Protocol protocol) { + this.protocol = protocol; + return this; + } + + public Builder runtimePort(int runtimePort) { + this.runtimePort = runtimePort; + return this; + } + + public Builder codeInterpreterPort(int codeInterpreterPort) { + this.codeInterpreterPort = codeInterpreterPort; + return this; + } + + public Builder customApiURL(String customApiURL) { + this.customApiURL = customApiURL; + return this; + } + + public Builder customSandboxBaseURL(String customSandboxBaseURL) { + this.customSandboxBaseURL = customSandboxBaseURL; + return this; + } + + public E2BURLBuilder build() { + return new E2BURLBuilder(scheme, domain, protocol, runtimePort, + codeInterpreterPort, customApiURL, customSandboxBaseURL); + } + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/url/RuntimeURLBuilder.java b/k8s/java/src/main/java/io/openkruise/agents/client/url/RuntimeURLBuilder.java new file mode 100644 index 0000000..43d0991 --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/url/RuntimeURLBuilder.java @@ -0,0 +1,85 @@ +package io.openkruise.agents.client.url; + +/** + * URL builder for Runtime direct connection mode. + * + * Runtime mode uses a single base URL for all operations. + * Port differentiation (49983 for commands/files, 49999 for code) is handled via + * the "e2b-sandbox-port" header in RuntimeConfig, not in the URL itself. + */ +public class RuntimeURLBuilder implements URLBuilder { + + private final String scheme; + private final String domain; + private final String runtimeUrl; + + public RuntimeURLBuilder(String scheme, String domain, String runtimeUrl) { + this.scheme = (scheme != null && !scheme.isEmpty()) ? scheme : "http"; + this.domain = domain; + this.runtimeUrl = runtimeUrl; + } + + @Override + public String buildAPIURL() { + // Runtime mode doesn't have a separate API URL + // Returns the base runtime URL + return getBaseURL(); + } + + @Override + public String buildSandboxURL(String sandboxID) { + // For runtime mode, the URL is the same, port is specified in header + return getBaseURL(); + } + + @Override + public String buildCodeInterpreterURL(String sandboxID) { + // For runtime mode, the URL is the same, port is specified in header + return getBaseURL(); + } + + @Override + public String buildURLWithPort(String sandboxID, int port) { + // For runtime mode, the URL is the same, port is specified in header + return getBaseURL(); + } + + /** + * Get the base URL for all operations. + * Port differentiation is handled via headers. + */ + private String getBaseURL() { + if (runtimeUrl != null && !runtimeUrl.isEmpty()) { + return runtimeUrl; + } + return String.format("%s://%s", scheme, domain); + } + + /** + * Builder for RuntimeURLBuilder. + */ + public static class Builder { + private String scheme = "http"; + private String domain; + private String runtimeUrl; + + public Builder scheme(String scheme) { + this.scheme = scheme; + return this; + } + + public Builder domain(String domain) { + this.domain = domain; + return this; + } + + public Builder runtimeUrl(String runtimeUrl) { + this.runtimeUrl = runtimeUrl; + return this; + } + + public RuntimeURLBuilder build() { + return new RuntimeURLBuilder(scheme, domain, runtimeUrl); + } + } +} diff --git a/k8s/java/src/main/java/io/openkruise/agents/client/url/URLBuilder.java b/k8s/java/src/main/java/io/openkruise/agents/client/url/URLBuilder.java new file mode 100644 index 0000000..a178f5b --- /dev/null +++ b/k8s/java/src/main/java/io/openkruise/agents/client/url/URLBuilder.java @@ -0,0 +1,40 @@ +package io.openkruise.agents.client.url; + +/** + * URL builder interface for constructing sandbox and API URLs. + * Provides a unified contract for different URL construction strategies. + */ +public interface URLBuilder { + + /** + * Build API base URL for control plane operations. + * + * @return API base URL + */ + String buildAPIURL(); + + /** + * Build sandbox URL for envd operations (port 49983). + * + * @param sandboxID sandbox identifier + * @return sandbox URL for commands and files + */ + String buildSandboxURL(String sandboxID); + + /** + * Build code interpreter URL for code execution (port 49999). + * + * @param sandboxID sandbox identifier + * @return code interpreter URL + */ + String buildCodeInterpreterURL(String sandboxID); + + /** + * Build URL with custom port. + * + * @param sandboxID sandbox identifier + * @param port custom port number + * @return URL with specified port + */ + String buildURLWithPort(String sandboxID, int port); +}