agentscope-runtime 0.1.0__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 (131) hide show
  1. agentscope_runtime/__init__.py +4 -0
  2. agentscope_runtime/engine/__init__.py +9 -0
  3. agentscope_runtime/engine/agents/__init__.py +2 -0
  4. agentscope_runtime/engine/agents/agentscope_agent/__init__.py +6 -0
  5. agentscope_runtime/engine/agents/agentscope_agent/agent.py +342 -0
  6. agentscope_runtime/engine/agents/agentscope_agent/hooks.py +156 -0
  7. agentscope_runtime/engine/agents/agno_agent.py +220 -0
  8. agentscope_runtime/engine/agents/base_agent.py +29 -0
  9. agentscope_runtime/engine/agents/langgraph_agent.py +59 -0
  10. agentscope_runtime/engine/agents/llm_agent.py +51 -0
  11. agentscope_runtime/engine/deployers/__init__.py +3 -0
  12. agentscope_runtime/engine/deployers/adapter/__init__.py +0 -0
  13. agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +2 -0
  14. agentscope_runtime/engine/deployers/adapter/a2a/a2a_adapter_utils.py +425 -0
  15. agentscope_runtime/engine/deployers/adapter/a2a/a2a_agent_adapter.py +69 -0
  16. agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +60 -0
  17. agentscope_runtime/engine/deployers/adapter/protocol_adapter.py +24 -0
  18. agentscope_runtime/engine/deployers/base.py +17 -0
  19. agentscope_runtime/engine/deployers/local_deployer.py +586 -0
  20. agentscope_runtime/engine/helpers/helper.py +127 -0
  21. agentscope_runtime/engine/llms/__init__.py +3 -0
  22. agentscope_runtime/engine/llms/base_llm.py +60 -0
  23. agentscope_runtime/engine/llms/qwen_llm.py +47 -0
  24. agentscope_runtime/engine/misc/__init__.py +0 -0
  25. agentscope_runtime/engine/runner.py +186 -0
  26. agentscope_runtime/engine/schemas/__init__.py +0 -0
  27. agentscope_runtime/engine/schemas/agent_schemas.py +551 -0
  28. agentscope_runtime/engine/schemas/context.py +54 -0
  29. agentscope_runtime/engine/services/__init__.py +9 -0
  30. agentscope_runtime/engine/services/base.py +77 -0
  31. agentscope_runtime/engine/services/context_manager.py +129 -0
  32. agentscope_runtime/engine/services/environment_manager.py +50 -0
  33. agentscope_runtime/engine/services/manager.py +174 -0
  34. agentscope_runtime/engine/services/memory_service.py +270 -0
  35. agentscope_runtime/engine/services/sandbox_service.py +198 -0
  36. agentscope_runtime/engine/services/session_history_service.py +256 -0
  37. agentscope_runtime/engine/tracing/__init__.py +40 -0
  38. agentscope_runtime/engine/tracing/base.py +309 -0
  39. agentscope_runtime/engine/tracing/local_logging_handler.py +356 -0
  40. agentscope_runtime/engine/tracing/tracing_metric.py +69 -0
  41. agentscope_runtime/engine/tracing/wrapper.py +321 -0
  42. agentscope_runtime/sandbox/__init__.py +14 -0
  43. agentscope_runtime/sandbox/box/__init__.py +0 -0
  44. agentscope_runtime/sandbox/box/base/__init__.py +0 -0
  45. agentscope_runtime/sandbox/box/base/base_sandbox.py +37 -0
  46. agentscope_runtime/sandbox/box/base/box/__init__.py +0 -0
  47. agentscope_runtime/sandbox/box/browser/__init__.py +0 -0
  48. agentscope_runtime/sandbox/box/browser/box/__init__.py +0 -0
  49. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +176 -0
  50. agentscope_runtime/sandbox/box/dummy/__init__.py +0 -0
  51. agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +26 -0
  52. agentscope_runtime/sandbox/box/filesystem/__init__.py +0 -0
  53. agentscope_runtime/sandbox/box/filesystem/box/__init__.py +0 -0
  54. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +87 -0
  55. agentscope_runtime/sandbox/box/sandbox.py +115 -0
  56. agentscope_runtime/sandbox/box/shared/__init__.py +0 -0
  57. agentscope_runtime/sandbox/box/shared/app.py +44 -0
  58. agentscope_runtime/sandbox/box/shared/dependencies/__init__.py +5 -0
  59. agentscope_runtime/sandbox/box/shared/dependencies/deps.py +22 -0
  60. agentscope_runtime/sandbox/box/shared/routers/__init__.py +12 -0
  61. agentscope_runtime/sandbox/box/shared/routers/generic.py +173 -0
  62. agentscope_runtime/sandbox/box/shared/routers/mcp.py +207 -0
  63. agentscope_runtime/sandbox/box/shared/routers/mcp_utils.py +153 -0
  64. agentscope_runtime/sandbox/box/shared/routers/runtime_watcher.py +187 -0
  65. agentscope_runtime/sandbox/box/shared/routers/workspace.py +325 -0
  66. agentscope_runtime/sandbox/box/training_box/__init__.py +0 -0
  67. agentscope_runtime/sandbox/box/training_box/base.py +120 -0
  68. agentscope_runtime/sandbox/box/training_box/env_service.py +752 -0
  69. agentscope_runtime/sandbox/box/training_box/environments/__init__.py +0 -0
  70. agentscope_runtime/sandbox/box/training_box/environments/appworld/appworld_env.py +987 -0
  71. agentscope_runtime/sandbox/box/training_box/registry.py +54 -0
  72. agentscope_runtime/sandbox/box/training_box/src/trajectory.py +278 -0
  73. agentscope_runtime/sandbox/box/training_box/training_box.py +219 -0
  74. agentscope_runtime/sandbox/build.py +213 -0
  75. agentscope_runtime/sandbox/client/__init__.py +5 -0
  76. agentscope_runtime/sandbox/client/http_client.py +527 -0
  77. agentscope_runtime/sandbox/client/training_client.py +265 -0
  78. agentscope_runtime/sandbox/constant.py +5 -0
  79. agentscope_runtime/sandbox/custom/__init__.py +16 -0
  80. agentscope_runtime/sandbox/custom/custom_sandbox.py +40 -0
  81. agentscope_runtime/sandbox/custom/example.py +37 -0
  82. agentscope_runtime/sandbox/enums.py +68 -0
  83. agentscope_runtime/sandbox/manager/__init__.py +4 -0
  84. agentscope_runtime/sandbox/manager/collections/__init__.py +22 -0
  85. agentscope_runtime/sandbox/manager/collections/base_mapping.py +20 -0
  86. agentscope_runtime/sandbox/manager/collections/base_queue.py +25 -0
  87. agentscope_runtime/sandbox/manager/collections/base_set.py +25 -0
  88. agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +22 -0
  89. agentscope_runtime/sandbox/manager/collections/in_memory_queue.py +28 -0
  90. agentscope_runtime/sandbox/manager/collections/in_memory_set.py +27 -0
  91. agentscope_runtime/sandbox/manager/collections/redis_mapping.py +26 -0
  92. agentscope_runtime/sandbox/manager/collections/redis_queue.py +27 -0
  93. agentscope_runtime/sandbox/manager/collections/redis_set.py +23 -0
  94. agentscope_runtime/sandbox/manager/container_clients/__init__.py +8 -0
  95. agentscope_runtime/sandbox/manager/container_clients/base_client.py +39 -0
  96. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +170 -0
  97. agentscope_runtime/sandbox/manager/sandbox_manager.py +694 -0
  98. agentscope_runtime/sandbox/manager/server/__init__.py +0 -0
  99. agentscope_runtime/sandbox/manager/server/app.py +194 -0
  100. agentscope_runtime/sandbox/manager/server/config.py +68 -0
  101. agentscope_runtime/sandbox/manager/server/models.py +17 -0
  102. agentscope_runtime/sandbox/manager/storage/__init__.py +10 -0
  103. agentscope_runtime/sandbox/manager/storage/data_storage.py +16 -0
  104. agentscope_runtime/sandbox/manager/storage/local_storage.py +44 -0
  105. agentscope_runtime/sandbox/manager/storage/oss_storage.py +89 -0
  106. agentscope_runtime/sandbox/manager/utils.py +78 -0
  107. agentscope_runtime/sandbox/mcp_server.py +192 -0
  108. agentscope_runtime/sandbox/model/__init__.py +12 -0
  109. agentscope_runtime/sandbox/model/api.py +16 -0
  110. agentscope_runtime/sandbox/model/container.py +72 -0
  111. agentscope_runtime/sandbox/model/manager_config.py +158 -0
  112. agentscope_runtime/sandbox/registry.py +129 -0
  113. agentscope_runtime/sandbox/tools/__init__.py +12 -0
  114. agentscope_runtime/sandbox/tools/base/__init__.py +8 -0
  115. agentscope_runtime/sandbox/tools/base/tool.py +52 -0
  116. agentscope_runtime/sandbox/tools/browser/__init__.py +57 -0
  117. agentscope_runtime/sandbox/tools/browser/tool.py +597 -0
  118. agentscope_runtime/sandbox/tools/filesystem/__init__.py +32 -0
  119. agentscope_runtime/sandbox/tools/filesystem/tool.py +319 -0
  120. agentscope_runtime/sandbox/tools/function_tool.py +321 -0
  121. agentscope_runtime/sandbox/tools/mcp_tool.py +191 -0
  122. agentscope_runtime/sandbox/tools/sandbox_tool.py +104 -0
  123. agentscope_runtime/sandbox/tools/tool.py +123 -0
  124. agentscope_runtime/sandbox/tools/utils.py +68 -0
  125. agentscope_runtime/version.py +2 -0
  126. agentscope_runtime-0.1.0.dist-info/METADATA +327 -0
  127. agentscope_runtime-0.1.0.dist-info/RECORD +131 -0
  128. agentscope_runtime-0.1.0.dist-info/WHEEL +5 -0
  129. agentscope_runtime-0.1.0.dist-info/entry_points.txt +4 -0
  130. agentscope_runtime-0.1.0.dist-info/licenses/LICENSE +202 -0
  131. agentscope_runtime-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,153 @@
1
+ # -*- coding: utf-8 -*-
2
+ import asyncio
3
+ import logging
4
+ import os
5
+ import shutil
6
+ import traceback
7
+ from contextlib import AsyncExitStack
8
+ from typing import Any
9
+
10
+ from mcp import ClientSession, StdioServerParameters
11
+ from mcp.client.sse import sse_client
12
+ from mcp.client.stdio import stdio_client
13
+ from mcp.client.streamable_http import streamablehttp_client
14
+
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class MCPSessionHandler:
20
+ """Manages MCP server connections and tool execution."""
21
+
22
+ def __init__(self, name: str, config: dict[str, Any]) -> None:
23
+ self.name: str = name
24
+ self.config: dict[str, Any] = config
25
+ self.stdio_context: Any | None = None
26
+ self.session: ClientSession | None = None
27
+ self._cleanup_lock: asyncio.Lock = asyncio.Lock()
28
+ self._exit_stack: AsyncExitStack = AsyncExitStack()
29
+
30
+ async def initialize(self) -> None:
31
+ """Initialize the server connection."""
32
+ command = (
33
+ shutil.which("npx")
34
+ if self.config.get("command") == "npx"
35
+ else self.config.get("command")
36
+ )
37
+
38
+ try:
39
+ if command:
40
+ server_params = StdioServerParameters(
41
+ command=command,
42
+ args=self.config.get("args", []),
43
+ env={**os.environ, **self.config.get("env", {})},
44
+ )
45
+
46
+ streams = await self._exit_stack.enter_async_context(
47
+ stdio_client(server_params),
48
+ )
49
+ else:
50
+ if self.config.get("type") in [
51
+ "streamable_http",
52
+ "streamableHttp",
53
+ ]:
54
+ streams = await self._exit_stack.enter_async_context(
55
+ streamablehttp_client(url=self.config["url"]),
56
+ )
57
+ streams = (streams[0], streams[1])
58
+ else:
59
+ streams = await self._exit_stack.enter_async_context(
60
+ sse_client(url=self.config["url"]),
61
+ )
62
+ session = await self._exit_stack.enter_async_context(
63
+ ClientSession(*streams),
64
+ )
65
+ await session.initialize()
66
+ self.session = session
67
+ except Exception as e:
68
+ logging.error(f"Error initializing server {self.name}: {e}")
69
+ await self.cleanup()
70
+ raise
71
+
72
+ async def list_tools(self) -> list[Any]:
73
+ """List available tools from the server.
74
+
75
+ Returns:
76
+ A list of available tools.
77
+
78
+ Raises:
79
+ RuntimeError: If the server is not initialized.
80
+ """
81
+ if not self.session:
82
+ raise RuntimeError(f"Server {self.name} not initialized")
83
+
84
+ tools_response = await self.session.list_tools()
85
+ tools = [
86
+ tool
87
+ for item in tools_response
88
+ if isinstance(item, tuple) and item[0] == "tools"
89
+ for tool in item[1]
90
+ ]
91
+
92
+ return tools
93
+
94
+ async def call_tool(
95
+ self,
96
+ tool_name: str,
97
+ arguments: dict[str, Any],
98
+ retries: int = 2,
99
+ delay: float = 1.0,
100
+ ) -> Any:
101
+ """Execute a tool with retry mechanism.
102
+
103
+ Args:
104
+ tool_name: Name of the tool to execute.
105
+ arguments: tool arguments.
106
+ retries: Number of retry attempts.
107
+ delay: Delay between retries in seconds.
108
+
109
+ Returns:
110
+ Tool execution result.
111
+
112
+ Raises:
113
+ RuntimeError: If server is not initialized.
114
+ Exception: If tool execution fails after all retries.
115
+ """
116
+ if not self.session:
117
+ raise RuntimeError(f"Server {self.name} not initialized")
118
+
119
+ attempt = 0
120
+
121
+ while attempt < retries:
122
+ try:
123
+ logging.info(f"Executing {tool_name}...")
124
+ result = await self.session.call_tool(tool_name, arguments)
125
+ return result
126
+
127
+ except Exception as e:
128
+ attempt += 1
129
+ logging.warning(
130
+ f"Error executing tool: {e} {traceback.format_exc()}."
131
+ f" Attempt {attempt} of {retries}.",
132
+ )
133
+ if attempt >= retries:
134
+ logging.error("Max retries reached. Failing.")
135
+ raise
136
+ logging.info(f"Retrying in {delay} seconds...")
137
+ await asyncio.sleep(delay)
138
+ return None
139
+
140
+ async def cleanup(self) -> None:
141
+ """Clean up server resources."""
142
+ async with self._cleanup_lock:
143
+ try:
144
+ await self._exit_stack.aclose()
145
+ except Exception as e:
146
+ if (
147
+ "Attempted to exit cancel scope in a different task"
148
+ in str(e)
149
+ ):
150
+ pass
151
+ finally:
152
+ self.session = None
153
+ self.stdio_context = None
@@ -0,0 +1,187 @@
1
+ # -*- coding: utf-8 -*-
2
+ import difflib
3
+ import logging
4
+ import traceback
5
+
6
+ import git
7
+ from fastapi import APIRouter, Body, HTTPException
8
+
9
+ watcher_router = APIRouter()
10
+
11
+
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def initialize_git_user(repo):
17
+ repo.config_writer().set_value("user", "name", "User").release()
18
+ repo.config_writer().set_value(
19
+ "user",
20
+ "email",
21
+ "user@example.com",
22
+ ).release()
23
+ return repo
24
+
25
+
26
+ @watcher_router.post(
27
+ "/watcher/commit_changes",
28
+ summary="...",
29
+ )
30
+ async def commit_changes(
31
+ commit_message: str = Body(
32
+ "Automated commit",
33
+ example="Your commit message",
34
+ embed=True,
35
+ ),
36
+ ):
37
+ """
38
+ Commit the uncommitted changes.
39
+ """
40
+ try:
41
+ repo_path = "."
42
+
43
+ repo = git.Repo(repo_path)
44
+ repo = initialize_git_user(repo)
45
+
46
+ # Add all changes to the staging area
47
+ repo.git.add(A=True)
48
+
49
+ # Commit the changes
50
+ commit = repo.index.commit(commit_message)
51
+ return {"commit": commit.hexsha, "message": commit_message}
52
+
53
+ except Exception as e:
54
+ logger.error(f"{str(e)}:\n{traceback.format_exc()}")
55
+ raise HTTPException(
56
+ status_code=500,
57
+ detail=f"{str(e)}: {traceback.format_exc()}",
58
+ ) from e
59
+
60
+
61
+ @watcher_router.post(
62
+ "/watcher/generate_diff",
63
+ summary="...",
64
+ )
65
+ async def generate_diff(
66
+ commit_a: str = Body(..., embed=True),
67
+ commit_b: str = Body(..., embed=True),
68
+ ):
69
+ """
70
+ Generate the diff of the uncommitted changes or two commits.
71
+ """
72
+ try:
73
+ repo_path = "."
74
+ repo = git.Repo(repo_path)
75
+ repo = initialize_git_user(repo)
76
+
77
+ if not commit_a and not commit_b:
78
+ # Default to uncommitted changes compared to the last commit
79
+ repo.git.add(A=True)
80
+ diff_index = repo.index.diff("HEAD")
81
+ print(diff_index, repo.git.status())
82
+ elif commit_a and commit_b:
83
+ # Get diff between two commits
84
+ diff_index = repo.commit(commit_a).diff(commit_b)
85
+ else:
86
+ return HTTPException(
87
+ detail="Invalid commit range",
88
+ status_code=400,
89
+ )
90
+ diffs = {}
91
+ for diff in diff_index:
92
+ if diff.a_blob and diff.b_blob:
93
+ # Both files are present in commits; perform a diff
94
+ a_content = (
95
+ diff.a_blob.data_stream.read()
96
+ .decode(
97
+ "utf-8",
98
+ )
99
+ .splitlines()
100
+ )
101
+ b_content = (
102
+ diff.b_blob.data_stream.read()
103
+ .decode(
104
+ "utf-8",
105
+ )
106
+ .splitlines()
107
+ )
108
+ elif diff.a_blob: # File was deleted
109
+ # Only 'a' file is present; 'b' file is empty
110
+ a_content = (
111
+ diff.a_blob.data_stream.read()
112
+ .decode(
113
+ "utf-8",
114
+ )
115
+ .splitlines()
116
+ )
117
+ b_content = []
118
+ elif diff.b_blob: # File was added
119
+ # Only 'b' file is present; 'a' file is empty
120
+ a_content = []
121
+ b_content = (
122
+ diff.b_blob.data_stream.read()
123
+ .decode(
124
+ "utf-8",
125
+ )
126
+ .splitlines()
127
+ )
128
+ else:
129
+ continue
130
+
131
+ # Generate the diff content
132
+ diff_text = "\n".join(
133
+ difflib.unified_diff(
134
+ a_content,
135
+ b_content,
136
+ fromfile=f"a/{diff.a_path}",
137
+ tofile=f"b/{diff.b_path}",
138
+ lineterm="",
139
+ ),
140
+ )
141
+ diffs[diff.b_path or diff.a_path] = diff_text
142
+ return {"diffs": diffs}
143
+
144
+ except Exception as e:
145
+ logger.error(f"{str(e)}:\n{traceback.format_exc()}")
146
+ raise HTTPException(
147
+ status_code=500,
148
+ detail=f"{str(e)}: {traceback.format_exc()}",
149
+ ) from e
150
+
151
+
152
+ @watcher_router.get(
153
+ "/watcher/git_logs",
154
+ summary="...",
155
+ )
156
+ async def git_logs():
157
+ """
158
+ Return the git logs.
159
+ """
160
+ try:
161
+ repo = git.Repo(".")
162
+ repo = initialize_git_user(repo)
163
+ logs = []
164
+ for commit in repo.iter_commits():
165
+ diff_result = {"diffs": {}}
166
+ if commit.parents:
167
+ parent_commit = commit.parents[0]
168
+ diff_result = await generate_diff(
169
+ commit.hexsha,
170
+ parent_commit.hexsha,
171
+ )
172
+
173
+ log_entry = {
174
+ "commit": commit.hexsha,
175
+ "author": commit.author.name,
176
+ "date": commit.committed_datetime.isoformat(),
177
+ "message": commit.message.strip(),
178
+ "diff": diff_result["diffs"],
179
+ }
180
+ logs.append(log_entry)
181
+ return {"logs": logs}
182
+ except Exception as e:
183
+ logger.error(f"{str(e)}:\n{traceback.format_exc()}")
184
+ raise HTTPException(
185
+ status_code=500,
186
+ detail=f"{str(e)}: {traceback.format_exc()}",
187
+ ) from e
@@ -0,0 +1,325 @@
1
+ # -*- coding: utf-8 -*-
2
+ import shutil
3
+ import os
4
+ import logging
5
+ import traceback
6
+
7
+ import aiofiles
8
+
9
+ from fastapi import APIRouter, HTTPException, Query, Body
10
+ from fastapi.responses import FileResponse
11
+
12
+ workspace_router = APIRouter()
13
+
14
+ # Configure logging
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def ensure_within_workspace(
20
+ path: str,
21
+ base_directory: str = "/workspace",
22
+ ) -> str:
23
+ """
24
+ Ensure the provided path is within the /workspace directory.
25
+ """
26
+ base_directory = os.path.abspath(base_directory)
27
+
28
+ # Determine if the input path is absolute or relative
29
+ if os.path.isabs(path):
30
+ full_path = os.path.abspath(path)
31
+ else:
32
+ full_path = os.path.abspath(os.path.join(base_directory, path))
33
+
34
+ # Check for path traversal attacks and ensure path is within base_directory
35
+ if not full_path.startswith(base_directory):
36
+ raise HTTPException(
37
+ status_code=403,
38
+ detail="Permission error. Access restricted to /workspace "
39
+ "directory.",
40
+ )
41
+
42
+ return full_path
43
+
44
+
45
+ @workspace_router.get(
46
+ "/workspace/files",
47
+ summary="Retrieve a file within the /workspace directory",
48
+ )
49
+ async def get_workspace_file(
50
+ file_path: str = Query(
51
+ ...,
52
+ description="Path to the file within /workspace relative to its root",
53
+ ),
54
+ ):
55
+ """
56
+ Get a file within the /workspace directory.
57
+ """
58
+ try:
59
+ # Ensure the file path is within the /workspace directory
60
+ full_path = ensure_within_workspace(file_path)
61
+
62
+ # Check if the file exists
63
+ if not os.path.isfile(full_path):
64
+ raise HTTPException(status_code=404, detail="File not found.")
65
+
66
+ # Return the file using FileResponse
67
+ return FileResponse(
68
+ full_path,
69
+ media_type="application/octet-stream",
70
+ filename=os.path.basename(full_path),
71
+ )
72
+
73
+ except Exception as e:
74
+ logger.error(f"{str(e)}:\n{traceback.format_exc()}")
75
+ raise HTTPException(
76
+ status_code=500,
77
+ detail=f"{str(e)}: {traceback.format_exc()}",
78
+ ) from e
79
+
80
+
81
+ @workspace_router.post(
82
+ "/workspace/files",
83
+ summary="Create or edit a file within the /workspace directory",
84
+ )
85
+ async def create_or_edit_file(
86
+ file_path: str = Query(
87
+ ...,
88
+ description="Path to the file within /workspace",
89
+ ),
90
+ content: str = Body(..., description="Content to write to the file"),
91
+ ):
92
+ try:
93
+ full_path = ensure_within_workspace(file_path)
94
+ async with aiofiles.open(full_path, "w", encoding="utf-8") as f:
95
+ await f.write(content)
96
+ return {"message": "File created or edited successfully."}
97
+ except Exception as e:
98
+ logger.error(
99
+ f"Error creating or editing file: {str(e)}:\
100
+ n{traceback.format_exc()}",
101
+ )
102
+ raise HTTPException(
103
+ status_code=500,
104
+ detail=f"Error creating or editing file: {str(e)}",
105
+ ) from e
106
+
107
+
108
+ @workspace_router.get(
109
+ "/workspace/list-directories",
110
+ summary="List file items in the /workspace directory, including nested "
111
+ "files and directories",
112
+ )
113
+ async def list_workspace_files(
114
+ directory: str = Query(
115
+ "/workspace",
116
+ description="Directory to list files and directories from, default "
117
+ "is /workspace.",
118
+ ),
119
+ ):
120
+ """
121
+ List all files and directories in the specified directory, including
122
+ nested items, with type indication and statistics.
123
+ """
124
+ try:
125
+ target_directory = ensure_within_workspace(directory)
126
+
127
+ # Verify if the specified directory exists
128
+ if not os.path.isdir(target_directory):
129
+ raise HTTPException(status_code=404, detail="Directory not found.")
130
+
131
+ nested_items = []
132
+ file_count = 0
133
+ directory_count = 0
134
+
135
+ for root, dirs, files in os.walk(target_directory):
136
+ for d in dirs:
137
+ dir_path = os.path.join(root, d)
138
+ nested_items.append(
139
+ {
140
+ "type": "directory",
141
+ "path": os.path.relpath(dir_path, target_directory),
142
+ },
143
+ )
144
+ directory_count += 1
145
+
146
+ for f in files:
147
+ file_path = os.path.join(root, f)
148
+ nested_items.append(
149
+ {
150
+ "type": "file",
151
+ "path": os.path.relpath(file_path, target_directory),
152
+ },
153
+ )
154
+ file_count += 1
155
+
156
+ return {
157
+ "items": nested_items,
158
+ "statistics": {
159
+ "total_directories": directory_count,
160
+ "total_files": file_count,
161
+ },
162
+ }
163
+
164
+ except Exception as e:
165
+ logger.error(
166
+ f"Error listing files: {str(e)}:\n{traceback.format_exc()}",
167
+ )
168
+ raise HTTPException(
169
+ status_code=500,
170
+ detail=f"An error occurred while listing files: {str(e)}",
171
+ ) from e
172
+
173
+
174
+ @workspace_router.post(
175
+ "/workspace/directories",
176
+ summary="Create a directory within the /workspace directory",
177
+ )
178
+ async def create_directory(
179
+ directory_path: str = Query(
180
+ ...,
181
+ description="Path to the directory within /workspace",
182
+ ),
183
+ ):
184
+ try:
185
+ full_path = ensure_within_workspace(directory_path)
186
+ os.makedirs(full_path, exist_ok=True)
187
+ return {"message": "Directory created successfully."}
188
+ except Exception as e:
189
+ logger.error(
190
+ f"Error creating directory: {str(e)}:\n{traceback.format_exc()}",
191
+ )
192
+ raise HTTPException(
193
+ status_code=500,
194
+ detail=f"Error creating directory: {str(e)}",
195
+ ) from e
196
+
197
+
198
+ @workspace_router.delete(
199
+ "/workspace/files",
200
+ summary="Delete a file within the /workspace directory",
201
+ )
202
+ async def delete_file(
203
+ file_path: str = Query(
204
+ ...,
205
+ description="Path to the file within /workspace",
206
+ ),
207
+ ):
208
+ try:
209
+ full_path = ensure_within_workspace(file_path)
210
+ if os.path.isfile(full_path):
211
+ os.remove(full_path)
212
+ return {"message": "File deleted successfully."}
213
+ else:
214
+ raise HTTPException(status_code=404, detail="File not found.")
215
+ except Exception as e:
216
+ logger.error(
217
+ f"Error deleting file: {str(e)}:\n{traceback.format_exc()}",
218
+ )
219
+ raise HTTPException(
220
+ status_code=500,
221
+ detail=f"Error deleting file: {str(e)}",
222
+ ) from e
223
+
224
+
225
+ @workspace_router.delete(
226
+ "/workspace/directories",
227
+ summary="Delete a directory within the /workspace directory",
228
+ )
229
+ async def delete_directory(
230
+ directory_path: str = Query(
231
+ ...,
232
+ description="Path to the directory within /workspace",
233
+ ),
234
+ recursive: bool = Query(
235
+ False,
236
+ description="Recursively delete directory contents",
237
+ ),
238
+ ):
239
+ try:
240
+ full_path = ensure_within_workspace(directory_path)
241
+ if recursive:
242
+ shutil.rmtree(full_path)
243
+ else:
244
+ os.rmdir(full_path)
245
+ return {"message": "Directory deleted successfully."}
246
+ except Exception as e:
247
+ logger.error(
248
+ f"Error deleting directory: {str(e)}:\n{traceback.format_exc()}",
249
+ )
250
+ raise HTTPException(
251
+ status_code=500,
252
+ detail=f"Error deleting directory: {str(e)}",
253
+ ) from e
254
+
255
+
256
+ @workspace_router.put(
257
+ "/workspace/move",
258
+ summary="Move or rename a file or directory within the /workspace "
259
+ "directory",
260
+ )
261
+ async def move_or_rename(
262
+ source_path: str = Query(
263
+ ...,
264
+ description="Source path within /workspace",
265
+ ),
266
+ destination_path: str = Query(
267
+ ...,
268
+ description="Destination path within /workspace",
269
+ ),
270
+ ):
271
+ try:
272
+ full_source_path = ensure_within_workspace(source_path)
273
+ full_destination_path = ensure_within_workspace(destination_path)
274
+ if not os.path.exists(full_source_path):
275
+ raise HTTPException(
276
+ status_code=404,
277
+ detail="Source file or directory not found.",
278
+ )
279
+ os.rename(full_source_path, full_destination_path)
280
+ return {"message": "Move or rename operation successful."}
281
+ except Exception as e:
282
+ logger.error(
283
+ f"Error moving or renaming: {str(e)}:\n{traceback.format_exc()}",
284
+ )
285
+ raise HTTPException(
286
+ status_code=500,
287
+ detail=f"Error moving or renaming: {str(e)}",
288
+ ) from e
289
+
290
+
291
+ @workspace_router.post(
292
+ "/workspace/copy",
293
+ summary="Copy a file or directory within the /workspace directory",
294
+ )
295
+ async def copy(
296
+ source_path: str = Query(
297
+ ...,
298
+ description="Source path within /workspace",
299
+ ),
300
+ destination_path: str = Query(
301
+ ...,
302
+ description="Destination path within /workspace",
303
+ ),
304
+ ):
305
+ try:
306
+ full_source_path = ensure_within_workspace(source_path)
307
+ full_destination_path = ensure_within_workspace(destination_path)
308
+ if not os.path.exists(full_source_path):
309
+ raise HTTPException(
310
+ status_code=404,
311
+ detail="Source file or directory not found.",
312
+ )
313
+
314
+ if os.path.isdir(full_source_path):
315
+ shutil.copytree(full_source_path, full_destination_path)
316
+ else:
317
+ shutil.copy2(full_source_path, full_destination_path)
318
+
319
+ return {"message": "Copy operation successful."}
320
+ except Exception as e:
321
+ logger.error(f"Error copying: {str(e)}:\n{traceback.format_exc()}")
322
+ raise HTTPException(
323
+ status_code=500,
324
+ detail=f"Error copying: " f"{str(e)}",
325
+ ) from e