Skip to content

Commit b2e266f

Browse files
Merge pull request #14 from questcollector/feat/jupyter
add Jupyter server code executor
2 parents 561ab99 + be6eb43 commit b2e266f

File tree

21 files changed

+1564
-144
lines changed

21 files changed

+1564
-144
lines changed

python/packages/autogen-kubernetes-mcp/README.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,52 @@ uv pip install autogen-kubernetes-mcp
2323
3. Run via `uvx`
2424

2525
```bash
26-
uvx autogen-kubernetes-mcp --namespace my-namespace --image python:3.11-slim
26+
uvx autogen-kubernetes-mcp commandline --namespace my-namespace --image python:3.11-slim
2727
```
28-
> Note: When using `uvx`, arguments must be passed after `--`.
2928

3029
4. Run via Python module
3130

3231
```bash
33-
python -m autogen_kubernetes_mcp --namespace my-namespace --image python:3.11-slim
32+
python -m autogen_kubernetes_mcp commandline --namespace my-namespace --image python:3.11-slim
3433
```
3534

3635
## Command-line Arguments
3736

38-
Command-line arguments are used when creating PodCommandLineCodeExecutor and MCP server.
37+
Command-line arguments are used when creating `PodCommandLineCodeExecutor`/`PodJupyterCodeExecutor` and MCP server.
38+
39+
`type` arguments: jupyter type creates `PodJupyterCodeExecutor` for python code executor tool.
40+
41+
MCP server have to run in same kubernetes cluster because `PodJupyterCodeExecutor` only uses service FQDN to connect jupyter server.
42+
43+
Not validated for non-text outputs yet.
44+
45+
```mermaid
46+
flowchart LR
47+
subgraph K8sCluster["Kubernetes Cluster"]
48+
E[PodJupyterCodeExecutor]
49+
J[JupyterServerPod]
50+
end
51+
52+
E --generate--> J
53+
```
3954

4055
All the arguments are optional
4156

4257
|Argument|Description|Default|
4358
|--|--|--|
59+
|`type`|Code Executor type, commandline(stateless), jupyter(stateful) supported||
4460
|`--host`|MCP server host address|`0.0.0.0`|
4561
|`--port`|MCP server port|`8000`|
46-
|`--kubeconfig`|Path to the kubeconfig file|(auto-detected)|
47-
|`--image`|Pod container image name|`python:3-slim`|
62+
|`--kubeconfig`|Path to the kubeconfig file|`None`(auto-detected)|
63+
|`--image`|Pod container image name|`None`|
4864
|`--pod-name`|Pod name|(auto-generated)|
4965
|`--timeout`|Code execution timeout(seconds)|`60`|
5066
|`--workspace-path`|Path inside the container where scripts are stored|`/workspace`|
5167
|`--namespace`, `-n`|Kubernetes namespace for Pod creation|`default`|
5268
|`--volume`|Kubernetes volume to mount into the Pod/container, accepts YAML format string, YAML file path|`None`|
5369
|`--pod-spec`|Custom Pod spec definition(YAML format string, YAML file path)|`None`|
70+
|`--command`|Custom executor container command|`None`|
71+
|`--args`|Custom executor container arguments|`None`|
5472

5573
## License
5674

python/packages/autogen-kubernetes-mcp/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "autogen-kubernetes-mcp"
7-
version = "0.5.2"
7+
version = "0.6.0"
88
description = "MPC server provides code interpreter with kubernetes workload"
99
readme = "README.md"
1010
requires-python = ">=3.10"
Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,73 @@
11
from typing import Any
22

33
from autogen_core import CancellationToken, ComponentModel
4-
from autogen_core.code_executor import CodeBlock
4+
from autogen_core.code_executor import CodeBlock, CodeExecutor
55
from autogen_kubernetes.code_executors import (
66
PodCommandLineCodeExecutor,
77
PodCommandLineCodeExecutorConfig,
8+
PodJupyterCodeExecutor,
9+
PodJupyterCodeExecutorConfig,
10+
PodJupyterServer,
11+
PodJupyterServerConfig,
812
)
913

1014

11-
def make_executor(args: dict[str, Any]) -> PodCommandLineCodeExecutor:
12-
pod_commandline_executor_config = PodCommandLineCodeExecutorConfig(**args)
13-
component_model = ComponentModel(
14-
provider="autogen_kubernetes.code_executors.PodCommandLineCodeExecutor",
15-
component_type=PodCommandLineCodeExecutor.component_type,
16-
version=PodCommandLineCodeExecutor.component_version,
17-
component_version=PodCommandLineCodeExecutor.component_version,
18-
description=PodCommandLineCodeExecutor.component_description,
19-
label=PodCommandLineCodeExecutor.__name__,
20-
config=pod_commandline_executor_config.model_dump(exclude_none=True),
21-
)
22-
return PodCommandLineCodeExecutor.load_component(component_model)
15+
async def make_executor(args: dict[str, Any]) -> list[Any]:
16+
closable_instances = []
17+
if args["type"] == "jupyter":
18+
jupyter_server_config = PodJupyterServerConfig(**args)
19+
jupyter_server = PodJupyterServer.load_component(
20+
ComponentModel(
21+
provider="autogen_kubernetes.code_executors.PodJupyterServer",
22+
component_type=PodJupyterServer.component_type,
23+
version=PodJupyterServer.component_version,
24+
component_version=PodJupyterServer.component_version,
25+
description=PodJupyterServer.component_description,
26+
label=PodJupyterServer.__name__,
27+
config=jupyter_server_config.model_dump(),
28+
)
29+
)
30+
await jupyter_server.start()
31+
jupyter_executor_config = PodJupyterCodeExecutorConfig(jupyter_server=jupyter_server, **args)
32+
jupyter_executor = PodJupyterCodeExecutor.load_component(
33+
ComponentModel(
34+
provider="autogen_kubernetes.code_executors.PodJupyterCodeExecutor",
35+
component_type=PodJupyterCodeExecutor.component_type,
36+
version=PodJupyterCodeExecutor.component_version,
37+
component_version=PodJupyterCodeExecutor.component_version,
38+
description=PodJupyterCodeExecutor.component_description,
39+
label=PodJupyterCodeExecutor.__name__,
40+
config=jupyter_executor_config.model_dump(),
41+
)
42+
)
43+
await jupyter_executor.start()
44+
closable_instances.extend([jupyter_executor, jupyter_server])
45+
else:
46+
pod_commandline_executor_config = PodCommandLineCodeExecutorConfig(**args)
47+
component_model = ComponentModel(
48+
provider="autogen_kubernetes.code_executors.PodCommandLineCodeExecutor",
49+
component_type=PodCommandLineCodeExecutor.component_type,
50+
version=PodCommandLineCodeExecutor.component_version,
51+
component_version=PodCommandLineCodeExecutor.component_version,
52+
description=PodCommandLineCodeExecutor.component_description,
53+
label=PodCommandLineCodeExecutor.__name__,
54+
config=pod_commandline_executor_config.model_dump(exclude_none=True),
55+
)
56+
cmd_executor = PodCommandLineCodeExecutor.load_component(component_model)
57+
await cmd_executor.start()
58+
closable_instances.append(cmd_executor)
59+
return closable_instances
2360

2461

25-
async def run_code(executor: PodCommandLineCodeExecutor, code: str) -> str:
62+
async def run_code(
63+
executor: CodeExecutor,
64+
code: str,
65+
cancellation_token: CancellationToken,
66+
) -> str:
2667
code_result = await executor.execute_code_blocks(
2768
code_blocks=[
2869
CodeBlock(language="python", code=code),
2970
],
30-
cancellation_token=CancellationToken(),
71+
cancellation_token=cancellation_token,
3172
)
3273
return code_result.output

python/packages/autogen-kubernetes-mcp/src/autogen_kubernetes_mcp/server.py

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,92 @@
11
import argparse
2-
from typing import Any
2+
import asyncio
3+
import uuid
4+
from contextlib import asynccontextmanager
5+
from typing import Any, AsyncIterator, TypedDict
36

4-
from mcp.server.fastmcp import FastMCP
7+
from autogen_core import CancellationToken
8+
from mcp.server.fastmcp import Context, FastMCP
59
from mcp.types import ToolAnnotations
610

711
from autogen_kubernetes_mcp._executor import make_executor, run_code
812

13+
STATEFUL_DESCRIPTION = r"""Use this tool to execute Python code in your chain of thought. The code will not be shown to the user. This tool should be used for internal reasoning, but not for code that is intended to be visible to the user (e.g. when creating plots, tables, or files).
14+
When you send a message containing Python code to python, it will be executed in a stateful Jupyter notebook environment. python will respond with the output of the execution or time out after 120.0 seconds. The drive at '/mnt/data' can be used to save and persist user files. Internet access for this session is UNKNOWN. Depends on the cluster"""
15+
STATELESS_DESCRIPTION = r"""Use this tool to execute Python code in your chain of thought. The code will not be shown to the user. This tool should be used for internal reasoning, but not for code that is intended to be visible to the user (e.g. when creating plots, tables, or files).
16+
When you send a message containing python code to python, it will be executed in a stateless docker container, and the stdout of that process will be returned to you."""
17+
18+
sessions: dict[str, list[Any]] = {}
19+
920

1021
def build_parser() -> argparse.ArgumentParser:
11-
parser = argparse.ArgumentParser(description="autogen-kubernetes arguments")
12-
parser.add_argument("--host", default="0.0.0.0")
13-
parser.add_argument("--port", type=int, default=8000)
22+
parser = argparse.ArgumentParser(description="autogen-kubernetes-mcp arguments")
23+
parser.add_argument(
24+
dest="type",
25+
choices=["commandline", "jupyter"],
26+
help="choose a code executor type (commandline, jupyter)",
27+
)
28+
parser.add_argument("--host", default="0.0.0.0", help="MCP server host")
29+
parser.add_argument("--port", type=int, default=8000, help="MCP server port")
1430
parser.add_argument("--kubeconfig", dest="kube_config_file", default=None)
15-
parser.add_argument("--image", default="python:3-slim")
31+
parser.add_argument("--image", default=None, help="image for code execution pod")
1632
parser.add_argument("--pod-name", default=None)
17-
parser.add_argument("--timeout", type=int, default=60)
33+
parser.add_argument("--timeout", type=int, default=argparse.SUPPRESS)
1834
parser.add_argument("--workspace-path", default="/workspace")
1935
parser.add_argument("-n", "--namespace", default="default")
2036
parser.add_argument("--volume", default=None)
2137
parser.add_argument("--pod-spec", default=None)
38+
parser.add_argument("--command", nargs="+", help="jupyter server pod commands", default=None)
39+
parser.add_argument("--args", nargs="+", help="jupyter server pod arguments", default=None)
2240

2341
return parser
2442

2543

44+
class State(TypedDict):
45+
sid: str
46+
47+
2648
def build_server(args: dict[str, Any]) -> FastMCP:
49+
tool_description = STATELESS_DESCRIPTION
50+
if args["type"] == "jupyter":
51+
tool_description = STATEFUL_DESCRIPTION
52+
if "timeout" not in args:
53+
args["timeout"] = 120
54+
55+
@asynccontextmanager
56+
async def session_lifespan(app: FastMCP) -> AsyncIterator[State]:
57+
sid = str(uuid.uuid4())
58+
executor = await make_executor(args)
59+
sessions[sid] = executor
60+
61+
yield {"sid": sid}
62+
63+
for instance in executor:
64+
await instance.stop()
65+
sessions.pop(sid, None)
66+
2767
mcp = FastMCP(
2868
name="python",
29-
instructions=r"""
30-
Use this tool to execute Python code in your chain of thought. The code will not be shown to the user. This tool should be used for internal reasoning, but not for code that is intended to be visible to the user (e.g. when creating plots, tables, or files).
31-
When you send a message containing python code to python, it will be executed in a stateless docker container, and the stdout of that process will be returned to you.
32-
""".strip(),
69+
instructions=tool_description.strip(),
3370
host=args["host"],
3471
port=args["port"],
72+
lifespan=session_lifespan,
3573
)
3674

3775
@mcp.tool(
3876
name="python",
3977
title="Execute Python code",
40-
description="""
41-
Use this tool to execute Python code in your chain of thought. The code will not be shown to the user. This tool should be used for internal reasoning, but not for code that is intended to be visible to the user (e.g. when creating plots, tables, or files).
42-
When you send a message containing python code to python, it will be executed in a stateless docker container, and the stdout of that process will be returned to you.
43-
""",
78+
description=tool_description,
4479
)
45-
async def python(code: str) -> str:
46-
async with make_executor(args) as executor:
47-
result: str = await run_code(executor, code)
80+
async def python(code: str, ctx: Context) -> str: # type: ignore
81+
sid = ctx.request_context.lifespan_context["sid"]
82+
executor = sessions[sid][0]
83+
cancellation_token = CancellationToken()
84+
try:
85+
result: str = await run_code(executor, code, cancellation_token)
4886
return result
87+
except asyncio.CancelledError:
88+
cancellation_token.cancel()
89+
return ""
4990

5091
return mcp
5192

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,42 @@
1+
import socket
2+
13
import pytest
2-
from autogen_kubernetes.code_executors import PodCommandLineCodeExecutor
4+
from autogen_core import CancellationToken
5+
from autogen_kubernetes.code_executors import (
6+
PodCommandLineCodeExecutor,
7+
PodJupyterCodeExecutor,
8+
)
39
from autogen_kubernetes_mcp._executor import make_executor, run_code
410
from conftest import kubernetes_enabled, state_kubernetes_enabled
511

612

13+
def can_resolve_svc_fqdn() -> bool:
14+
try:
15+
socket.gethostbyname("kubernetes.default")
16+
return True
17+
except socket.error:
18+
return False
19+
20+
721
@pytest.mark.skipif(not state_kubernetes_enabled, reason="kubernetes not accessible")
822
@pytest.mark.asyncio
9-
async def test_vanilla_executor() -> None:
10-
async with make_executor({}) as executor:
11-
assert isinstance(executor, PodCommandLineCodeExecutor)
12-
result = await run_code(executor, 'print("Hello")')
13-
assert "Hello" in result
23+
async def test_vanilla_commandline_executor() -> None:
24+
instances = await make_executor({"type": "commandline"})
25+
executor = instances[0]
26+
assert isinstance(executor, PodCommandLineCodeExecutor)
27+
result = await run_code(executor, 'print("Hello")', CancellationToken())
28+
assert "Hello" in result
29+
for instance in instances:
30+
await instance.stop()
31+
32+
33+
@pytest.mark.skipif(not state_kubernetes_enabled or not can_resolve_svc_fqdn(), reason="kubernetes not accessible")
34+
@pytest.mark.asyncio
35+
async def test_vanilla_jupyter_executor() -> None:
36+
instances = await make_executor({"type": "jupyter"})
37+
executor = instances[0]
38+
assert isinstance(executor, PodJupyterCodeExecutor)
39+
result = await run_code(executor, 'print("Hello")', CancellationToken())
40+
assert "Hello" in result
41+
for instance in instances:
42+
await instance.stop()

python/packages/autogen-kubernetes/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,63 @@ async with PodCommandLineCodeExecutor(functions=[test_function]) as executor:
420420
CommandLineCodeResult(exit_code=0, output='kube_config_path not provided and default location (~/.kube/config) does not exist. Using inCluster Config. This might not work.\nTrue\n', code_file='/workspace/tmp_code_c61a3c1e421357bd54041ad195e242d8205b86a3a4a0778b8e2684bc373aac22.py')
421421
```
422422

423+
### PodJupyterServer
424+
425+
- Creates a Jupyter Server Pod using `jupyter-kernel-gateway`, along with the required token secret and service resources.
426+
- Similar to `PodCommandLineCodeExecutor`, the `service_spec`, and `secret_spec` can be customized in multiple formats: Python dictionary, YAML/json string, YAML/json file path, or Kubernetes client model.
427+
- When used with default arguments, it runs on the `quay.io/jupyter/docker-stacks-foundation` image with `jupyter-kernel-gateway` and `ipykernal` installed, and executes via `jupyter-kernel-gateway`. For efficiency in production, building a custom image is recommended. Below is the sample Dockerfile
428+
429+
```Dockerfile
430+
FROM quay.io/jupyter/docker-stacks-foundation
431+
432+
RUN mamba install --yes jupyter_kernel_gateway ipykernel && \
433+
mamba clean --all -f -y && \
434+
fix-permissions "${CONDA_DIR}" && \
435+
fix-permissions "/home/${NB_USER}"
436+
CMD python -m jupyter kernelgateway --KernelGatewayApp.ip=0.0.0.0 \
437+
--JupyterApp.answer_yes=true \
438+
--JupyterWebsocketPersonality.list_kernels=true
439+
```
440+
441+
### PodJupyterCodeExecutor
442+
443+
- A `CodeExecutor` that leverages PodJupyterServer and the jupyter-kernel-gateway server to executor code statefully and retrieve results
444+
- When used together with PodJupyterServer,note that the server generates PodJupyterConnectionInfo based on the service FQDN. This means it cannot be directly used in a non-incluster environment.
445+
- After creating the PodJupyterServer, you must construct and provide PodJupyterConnectionInfo so that PodJupyterCodeExecutor can access it properly.
446+
447+
Using with PodJupyterServer
448+
```python
449+
async with PodJupyeterServer() as jupyter_server:
450+
async with PodJupyterCodeExecutor(jupyter_server) as executor:
451+
code_result = await executor.execute_code_blocks(
452+
code_blocks=[
453+
CodeBlock(language="python", code="print('Hello, World!')"),
454+
],
455+
cancellation_token=CancellationToken(),
456+
)
457+
print(code_result)
458+
```
459+
460+
Using custom PodJupyterConnectionInfo
461+
```python
462+
async with PodJupyeterServer() as jupyter_server:
463+
# connection info for created jupyter server pod
464+
connection_info = PodJupyetrConnectionInfo(
465+
host="https://jupyter-server/access/path",
466+
port="443",
467+
token=SecretStr("token-string")
468+
)
469+
async with PodJupyterCodeExecutor(connection_info) as executor:
470+
code_result = await executor.execute_code_blocks(
471+
code_blocks=[
472+
CodeBlock(language="python", code="print('Hello, World!')"),
473+
],
474+
cancellation_token=CancellationToken(),
475+
)
476+
print(code_result)
477+
```
478+
479+
- Even without using PodJupyterServer, you can configure a custom Jupyter server that meets the required conditions and provide PodJupyterConnectionInfo for PodJupyterCodeExecutor to work with
423480

424481
## Contribute
425482

python/packages/autogen-kubernetes/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "autogen-kubernetes"
7-
version = "0.5.2"
7+
version = "0.6.0"
88
license = {file = "LICENSE-CODE"}
99
description = "autogen kubernetes extension"
1010
keywords = ["autogen", "agent", "AI", "kubernetes"]

0 commit comments

Comments
 (0)