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.
- agentscope_runtime/__init__.py +4 -0
- agentscope_runtime/engine/__init__.py +9 -0
- agentscope_runtime/engine/agents/__init__.py +2 -0
- agentscope_runtime/engine/agents/agentscope_agent/__init__.py +6 -0
- agentscope_runtime/engine/agents/agentscope_agent/agent.py +342 -0
- agentscope_runtime/engine/agents/agentscope_agent/hooks.py +156 -0
- agentscope_runtime/engine/agents/agno_agent.py +220 -0
- agentscope_runtime/engine/agents/base_agent.py +29 -0
- agentscope_runtime/engine/agents/langgraph_agent.py +59 -0
- agentscope_runtime/engine/agents/llm_agent.py +51 -0
- agentscope_runtime/engine/deployers/__init__.py +3 -0
- agentscope_runtime/engine/deployers/adapter/__init__.py +0 -0
- agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +2 -0
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_adapter_utils.py +425 -0
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_agent_adapter.py +69 -0
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +60 -0
- agentscope_runtime/engine/deployers/adapter/protocol_adapter.py +24 -0
- agentscope_runtime/engine/deployers/base.py +17 -0
- agentscope_runtime/engine/deployers/local_deployer.py +586 -0
- agentscope_runtime/engine/helpers/helper.py +127 -0
- agentscope_runtime/engine/llms/__init__.py +3 -0
- agentscope_runtime/engine/llms/base_llm.py +60 -0
- agentscope_runtime/engine/llms/qwen_llm.py +47 -0
- agentscope_runtime/engine/misc/__init__.py +0 -0
- agentscope_runtime/engine/runner.py +186 -0
- agentscope_runtime/engine/schemas/__init__.py +0 -0
- agentscope_runtime/engine/schemas/agent_schemas.py +551 -0
- agentscope_runtime/engine/schemas/context.py +54 -0
- agentscope_runtime/engine/services/__init__.py +9 -0
- agentscope_runtime/engine/services/base.py +77 -0
- agentscope_runtime/engine/services/context_manager.py +129 -0
- agentscope_runtime/engine/services/environment_manager.py +50 -0
- agentscope_runtime/engine/services/manager.py +174 -0
- agentscope_runtime/engine/services/memory_service.py +270 -0
- agentscope_runtime/engine/services/sandbox_service.py +198 -0
- agentscope_runtime/engine/services/session_history_service.py +256 -0
- agentscope_runtime/engine/tracing/__init__.py +40 -0
- agentscope_runtime/engine/tracing/base.py +309 -0
- agentscope_runtime/engine/tracing/local_logging_handler.py +356 -0
- agentscope_runtime/engine/tracing/tracing_metric.py +69 -0
- agentscope_runtime/engine/tracing/wrapper.py +321 -0
- agentscope_runtime/sandbox/__init__.py +14 -0
- agentscope_runtime/sandbox/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/base/__init__.py +0 -0
- agentscope_runtime/sandbox/box/base/base_sandbox.py +37 -0
- agentscope_runtime/sandbox/box/base/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/browser/__init__.py +0 -0
- agentscope_runtime/sandbox/box/browser/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/browser/browser_sandbox.py +176 -0
- agentscope_runtime/sandbox/box/dummy/__init__.py +0 -0
- agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +26 -0
- agentscope_runtime/sandbox/box/filesystem/__init__.py +0 -0
- agentscope_runtime/sandbox/box/filesystem/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +87 -0
- agentscope_runtime/sandbox/box/sandbox.py +115 -0
- agentscope_runtime/sandbox/box/shared/__init__.py +0 -0
- agentscope_runtime/sandbox/box/shared/app.py +44 -0
- agentscope_runtime/sandbox/box/shared/dependencies/__init__.py +5 -0
- agentscope_runtime/sandbox/box/shared/dependencies/deps.py +22 -0
- agentscope_runtime/sandbox/box/shared/routers/__init__.py +12 -0
- agentscope_runtime/sandbox/box/shared/routers/generic.py +173 -0
- agentscope_runtime/sandbox/box/shared/routers/mcp.py +207 -0
- agentscope_runtime/sandbox/box/shared/routers/mcp_utils.py +153 -0
- agentscope_runtime/sandbox/box/shared/routers/runtime_watcher.py +187 -0
- agentscope_runtime/sandbox/box/shared/routers/workspace.py +325 -0
- agentscope_runtime/sandbox/box/training_box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/training_box/base.py +120 -0
- agentscope_runtime/sandbox/box/training_box/env_service.py +752 -0
- agentscope_runtime/sandbox/box/training_box/environments/__init__.py +0 -0
- agentscope_runtime/sandbox/box/training_box/environments/appworld/appworld_env.py +987 -0
- agentscope_runtime/sandbox/box/training_box/registry.py +54 -0
- agentscope_runtime/sandbox/box/training_box/src/trajectory.py +278 -0
- agentscope_runtime/sandbox/box/training_box/training_box.py +219 -0
- agentscope_runtime/sandbox/build.py +213 -0
- agentscope_runtime/sandbox/client/__init__.py +5 -0
- agentscope_runtime/sandbox/client/http_client.py +527 -0
- agentscope_runtime/sandbox/client/training_client.py +265 -0
- agentscope_runtime/sandbox/constant.py +5 -0
- agentscope_runtime/sandbox/custom/__init__.py +16 -0
- agentscope_runtime/sandbox/custom/custom_sandbox.py +40 -0
- agentscope_runtime/sandbox/custom/example.py +37 -0
- agentscope_runtime/sandbox/enums.py +68 -0
- agentscope_runtime/sandbox/manager/__init__.py +4 -0
- agentscope_runtime/sandbox/manager/collections/__init__.py +22 -0
- agentscope_runtime/sandbox/manager/collections/base_mapping.py +20 -0
- agentscope_runtime/sandbox/manager/collections/base_queue.py +25 -0
- agentscope_runtime/sandbox/manager/collections/base_set.py +25 -0
- agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +22 -0
- agentscope_runtime/sandbox/manager/collections/in_memory_queue.py +28 -0
- agentscope_runtime/sandbox/manager/collections/in_memory_set.py +27 -0
- agentscope_runtime/sandbox/manager/collections/redis_mapping.py +26 -0
- agentscope_runtime/sandbox/manager/collections/redis_queue.py +27 -0
- agentscope_runtime/sandbox/manager/collections/redis_set.py +23 -0
- agentscope_runtime/sandbox/manager/container_clients/__init__.py +8 -0
- agentscope_runtime/sandbox/manager/container_clients/base_client.py +39 -0
- agentscope_runtime/sandbox/manager/container_clients/docker_client.py +170 -0
- agentscope_runtime/sandbox/manager/sandbox_manager.py +694 -0
- agentscope_runtime/sandbox/manager/server/__init__.py +0 -0
- agentscope_runtime/sandbox/manager/server/app.py +194 -0
- agentscope_runtime/sandbox/manager/server/config.py +68 -0
- agentscope_runtime/sandbox/manager/server/models.py +17 -0
- agentscope_runtime/sandbox/manager/storage/__init__.py +10 -0
- agentscope_runtime/sandbox/manager/storage/data_storage.py +16 -0
- agentscope_runtime/sandbox/manager/storage/local_storage.py +44 -0
- agentscope_runtime/sandbox/manager/storage/oss_storage.py +89 -0
- agentscope_runtime/sandbox/manager/utils.py +78 -0
- agentscope_runtime/sandbox/mcp_server.py +192 -0
- agentscope_runtime/sandbox/model/__init__.py +12 -0
- agentscope_runtime/sandbox/model/api.py +16 -0
- agentscope_runtime/sandbox/model/container.py +72 -0
- agentscope_runtime/sandbox/model/manager_config.py +158 -0
- agentscope_runtime/sandbox/registry.py +129 -0
- agentscope_runtime/sandbox/tools/__init__.py +12 -0
- agentscope_runtime/sandbox/tools/base/__init__.py +8 -0
- agentscope_runtime/sandbox/tools/base/tool.py +52 -0
- agentscope_runtime/sandbox/tools/browser/__init__.py +57 -0
- agentscope_runtime/sandbox/tools/browser/tool.py +597 -0
- agentscope_runtime/sandbox/tools/filesystem/__init__.py +32 -0
- agentscope_runtime/sandbox/tools/filesystem/tool.py +319 -0
- agentscope_runtime/sandbox/tools/function_tool.py +321 -0
- agentscope_runtime/sandbox/tools/mcp_tool.py +191 -0
- agentscope_runtime/sandbox/tools/sandbox_tool.py +104 -0
- agentscope_runtime/sandbox/tools/tool.py +123 -0
- agentscope_runtime/sandbox/tools/utils.py +68 -0
- agentscope_runtime/version.py +2 -0
- agentscope_runtime-0.1.0.dist-info/METADATA +327 -0
- agentscope_runtime-0.1.0.dist-info/RECORD +131 -0
- agentscope_runtime-0.1.0.dist-info/WHEEL +5 -0
- agentscope_runtime-0.1.0.dist-info/entry_points.txt +4 -0
- agentscope_runtime-0.1.0.dist-info/licenses/LICENSE +202 -0
- 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
|
|
File without changes
|