agentscope-runtime 1.0.4a1__py3-none-any.whl → 1.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. agentscope_runtime/adapters/agentscope/stream.py +2 -8
  2. agentscope_runtime/adapters/langgraph/stream.py +120 -70
  3. agentscope_runtime/adapters/ms_agent_framework/__init__.py +0 -0
  4. agentscope_runtime/adapters/ms_agent_framework/message.py +205 -0
  5. agentscope_runtime/adapters/ms_agent_framework/stream.py +418 -0
  6. agentscope_runtime/adapters/utils.py +6 -0
  7. agentscope_runtime/cli/commands/deploy.py +836 -1
  8. agentscope_runtime/cli/commands/stop.py +16 -0
  9. agentscope_runtime/common/container_clients/__init__.py +52 -0
  10. agentscope_runtime/common/container_clients/agentrun_client.py +6 -4
  11. agentscope_runtime/common/container_clients/boxlite_client.py +442 -0
  12. agentscope_runtime/common/container_clients/docker_client.py +0 -20
  13. agentscope_runtime/common/container_clients/fc_client.py +6 -4
  14. agentscope_runtime/common/container_clients/gvisor_client.py +38 -0
  15. agentscope_runtime/common/container_clients/knative_client.py +467 -0
  16. agentscope_runtime/common/utils/deprecation.py +164 -0
  17. agentscope_runtime/engine/__init__.py +4 -0
  18. agentscope_runtime/engine/app/agent_app.py +16 -4
  19. agentscope_runtime/engine/constant.py +1 -0
  20. agentscope_runtime/engine/deployers/__init__.py +34 -11
  21. agentscope_runtime/engine/deployers/adapter/__init__.py +8 -0
  22. agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +26 -51
  23. agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +23 -13
  24. agentscope_runtime/engine/deployers/adapter/a2a/a2a_registry.py +4 -201
  25. agentscope_runtime/engine/deployers/adapter/a2a/nacos_a2a_registry.py +152 -25
  26. agentscope_runtime/engine/deployers/adapter/agui/__init__.py +8 -0
  27. agentscope_runtime/engine/deployers/adapter/agui/agui_adapter_utils.py +652 -0
  28. agentscope_runtime/engine/deployers/adapter/agui/agui_protocol_adapter.py +225 -0
  29. agentscope_runtime/engine/deployers/agentrun_deployer.py +2 -2
  30. agentscope_runtime/engine/deployers/fc_deployer.py +1506 -0
  31. agentscope_runtime/engine/deployers/knative_deployer.py +290 -0
  32. agentscope_runtime/engine/deployers/pai_deployer.py +2335 -0
  33. agentscope_runtime/engine/deployers/utils/net_utils.py +37 -0
  34. agentscope_runtime/engine/deployers/utils/oss_utils.py +38 -0
  35. agentscope_runtime/engine/deployers/utils/package.py +46 -42
  36. agentscope_runtime/engine/helpers/agent_api_client.py +372 -0
  37. agentscope_runtime/engine/runner.py +13 -0
  38. agentscope_runtime/engine/schemas/agent_schemas.py +9 -3
  39. agentscope_runtime/engine/services/agent_state/__init__.py +7 -0
  40. agentscope_runtime/engine/services/memory/__init__.py +7 -0
  41. agentscope_runtime/engine/services/memory/redis_memory_service.py +15 -16
  42. agentscope_runtime/engine/services/session_history/__init__.py +7 -0
  43. agentscope_runtime/engine/tracing/local_logging_handler.py +2 -3
  44. agentscope_runtime/engine/tracing/wrapper.py +18 -4
  45. agentscope_runtime/sandbox/__init__.py +14 -6
  46. agentscope_runtime/sandbox/box/base/__init__.py +2 -2
  47. agentscope_runtime/sandbox/box/base/base_sandbox.py +51 -1
  48. agentscope_runtime/sandbox/box/browser/__init__.py +2 -2
  49. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +198 -2
  50. agentscope_runtime/sandbox/box/filesystem/__init__.py +2 -2
  51. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +99 -2
  52. agentscope_runtime/sandbox/box/gui/__init__.py +2 -2
  53. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +117 -1
  54. agentscope_runtime/sandbox/box/mobile/__init__.py +2 -2
  55. agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +247 -100
  56. agentscope_runtime/sandbox/box/sandbox.py +102 -65
  57. agentscope_runtime/sandbox/box/shared/routers/generic.py +36 -29
  58. agentscope_runtime/sandbox/client/__init__.py +6 -1
  59. agentscope_runtime/sandbox/client/async_http_client.py +339 -0
  60. agentscope_runtime/sandbox/client/base.py +74 -0
  61. agentscope_runtime/sandbox/client/http_client.py +108 -329
  62. agentscope_runtime/sandbox/enums.py +7 -0
  63. agentscope_runtime/sandbox/manager/sandbox_manager.py +275 -29
  64. agentscope_runtime/sandbox/manager/server/app.py +7 -1
  65. agentscope_runtime/sandbox/manager/server/config.py +3 -1
  66. agentscope_runtime/sandbox/model/manager_config.py +11 -9
  67. agentscope_runtime/tools/modelstudio_memory/__init__.py +106 -0
  68. agentscope_runtime/tools/modelstudio_memory/base.py +220 -0
  69. agentscope_runtime/tools/modelstudio_memory/config.py +86 -0
  70. agentscope_runtime/tools/modelstudio_memory/core.py +594 -0
  71. agentscope_runtime/tools/modelstudio_memory/exceptions.py +60 -0
  72. agentscope_runtime/tools/modelstudio_memory/schemas.py +253 -0
  73. agentscope_runtime/version.py +1 -1
  74. {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.dist-info}/METADATA +186 -73
  75. {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.dist-info}/RECORD +79 -55
  76. {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.dist-info}/WHEEL +0 -0
  77. {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.dist-info}/entry_points.txt +0 -0
  78. {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.dist-info}/licenses/LICENSE +0 -0
  79. {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.dist-info}/top_level.txt +0 -0
@@ -6,77 +6,56 @@ from typing import Any, Optional
6
6
 
7
7
  from ..enums import SandboxType
8
8
  from ..manager.sandbox_manager import SandboxManager
9
+ from ..manager.server.app import get_config
9
10
 
10
11
 
11
12
  logging.basicConfig(level=logging.INFO)
12
13
  logger = logging.getLogger(__name__)
13
14
 
14
15
 
15
- class Sandbox:
16
+ class SandboxBase:
16
17
  """
17
- Sandbox Interface.
18
+ Common base class for both sync and async Sandbox interfaces.
18
19
  """
19
20
 
20
21
  def __init__(
21
22
  self,
22
23
  sandbox_id: Optional[str] = None,
23
- timeout: int = 3000, # TODO: enable life circle management
24
+ timeout: int = 3000,
24
25
  base_url: Optional[str] = None,
25
- bearer_token: Optional[str] = None, # TODO: support api_key
26
+ bearer_token: Optional[str] = None,
26
27
  sandbox_type: SandboxType = SandboxType.BASE,
27
28
  ) -> None:
28
- """
29
- Initialize the sandbox interface.
30
- """
31
29
  self.base_url = base_url
30
+ self.embed_mode = not bool(base_url)
31
+ self.sandbox_type = sandbox_type
32
+ self.timeout = timeout
33
+ self._sandbox_id = sandbox_id
34
+
32
35
  if base_url:
33
- self.embed_mode = False
36
+ # Remote Manager
34
37
  self.manager_api = SandboxManager(
35
38
  base_url=base_url,
36
39
  bearer_token=bearer_token,
37
- ).__enter__()
40
+ )
38
41
  else:
39
- # Launch a local manager
40
- self.embed_mode = True
42
+ # Embedded Manager
41
43
  self.manager_api = SandboxManager(
44
+ config=get_config(),
42
45
  default_type=sandbox_type,
43
46
  )
44
47
 
45
- if sandbox_id is None:
46
- logger.debug(
47
- "You are using embed mode, it might take several seconds to "
48
- "init the runtime, please wait.",
49
- )
50
-
51
- sandbox_id = self.manager_api.create_from_pool(
52
- sandbox_type=SandboxType(sandbox_type).value,
53
- )
54
- if sandbox_id is None:
55
- raise RuntimeError(
56
- "No sandbox available. "
57
- "Please check if sandbox images exist, build or pull "
58
- "missing images in sandbox server.",
59
- )
60
- self._sandbox_id = sandbox_id
61
-
62
- self._sandbox_id = sandbox_id
63
- self.sandbox_type = sandbox_type
64
- self.timeout = timeout
65
-
66
- # Clean up function enabled in embed mode
67
- if self.embed_mode:
68
- atexit.register(self._cleanup)
69
- self._register_signal_handlers()
48
+ @property
49
+ def sandbox_id(self) -> Optional[str]:
50
+ return self._sandbox_id
70
51
 
71
- def _register_signal_handlers(self) -> None:
72
- """
73
- Register signal handlers for graceful shutdown and cleanup.
74
- Handles SIGINT (Ctrl+C) and, if available, SIGTERM to ensure that
75
- the sandbox is properly cleaned up when the process receives these
76
- signals. On platforms where SIGTERM is not available (e.g.,
77
- Windows), only SIGINT is handled.
78
- """
52
+ @sandbox_id.setter
53
+ def sandbox_id(self, value: str) -> None:
54
+ if not value:
55
+ raise ValueError("Sandbox ID cannot be empty.")
56
+ self._sandbox_id = value
79
57
 
58
+ def _register_signal_handlers(self):
80
59
  def _handler(signum, frame): # pylint: disable=unused-argument
81
60
  logger.debug(
82
61
  f"Received signal {signum}, stopping Sandbox"
@@ -85,7 +64,6 @@ class Sandbox:
85
64
  self._cleanup()
86
65
  raise SystemExit(0)
87
66
 
88
- # Windows does not support SIGTERM
89
67
  if hasattr(signal, "SIGTERM"):
90
68
  signals = [signal.SIGINT, signal.SIGTERM]
91
69
  else:
@@ -107,39 +85,36 @@ class Sandbox:
107
85
  specific sandbox instance.
108
86
  """
109
87
  try:
110
- # Remote not need to close the embed_manager
111
88
  if self.embed_mode:
112
- # Clean all
113
89
  self.manager_api.__exit__(None, None, None)
114
90
  else:
115
- # Clean the specific sandbox
116
91
  self.manager_api.release(self.sandbox_id)
117
92
  except Exception as e:
118
93
  import traceback
119
94
 
120
95
  logger.error(
121
- f"Cleanup {self.sandbox_id} error: {e}\n"
122
- f"{traceback.format_exc()}",
96
+ f"Cleanup {self.sandbox_id} error: {e}"
97
+ f"\n{traceback.format_exc()}",
123
98
  )
124
99
 
100
+
101
+ class Sandbox(SandboxBase):
125
102
  def __enter__(self):
103
+ # Create sandbox if sandbox_id not provided
104
+ if self.sandbox_id is None:
105
+ self.sandbox_id = self.manager_api.create_from_pool(
106
+ sandbox_type=SandboxType(self.sandbox_type).value,
107
+ )
108
+ if self.sandbox_id is None:
109
+ raise RuntimeError("No sandbox available.")
110
+ if self.embed_mode:
111
+ atexit.register(self._cleanup)
112
+ self._register_signal_handlers()
126
113
  return self
127
114
 
128
115
  def __exit__(self, exc_type, exc_value, traceback):
129
116
  self._cleanup()
130
117
 
131
- @property
132
- def sandbox_id(self) -> str:
133
- """Get the sandbox ID."""
134
- return self._sandbox_id
135
-
136
- @sandbox_id.setter
137
- def sandbox_id(self, value: str) -> None:
138
- """Set the sandbox ID."""
139
- if not value:
140
- raise ValueError("Sandbox ID cannot be empty.")
141
- self._sandbox_id = value
142
-
143
118
  def get_info(self) -> dict:
144
119
  return self.manager_api.get_info(self.sandbox_id)
145
120
 
@@ -156,15 +131,77 @@ class Sandbox:
156
131
  ) -> Any:
157
132
  if arguments is None:
158
133
  arguments = {}
159
-
160
134
  return self.manager_api.call_tool(self.sandbox_id, name, arguments)
161
135
 
162
- def add_mcp_servers(
136
+ def add_mcp_servers(self, server_configs: dict, overwrite=False):
137
+ return self.manager_api.add_mcp_servers(
138
+ self.sandbox_id,
139
+ server_configs,
140
+ overwrite,
141
+ )
142
+
143
+
144
+ class SandboxAsync(SandboxBase):
145
+ async def __aenter__(self):
146
+ if self.sandbox_id is None:
147
+ self.sandbox_id = await self.manager_api.create_from_pool_async(
148
+ sandbox_type=SandboxType(self.sandbox_type).value,
149
+ )
150
+ if self.sandbox_id is None:
151
+ raise RuntimeError("No sandbox available.")
152
+ if self.embed_mode:
153
+ atexit.register(self._cleanup)
154
+ self._register_signal_handlers()
155
+ return self
156
+
157
+ async def __aexit__(self, exc_type, exc_value, traceback):
158
+ await self._cleanup_async()
159
+
160
+ async def _cleanup_async(self):
161
+ try:
162
+ if self.embed_mode:
163
+ await self.manager_api.__aexit__(None, None, None)
164
+ else:
165
+ await self.manager_api.release_async(self.sandbox_id)
166
+ except Exception as e:
167
+ import traceback
168
+
169
+ logger.error(
170
+ f"Async Cleanup {self.sandbox_id} error: {e}"
171
+ f"\n{traceback.format_exc()}",
172
+ )
173
+
174
+ async def get_info_async(self) -> dict:
175
+ return await self.manager_api.get_info_async(self.sandbox_id)
176
+
177
+ async def list_tools_async(
178
+ self,
179
+ tool_type: Optional[str] = None,
180
+ ) -> dict:
181
+ return await self.manager_api.list_tools_async(
182
+ self.sandbox_id,
183
+ tool_type=tool_type,
184
+ )
185
+
186
+ async def call_tool_async(
187
+ self,
188
+ name: str,
189
+ arguments: Optional[dict[str, Any]] = None,
190
+ ) -> Any:
191
+ if arguments is None:
192
+ arguments = {}
193
+ return await self.manager_api.call_tool_async(
194
+ self.sandbox_id,
195
+ name,
196
+ arguments,
197
+ )
198
+
199
+ async def add_mcp_servers_async(
163
200
  self,
164
201
  server_configs: dict,
165
202
  overwrite=False,
166
203
  ):
167
- return self.manager_api.add_mcp_servers(
204
+ return await self.manager_api.add_mcp_servers_async(
168
205
  self.sandbox_id,
169
206
  server_configs,
170
207
  overwrite,
@@ -2,7 +2,7 @@
2
2
  import io
3
3
  import sys
4
4
  import logging
5
- import subprocess
5
+ import asyncio
6
6
  import traceback
7
7
  from contextlib import redirect_stderr, redirect_stdout
8
8
 
@@ -44,26 +44,33 @@ async def run_ipython_cell(
44
44
  stdout_buf = io.StringIO()
45
45
  stderr_buf = io.StringIO()
46
46
 
47
- with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
48
- preprocessing_exc_tuple = None
49
- try:
50
- transformed_cell = ipy.transform_cell(code)
51
- except Exception:
52
- transformed_cell = code
53
- preprocessing_exc_tuple = sys.exc_info()
54
-
55
- if transformed_cell is None:
56
- raise HTTPException(
57
- status_code=500,
58
- detail="IPython cell transformation failed: "
59
- "transformed_cell is None.",
47
+ def thread_target():
48
+ with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
49
+ preprocessing_exc_tuple = None
50
+ try:
51
+ transformed_cell = ipy.transform_cell(code)
52
+ except Exception:
53
+ transformed_cell = code
54
+ preprocessing_exc_tuple = sys.exc_info()
55
+
56
+ if transformed_cell is None:
57
+ raise HTTPException(
58
+ status_code=500,
59
+ detail=(
60
+ "IPython cell transformation failed: "
61
+ "transformed_cell is None."
62
+ ),
63
+ )
64
+
65
+ asyncio.run(
66
+ ipy.run_cell_async(
67
+ code,
68
+ transformed_cell=transformed_cell,
69
+ preprocessing_exc_tuple=preprocessing_exc_tuple,
70
+ ),
60
71
  )
61
72
 
62
- await ipy.run_cell_async(
63
- code,
64
- transformed_cell=transformed_cell,
65
- preprocessing_exc_tuple=preprocessing_exc_tuple,
66
- )
73
+ await asyncio.to_thread(thread_target)
67
74
 
68
75
  stdout_content = stdout_buf.getvalue()
69
76
  stderr_content = stderr_buf.getvalue()
@@ -128,16 +135,16 @@ async def run_shell_command(
128
135
  if not command:
129
136
  raise HTTPException(status_code=400, detail="Command is required.")
130
137
 
131
- result = subprocess.run(
138
+ proc = await asyncio.create_subprocess_shell(
132
139
  command,
133
- shell=True,
134
- stdout=subprocess.PIPE,
135
- stderr=subprocess.PIPE,
136
- text=True,
137
- check=False,
140
+ stdout=asyncio.subprocess.PIPE,
141
+ stderr=asyncio.subprocess.PIPE,
138
142
  )
139
- stdout_content = result.stdout
140
- stderr_content = result.stderr
143
+
144
+ stdout_bytes, stderr_bytes = await proc.communicate()
145
+
146
+ stdout_content = stdout_bytes.decode()
147
+ stderr_content = stderr_bytes.decode()
141
148
 
142
149
  content_list = []
143
150
 
@@ -161,7 +168,7 @@ async def run_shell_command(
161
168
  content_list.append(
162
169
  TextContent(
163
170
  type="text",
164
- text=str(result.returncode),
171
+ text=str(proc.returncode),
165
172
  description="returncode",
166
173
  ),
167
174
  )
@@ -173,7 +180,7 @@ async def run_shell_command(
173
180
  + "\n"
174
181
  + stderr_content
175
182
  + "\n"
176
- + str(result.returncode),
183
+ + str(proc.returncode),
177
184
  description="output",
178
185
  ),
179
186
  )
@@ -1,5 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from .http_client import SandboxHttpClient
3
3
  from .training_client import TrainingSandboxClient
4
+ from .async_http_client import SandboxHttpAsyncClient
4
5
 
5
- __all__ = ["SandboxHttpClient", "TrainingSandboxClient"]
6
+ __all__ = [
7
+ "SandboxHttpClient",
8
+ "SandboxHttpAsyncClient",
9
+ "TrainingSandboxClient",
10
+ ]
@@ -0,0 +1,339 @@
1
+ # -*- coding: utf-8 -*-
2
+ # pylint: disable=unused-argument
3
+ import logging
4
+ import asyncio
5
+ from typing import Any, Optional
6
+
7
+ import httpx
8
+ from pydantic import Field
9
+
10
+ from .base import SandboxHttpBase
11
+ from ..model import ContainerModel
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class SandboxHttpAsyncClient(SandboxHttpBase):
17
+ """
18
+ A Python async client for interacting with the runtime API.
19
+ Connect directly to the container.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ model: Optional[ContainerModel] = None,
25
+ timeout: int = 60,
26
+ domain: str = "localhost",
27
+ ) -> None:
28
+ """
29
+ Initialize the Python async client.
30
+
31
+ Args:
32
+ model (ContainerModel): The pydantic model representing the
33
+ runtime sandbox.
34
+ """
35
+ super().__init__(model, timeout, domain)
36
+ self.client = httpx.AsyncClient(
37
+ timeout=self.timeout,
38
+ headers=self.headers,
39
+ )
40
+
41
+ async def __aenter__(self):
42
+ # Wait for the runtime api server to be healthy
43
+ await self.wait_until_healthy()
44
+ return self
45
+
46
+ async def __aexit__(self, exc_type, exc_value, traceback):
47
+ await self.client.aclose()
48
+
49
+ async def _request(self, method: str, url: str, **kwargs):
50
+ return await self.client.request(method, url, **kwargs)
51
+
52
+ async def safe_request(self, method: str, url: str, **kwargs):
53
+ """
54
+ Unified HTTP request method with async exception handling.
55
+ Returns JSON if possible, otherwise plain text.
56
+ """
57
+ try:
58
+ r = await self._request(method, url, **kwargs)
59
+ r.raise_for_status()
60
+ try:
61
+ return r.json()
62
+ except ValueError:
63
+ return r.text
64
+ except httpx.RequestError as e:
65
+ logger.error(f"HTTP error: {e}")
66
+ return {
67
+ "isError": True,
68
+ "content": [{"type": "text", "text": str(e)}],
69
+ }
70
+
71
+ async def check_health(self) -> bool:
72
+ """
73
+ Check if the runtime service is running by verifying the health
74
+ endpoint.
75
+
76
+ Returns:
77
+ bool: True if the service is reachable, False otherwise.
78
+ """
79
+ try:
80
+ r = await self._request(
81
+ "get",
82
+ f"{self.base_url}/healthz",
83
+ )
84
+ return r.status_code == 200
85
+ except httpx.RequestError:
86
+ return False
87
+
88
+ async def wait_until_healthy(self) -> None:
89
+ """
90
+ Wait until the runtime service is running for a specified timeout.
91
+ """
92
+ start_time = asyncio.get_event_loop().time()
93
+ while (
94
+ asyncio.get_event_loop().time() - start_time < self.start_timeout
95
+ ):
96
+ if await self.check_health():
97
+ return
98
+ await asyncio.sleep(1)
99
+ raise TimeoutError(
100
+ "Runtime service did not start within the specified timeout.",
101
+ )
102
+
103
+ async def add_mcp_servers(self, server_configs, overwrite=False):
104
+ """
105
+ Add MCP servers to runtime.
106
+ """
107
+ endpoint = f"{self.base_url}/mcp/add_servers"
108
+ return await self.safe_request(
109
+ "post",
110
+ endpoint,
111
+ json={"server_configs": server_configs, "overwrite": overwrite},
112
+ )
113
+
114
+ async def list_tools(self, tool_type=None, **kwargs) -> dict:
115
+ """
116
+ List available MCP tools plus generic built-in tools.
117
+ """
118
+ data = await self.safe_request(
119
+ "get",
120
+ f"{self.base_url}/mcp/list_tools",
121
+ )
122
+ if isinstance(data, dict) and "isError" not in data:
123
+ data["generic"] = self.generic_tools
124
+ if tool_type:
125
+ return {tool_type: data.get(tool_type, {})}
126
+ return data
127
+
128
+ async def call_tool(
129
+ self,
130
+ name: str,
131
+ arguments: Optional[dict[str, Any]] = None,
132
+ ) -> dict:
133
+ """
134
+ Call a specific MCP tool.
135
+
136
+ If it's a generic tool, call the corresponding local method.
137
+ """
138
+ if arguments is None:
139
+ arguments = {}
140
+ if name in self.generic_tools:
141
+ if name == "run_ipython_cell":
142
+ return await self.run_ipython_cell(**arguments)
143
+ elif name == "run_shell_command":
144
+ return await self.run_shell_command(**arguments)
145
+
146
+ return await self.safe_request(
147
+ "post",
148
+ f"{self.base_url}/mcp/call_tool",
149
+ json={"tool_name": name, "arguments": arguments},
150
+ )
151
+
152
+ async def run_ipython_cell(
153
+ self,
154
+ code: str = Field(description="IPython code to execute"),
155
+ ) -> dict:
156
+ """
157
+ Run an IPython cell.
158
+ """
159
+ return await self.safe_request(
160
+ "post",
161
+ f"{self.base_url}/tools/run_ipython_cell",
162
+ json={"code": code},
163
+ )
164
+
165
+ async def run_shell_command(
166
+ self,
167
+ command: str = Field(description="Shell command to execute"),
168
+ ) -> dict:
169
+ """
170
+ Run a shell command.
171
+ """
172
+ return await self.safe_request(
173
+ "post",
174
+ f"{self.base_url}/tools/run_shell_command",
175
+ json={"command": command},
176
+ )
177
+
178
+ async def commit_changes(
179
+ self,
180
+ commit_message: str = "Automated commit",
181
+ ) -> dict:
182
+ """
183
+ Commit the uncommitted changes with a given commit message.
184
+ """
185
+ return await self.safe_request(
186
+ "post",
187
+ f"{self.base_url}/watcher/commit_changes",
188
+ json={"commit_message": commit_message},
189
+ )
190
+
191
+ async def generate_diff(
192
+ self,
193
+ commit_a: Optional[str] = None,
194
+ commit_b: Optional[str] = None,
195
+ ) -> dict:
196
+ """
197
+ Generate the diff between two commits or between uncommitted changes
198
+ and the latest commit.
199
+ """
200
+ return await self.safe_request(
201
+ "post",
202
+ f"{self.base_url}/watcher/generate_diff",
203
+ json={"commit_a": commit_a, "commit_b": commit_b},
204
+ )
205
+
206
+ async def git_logs(self) -> dict:
207
+ """
208
+ Retrieve the git logs.
209
+ """
210
+ return await self.safe_request(
211
+ "get",
212
+ f"{self.base_url}/watcher/git_logs",
213
+ )
214
+
215
+ # -------- Workspace File APIs --------
216
+
217
+ async def get_workspace_file(self, file_path: str) -> dict:
218
+ """
219
+ Retrieve a file from the /workspace directory.
220
+ """
221
+ try:
222
+ endpoint = f"{self.base_url}/workspace/files"
223
+ params = {"file_path": file_path}
224
+ r = await self._request("get", endpoint, params=params)
225
+ r.raise_for_status()
226
+
227
+ # Check for empty content
228
+ if r.headers.get("Content-Length") == "0":
229
+ logger.warning(f"The file {file_path} is empty.")
230
+ return {"data": b""}
231
+
232
+ # Accumulate the content in chunks
233
+ file_content = bytearray()
234
+ async for chunk in r.aiter_bytes():
235
+ file_content.extend(chunk)
236
+
237
+ return {"data": bytes(file_content)}
238
+ except httpx.RequestError as e:
239
+ logger.error(f"An error occurred while retrieving the file: {e}")
240
+ return {
241
+ "isError": True,
242
+ "content": [{"type": "text", "text": str(e)}],
243
+ }
244
+
245
+ async def create_or_edit_workspace_file(
246
+ self,
247
+ file_path: str,
248
+ content: str,
249
+ ) -> dict:
250
+ """
251
+ Create or edit a file within the /workspace directory.
252
+ """
253
+ return await self.safe_request(
254
+ "post",
255
+ f"{self.base_url}/workspace/files",
256
+ params={"file_path": file_path},
257
+ json={"content": content},
258
+ )
259
+
260
+ async def list_workspace_directories(
261
+ self,
262
+ directory: str = "/workspace",
263
+ ) -> dict:
264
+ """
265
+ List files in the specified directory within the /workspace.
266
+ """
267
+ return await self.safe_request(
268
+ "get",
269
+ f"{self.base_url}/workspace/list-directories",
270
+ params={"directory": directory},
271
+ )
272
+
273
+ async def create_workspace_directory(self, directory_path: str) -> dict:
274
+ """
275
+ Create a directory within the /workspace directory.
276
+ """
277
+ return await self.safe_request(
278
+ "post",
279
+ f"{self.base_url}/workspace/directories",
280
+ params={"directory_path": directory_path},
281
+ )
282
+
283
+ async def delete_workspace_file(self, file_path: str) -> dict:
284
+ """
285
+ Delete a file within the /workspace directory.
286
+ """
287
+ return await self.safe_request(
288
+ "delete",
289
+ f"{self.base_url}/workspace/files",
290
+ params={"file_path": file_path},
291
+ )
292
+
293
+ async def delete_workspace_directory(
294
+ self,
295
+ directory_path: str,
296
+ recursive: bool = False,
297
+ ) -> dict:
298
+ """
299
+ Delete a directory within the /workspace directory.
300
+ """
301
+ return await self.safe_request(
302
+ "delete",
303
+ f"{self.base_url}/workspace/directories",
304
+ params={"directory_path": directory_path, "recursive": recursive},
305
+ )
306
+
307
+ async def move_or_rename_workspace_item(
308
+ self,
309
+ source_path: str,
310
+ destination_path: str,
311
+ ) -> dict:
312
+ """
313
+ Move or rename a file or directory within the /workspace directory.
314
+ """
315
+ return await self.safe_request(
316
+ "put",
317
+ f"{self.base_url}/workspace/move",
318
+ params={
319
+ "source_path": source_path,
320
+ "destination_path": destination_path,
321
+ },
322
+ )
323
+
324
+ async def copy_workspace_item(
325
+ self,
326
+ source_path: str,
327
+ destination_path: str,
328
+ ) -> dict:
329
+ """
330
+ Copy a file or directory within the /workspace directory.
331
+ """
332
+ return await self.safe_request(
333
+ "post",
334
+ f"{self.base_url}/workspace/copy",
335
+ params={
336
+ "source_path": source_path,
337
+ "destination_path": destination_path,
338
+ },
339
+ )