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,87 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# pylint: disable=dangerous-default-value
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from ...constant import IMAGE_TAG
|
|
6
|
+
from ...registry import SandboxRegistry
|
|
7
|
+
from ...enums import SandboxType
|
|
8
|
+
from ...box.sandbox import Sandbox
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@SandboxRegistry.register(
|
|
12
|
+
f"agentscope/runtime-sandbox-filesystem:{IMAGE_TAG}",
|
|
13
|
+
sandbox_type=SandboxType.FILESYSTEM,
|
|
14
|
+
security_level="medium",
|
|
15
|
+
timeout=60,
|
|
16
|
+
description="Filesystem sandbox",
|
|
17
|
+
)
|
|
18
|
+
class FilesystemSandbox(Sandbox):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
sandbox_id: Optional[str] = None,
|
|
22
|
+
timeout: int = 3000,
|
|
23
|
+
base_url: Optional[str] = None,
|
|
24
|
+
bearer_token: Optional[str] = None,
|
|
25
|
+
):
|
|
26
|
+
super().__init__(
|
|
27
|
+
sandbox_id,
|
|
28
|
+
timeout,
|
|
29
|
+
base_url,
|
|
30
|
+
bearer_token,
|
|
31
|
+
SandboxType.FILESYSTEM,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def read_file(self, path: str):
|
|
35
|
+
return self.call_tool("read_file", {"path": path})
|
|
36
|
+
|
|
37
|
+
def read_multiple_files(self, paths: list):
|
|
38
|
+
return self.call_tool("read_multiple_files", {"paths": paths})
|
|
39
|
+
|
|
40
|
+
def write_file(self, path: str, content: str):
|
|
41
|
+
return self.call_tool("write_file", {"path": path, "content": content})
|
|
42
|
+
|
|
43
|
+
def edit_file(self, path: str, edits: list, dry_run: bool = False):
|
|
44
|
+
return self.call_tool(
|
|
45
|
+
"edit_file",
|
|
46
|
+
{
|
|
47
|
+
"path": path,
|
|
48
|
+
"edits": edits,
|
|
49
|
+
"dryRun": dry_run,
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def create_directory(self, path: str):
|
|
54
|
+
return self.call_tool("create_directory", {"path": path})
|
|
55
|
+
|
|
56
|
+
def list_directory(self, path: str):
|
|
57
|
+
return self.call_tool("list_directory", {"path": path})
|
|
58
|
+
|
|
59
|
+
def directory_tree(self, path: str):
|
|
60
|
+
return self.call_tool("directory_tree", {"path": path})
|
|
61
|
+
|
|
62
|
+
def move_file(self, source: str, destination: str):
|
|
63
|
+
return self.call_tool(
|
|
64
|
+
"move_file",
|
|
65
|
+
{"source": source, "destination": destination},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def search_files(
|
|
69
|
+
self,
|
|
70
|
+
path: str,
|
|
71
|
+
pattern: str,
|
|
72
|
+
exclude_patterns: list = [],
|
|
73
|
+
):
|
|
74
|
+
return self.call_tool(
|
|
75
|
+
"search_files",
|
|
76
|
+
{
|
|
77
|
+
"path": path,
|
|
78
|
+
"pattern": pattern,
|
|
79
|
+
"excludePatterns": exclude_patterns,
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def get_file_info(self, path: str):
|
|
84
|
+
return self.call_tool("get_file_info", {"path": path})
|
|
85
|
+
|
|
86
|
+
def list_allowed_directories(self):
|
|
87
|
+
return self.call_tool("list_allowed_directories", {})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from ..enums import SandboxType
|
|
6
|
+
from ..manager.sandbox_manager import SandboxManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(level=logging.INFO)
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Sandbox:
|
|
14
|
+
"""
|
|
15
|
+
Sandbox Interface.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
sandbox_id: Optional[str] = None,
|
|
21
|
+
timeout: int = 3000, # TODO: enable life circle management
|
|
22
|
+
base_url: Optional[str] = None,
|
|
23
|
+
bearer_token: Optional[str] = None, # TODO: support api_key
|
|
24
|
+
sandbox_type: SandboxType = SandboxType.BASE,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Initialize the sandbox interface.
|
|
28
|
+
"""
|
|
29
|
+
if base_url:
|
|
30
|
+
self.embed_mode = False
|
|
31
|
+
self.manager_api = SandboxManager(
|
|
32
|
+
base_url=base_url,
|
|
33
|
+
bearer_token=bearer_token,
|
|
34
|
+
).__enter__()
|
|
35
|
+
else:
|
|
36
|
+
# Launch a local manager
|
|
37
|
+
self.embed_mode = True
|
|
38
|
+
self.manager_api = SandboxManager(
|
|
39
|
+
default_type=sandbox_type,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if sandbox_id is None:
|
|
43
|
+
logger.debug(
|
|
44
|
+
"You are using embed mode, it might take several seconds to "
|
|
45
|
+
"init the runtime, please wait.",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
sandbox_id = self.manager_api.create_from_pool(
|
|
49
|
+
sandbox_type=sandbox_type.value,
|
|
50
|
+
)
|
|
51
|
+
if sandbox_id is None:
|
|
52
|
+
raise RuntimeError(
|
|
53
|
+
"No sandbox available. "
|
|
54
|
+
"Please check if sandbox images exist, build or pull "
|
|
55
|
+
"missing images in sandbox server.",
|
|
56
|
+
)
|
|
57
|
+
self._sandbox_id = sandbox_id
|
|
58
|
+
|
|
59
|
+
self._sandbox_id = sandbox_id
|
|
60
|
+
self.sandbox_type = sandbox_type
|
|
61
|
+
self.timeout = timeout
|
|
62
|
+
|
|
63
|
+
def __enter__(self):
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
67
|
+
# Remote not need to close the embed_manager
|
|
68
|
+
if self.embed_mode:
|
|
69
|
+
# Clean all
|
|
70
|
+
self.manager_api.__exit__(exc_type, exc_value, traceback)
|
|
71
|
+
else:
|
|
72
|
+
# Clean the specific sandbox
|
|
73
|
+
self.manager_api.release(self.sandbox_id)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def sandbox_id(self) -> str:
|
|
77
|
+
"""Get the sandbox ID."""
|
|
78
|
+
return self._sandbox_id
|
|
79
|
+
|
|
80
|
+
@sandbox_id.setter
|
|
81
|
+
def sandbox_id(self, value: str) -> None:
|
|
82
|
+
"""Set the sandbox ID."""
|
|
83
|
+
if not value:
|
|
84
|
+
raise ValueError("Sandbox ID cannot be empty.")
|
|
85
|
+
self._sandbox_id = value
|
|
86
|
+
|
|
87
|
+
def get_info(self) -> dict:
|
|
88
|
+
return self.manager_api.get_info(self.sandbox_id)
|
|
89
|
+
|
|
90
|
+
def list_tools(self, tool_type: Optional[str] = None) -> dict:
|
|
91
|
+
return self.manager_api.list_tools(
|
|
92
|
+
self.sandbox_id,
|
|
93
|
+
tool_type=tool_type,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def call_tool(
|
|
97
|
+
self,
|
|
98
|
+
name: str,
|
|
99
|
+
arguments: Optional[dict[str, Any]] = None,
|
|
100
|
+
) -> Any:
|
|
101
|
+
if arguments is None:
|
|
102
|
+
arguments = {}
|
|
103
|
+
|
|
104
|
+
return self.manager_api.call_tool(self.sandbox_id, name, arguments)
|
|
105
|
+
|
|
106
|
+
def add_mcp_servers(
|
|
107
|
+
self,
|
|
108
|
+
server_configs: dict,
|
|
109
|
+
overwrite=False,
|
|
110
|
+
):
|
|
111
|
+
return self.manager_api.add_mcp_servers(
|
|
112
|
+
self.sandbox_id,
|
|
113
|
+
server_configs,
|
|
114
|
+
overwrite,
|
|
115
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from fastapi import FastAPI, Response, Depends
|
|
5
|
+
from routers import (
|
|
6
|
+
generic_router,
|
|
7
|
+
mcp_router,
|
|
8
|
+
watcher_router,
|
|
9
|
+
workspace_router,
|
|
10
|
+
)
|
|
11
|
+
from dependencies import verify_secret_token
|
|
12
|
+
|
|
13
|
+
logging.basicConfig(level=logging.INFO)
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Initialize FastAPI app
|
|
17
|
+
app = FastAPI(
|
|
18
|
+
title="AgentScope Runtime Sandbox Server",
|
|
19
|
+
version="1.0",
|
|
20
|
+
description="Agentscope runtime sandbox server.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.get(
|
|
25
|
+
"/healthz",
|
|
26
|
+
summary="Check the health of the API",
|
|
27
|
+
dependencies=[Depends(verify_secret_token)],
|
|
28
|
+
)
|
|
29
|
+
async def healthz():
|
|
30
|
+
return Response(content="OK", status_code=200)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
app.include_router(mcp_router, dependencies=[Depends(verify_secret_token)])
|
|
34
|
+
app.include_router(generic_router, dependencies=[Depends(verify_secret_token)])
|
|
35
|
+
app.include_router(watcher_router, dependencies=[Depends(verify_secret_token)])
|
|
36
|
+
app.include_router(
|
|
37
|
+
workspace_router,
|
|
38
|
+
dependencies=[Depends(verify_secret_token)],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
import uvicorn
|
|
43
|
+
|
|
44
|
+
uvicorn.run(app, host="0.0.0.0", port=8000, workers=1)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from fastapi import Header, HTTPException, status
|
|
6
|
+
|
|
7
|
+
SECRET_TOKEN = os.getenv("SECRET_TOKEN", "secret_token123")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def verify_secret_token(authorization: Optional[str] = Header(None)):
|
|
11
|
+
if authorization is None or not authorization.startswith("Bearer "):
|
|
12
|
+
raise HTTPException(
|
|
13
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
14
|
+
detail="Missing or invalid authorization header",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
token = authorization.split("Bearer ")[1]
|
|
18
|
+
if token != SECRET_TOKEN:
|
|
19
|
+
raise HTTPException(
|
|
20
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
21
|
+
detail="Invalid secret token",
|
|
22
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from .generic import generic_router
|
|
3
|
+
from .mcp import mcp_router
|
|
4
|
+
from .runtime_watcher import watcher_router
|
|
5
|
+
from .workspace import workspace_router
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"mcp_router",
|
|
9
|
+
"generic_router",
|
|
10
|
+
"watcher_router",
|
|
11
|
+
"workspace_router",
|
|
12
|
+
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import io
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
import traceback
|
|
6
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Body, HTTPException
|
|
9
|
+
from IPython.core.interactiveshell import InteractiveShell
|
|
10
|
+
from mcp.types import CallToolResult, TextContent
|
|
11
|
+
|
|
12
|
+
SPLIT_OUTPUT_MODE = True
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
generic_router = APIRouter()
|
|
16
|
+
|
|
17
|
+
# Initialize IPython shell
|
|
18
|
+
ipy = InteractiveShell.instance()
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(level=logging.INFO)
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@generic_router.post(
|
|
25
|
+
"/tools/run_ipython_cell",
|
|
26
|
+
summary="Invoke a cell in a stateful IPython (Jupyter) kernel",
|
|
27
|
+
)
|
|
28
|
+
async def run_ipython_cell(
|
|
29
|
+
code: str = Body(
|
|
30
|
+
...,
|
|
31
|
+
example="print('Hello World')",
|
|
32
|
+
embed=True,
|
|
33
|
+
),
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Execute code in an IPython kernel and return the results.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
if not code:
|
|
40
|
+
raise HTTPException(status_code=400, detail="Code is required.")
|
|
41
|
+
|
|
42
|
+
# Capture stdout and stderr separately
|
|
43
|
+
stdout_buf = io.StringIO()
|
|
44
|
+
stderr_buf = io.StringIO()
|
|
45
|
+
|
|
46
|
+
with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
|
|
47
|
+
ipy.run_cell(code)
|
|
48
|
+
|
|
49
|
+
stdout_content = stdout_buf.getvalue()
|
|
50
|
+
stderr_content = stderr_buf.getvalue()
|
|
51
|
+
|
|
52
|
+
content_list = []
|
|
53
|
+
|
|
54
|
+
if SPLIT_OUTPUT_MODE:
|
|
55
|
+
content_list.append(
|
|
56
|
+
TextContent(
|
|
57
|
+
type="text",
|
|
58
|
+
text=stdout_content,
|
|
59
|
+
description="stdout",
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if stderr_content:
|
|
64
|
+
content_list.append(
|
|
65
|
+
TextContent(
|
|
66
|
+
type="text",
|
|
67
|
+
text=stderr_content,
|
|
68
|
+
description="stderr",
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
content_list.append(
|
|
73
|
+
TextContent(
|
|
74
|
+
type="text",
|
|
75
|
+
text=stdout_content + "\n" + stderr_content,
|
|
76
|
+
description="output",
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
is_error = bool(stderr_content)
|
|
81
|
+
|
|
82
|
+
return CallToolResult(
|
|
83
|
+
content=content_list,
|
|
84
|
+
isError=is_error,
|
|
85
|
+
).model_dump()
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
raise HTTPException(
|
|
89
|
+
status_code=500,
|
|
90
|
+
detail=f"{str(e)}: {traceback.format_exc()}",
|
|
91
|
+
) from e
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@generic_router.post(
|
|
95
|
+
"/tools/run_shell_command",
|
|
96
|
+
summary="Invoke a shell command.",
|
|
97
|
+
)
|
|
98
|
+
async def run_shell_command(
|
|
99
|
+
command: str = Body(
|
|
100
|
+
...,
|
|
101
|
+
example="pwd",
|
|
102
|
+
embed=True,
|
|
103
|
+
),
|
|
104
|
+
):
|
|
105
|
+
"""
|
|
106
|
+
Execute a shell command and return the results.
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
if not command:
|
|
110
|
+
raise HTTPException(status_code=400, detail="Command is required.")
|
|
111
|
+
|
|
112
|
+
result = subprocess.run(
|
|
113
|
+
command,
|
|
114
|
+
shell=True,
|
|
115
|
+
stdout=subprocess.PIPE,
|
|
116
|
+
stderr=subprocess.PIPE,
|
|
117
|
+
text=True,
|
|
118
|
+
check=False,
|
|
119
|
+
)
|
|
120
|
+
stdout_content = result.stdout
|
|
121
|
+
stderr_content = result.stderr
|
|
122
|
+
|
|
123
|
+
content_list = []
|
|
124
|
+
|
|
125
|
+
if SPLIT_OUTPUT_MODE:
|
|
126
|
+
content_list.append(
|
|
127
|
+
TextContent(
|
|
128
|
+
type="text",
|
|
129
|
+
text=stdout_content,
|
|
130
|
+
description="stdout",
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if stderr_content:
|
|
135
|
+
content_list.append(
|
|
136
|
+
TextContent(
|
|
137
|
+
type="text",
|
|
138
|
+
text=stderr_content,
|
|
139
|
+
description="stderr",
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
content_list.append(
|
|
143
|
+
TextContent(
|
|
144
|
+
type="text",
|
|
145
|
+
text=str(result.returncode),
|
|
146
|
+
description="returncode",
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
content_list.append(
|
|
151
|
+
TextContent(
|
|
152
|
+
type="text",
|
|
153
|
+
text=stdout_content
|
|
154
|
+
+ "\n"
|
|
155
|
+
+ stderr_content
|
|
156
|
+
+ "\n"
|
|
157
|
+
+ str(result.returncode),
|
|
158
|
+
description="output",
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
is_error = bool(stderr_content)
|
|
163
|
+
|
|
164
|
+
return CallToolResult(
|
|
165
|
+
content=content_list,
|
|
166
|
+
isError=is_error,
|
|
167
|
+
).model_dump()
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
raise HTTPException(
|
|
171
|
+
status_code=500,
|
|
172
|
+
detail=f"{str(e)}: {traceback.format_exc()}",
|
|
173
|
+
) from e
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import copy
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import traceback
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Body, HTTPException, Response
|
|
9
|
+
|
|
10
|
+
from .mcp_utils import MCPSessionHandler
|
|
11
|
+
|
|
12
|
+
mcp_router = APIRouter()
|
|
13
|
+
|
|
14
|
+
_MCP_SERVERS = {}
|
|
15
|
+
current_directory = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
+
mcp_server_configs_path = os.path.abspath(
|
|
17
|
+
os.path.join(current_directory, "../mcp_server_configs.json"),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(level=logging.INFO)
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# NOTE: DO NOT use API-KEY Server in release version due to security issues
|
|
25
|
+
@mcp_router.post(
|
|
26
|
+
"/mcp/add_servers",
|
|
27
|
+
summary="Add and initialize MCP servers",
|
|
28
|
+
)
|
|
29
|
+
async def add_servers(
|
|
30
|
+
server_configs: dict = Body(
|
|
31
|
+
{},
|
|
32
|
+
embed=True,
|
|
33
|
+
),
|
|
34
|
+
overwrite: bool = Body(
|
|
35
|
+
False,
|
|
36
|
+
embed=True,
|
|
37
|
+
),
|
|
38
|
+
):
|
|
39
|
+
global _MCP_SERVERS
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
if not server_configs:
|
|
43
|
+
raise HTTPException(
|
|
44
|
+
status_code=400,
|
|
45
|
+
detail="server_configs is required.",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
new_servers = [
|
|
49
|
+
MCPSessionHandler(name, config)
|
|
50
|
+
for name, config in server_configs["mcpServers"].items()
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
fail_servers = []
|
|
54
|
+
|
|
55
|
+
# Initialize the servers
|
|
56
|
+
for server in new_servers:
|
|
57
|
+
if server.name in _MCP_SERVERS:
|
|
58
|
+
if not overwrite:
|
|
59
|
+
continue
|
|
60
|
+
# Cleanup old server
|
|
61
|
+
await _MCP_SERVERS.pop(server.name).cleanup()
|
|
62
|
+
try:
|
|
63
|
+
await server.initialize()
|
|
64
|
+
_MCP_SERVERS[server.name] = server
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logging.error(f"Failed to initialize server: {e}")
|
|
67
|
+
fail_servers.append(server)
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if fail_servers:
|
|
71
|
+
for server in fail_servers:
|
|
72
|
+
await server.cleanup()
|
|
73
|
+
raise HTTPException(
|
|
74
|
+
status_code=500,
|
|
75
|
+
detail=f"Failed to initialize server: "
|
|
76
|
+
f"{[server.name for server in fail_servers]}",
|
|
77
|
+
)
|
|
78
|
+
return Response(content="OK", status_code=200)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise HTTPException(
|
|
81
|
+
status_code=500,
|
|
82
|
+
detail=f"{str(e)}: {traceback.format_exc()}",
|
|
83
|
+
) from e
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@mcp_router.get(
|
|
87
|
+
"/mcp/list_tools",
|
|
88
|
+
summary="List MCP tools",
|
|
89
|
+
)
|
|
90
|
+
async def list_tools():
|
|
91
|
+
try:
|
|
92
|
+
mcp_tools = {}
|
|
93
|
+
|
|
94
|
+
for server_name, server in _MCP_SERVERS.items():
|
|
95
|
+
tools = await server.list_tools()
|
|
96
|
+
server_tools = {}
|
|
97
|
+
for tool in tools:
|
|
98
|
+
name = tool.name
|
|
99
|
+
if name in server_tools:
|
|
100
|
+
logging.warning(
|
|
101
|
+
f"Service function `{name}` already exists, "
|
|
102
|
+
f"skip adding it.",
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
json_schema = {
|
|
106
|
+
"type": "function",
|
|
107
|
+
"function": {
|
|
108
|
+
"name": tool.name,
|
|
109
|
+
"description": tool.description,
|
|
110
|
+
"parameters": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"properties": tool.inputSchema.get(
|
|
113
|
+
"properties",
|
|
114
|
+
{},
|
|
115
|
+
),
|
|
116
|
+
"required": tool.inputSchema.get(
|
|
117
|
+
"required",
|
|
118
|
+
[],
|
|
119
|
+
),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
server_tools[tool.name] = {
|
|
124
|
+
"name": tool.name,
|
|
125
|
+
"json_schema": json_schema,
|
|
126
|
+
}
|
|
127
|
+
mcp_tools[server_name] = copy.deepcopy(server_tools)
|
|
128
|
+
return mcp_tools
|
|
129
|
+
except Exception as e:
|
|
130
|
+
raise HTTPException(
|
|
131
|
+
status_code=500,
|
|
132
|
+
detail=f"{str(e)}: {traceback.format_exc()}",
|
|
133
|
+
) from e
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@mcp_router.post(
|
|
137
|
+
"/mcp/call_tool",
|
|
138
|
+
summary="Execute MCP tool",
|
|
139
|
+
)
|
|
140
|
+
async def call_tool(
|
|
141
|
+
tool_name: str = Body(
|
|
142
|
+
...,
|
|
143
|
+
embed=True,
|
|
144
|
+
),
|
|
145
|
+
arguments: dict = Body(
|
|
146
|
+
{},
|
|
147
|
+
embed=True,
|
|
148
|
+
),
|
|
149
|
+
) -> None:
|
|
150
|
+
try:
|
|
151
|
+
if not tool_name:
|
|
152
|
+
raise HTTPException(
|
|
153
|
+
status_code=400,
|
|
154
|
+
detail="tool_name is required.",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
tools = await list_tools()
|
|
158
|
+
for server_name, server_tools in tools.items():
|
|
159
|
+
if tool_name not in server_tools:
|
|
160
|
+
continue
|
|
161
|
+
server = _MCP_SERVERS[server_name]
|
|
162
|
+
result = await server.call_tool(tool_name, arguments)
|
|
163
|
+
return result.model_dump()
|
|
164
|
+
raise ModuleNotFoundError(f"Tool '{tool_name}' not found.")
|
|
165
|
+
except Exception as e:
|
|
166
|
+
raise HTTPException(
|
|
167
|
+
status_code=500,
|
|
168
|
+
detail=f"{str(e)}: {traceback.format_exc()}",
|
|
169
|
+
) from e
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@mcp_router.on_event("shutdown")
|
|
173
|
+
async def cleanup_servers() -> None:
|
|
174
|
+
"""Clean up all servers properly."""
|
|
175
|
+
global _MCP_SERVERS
|
|
176
|
+
|
|
177
|
+
for server in reversed(list(_MCP_SERVERS.values())):
|
|
178
|
+
try:
|
|
179
|
+
await server.cleanup()
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logging.error(f"Failed to cleanup server: {e}")
|
|
182
|
+
|
|
183
|
+
_MCP_SERVERS = {}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@mcp_router.on_event("startup")
|
|
187
|
+
async def startup_event():
|
|
188
|
+
# Load MCP server configs
|
|
189
|
+
try:
|
|
190
|
+
with open(mcp_server_configs_path, "r", encoding="utf-8") as file:
|
|
191
|
+
mcp_server_configs = json.load(file)
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"Failed to load MCP server configs: {e}")
|
|
195
|
+
mcp_server_configs = {}
|
|
196
|
+
|
|
197
|
+
# Call the add_servers function
|
|
198
|
+
if mcp_server_configs:
|
|
199
|
+
try:
|
|
200
|
+
await add_servers(
|
|
201
|
+
server_configs=mcp_server_configs,
|
|
202
|
+
overwrite=False,
|
|
203
|
+
)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.error(
|
|
206
|
+
f"Failed to add MCP servers: {e}, {traceback.format_exc()}",
|
|
207
|
+
)
|