alita-sdk 0.3.365__py3-none-any.whl → 0.3.462__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.
Potentially problematic release.
This version of alita-sdk might be problematic. Click here for more details.
- alita_sdk/cli/__init__.py +10 -0
- alita_sdk/cli/__main__.py +17 -0
- alita_sdk/cli/agent_executor.py +144 -0
- alita_sdk/cli/agent_loader.py +197 -0
- alita_sdk/cli/agent_ui.py +166 -0
- alita_sdk/cli/agents.py +1069 -0
- alita_sdk/cli/callbacks.py +576 -0
- alita_sdk/cli/cli.py +159 -0
- alita_sdk/cli/config.py +153 -0
- alita_sdk/cli/formatting.py +182 -0
- alita_sdk/cli/mcp_loader.py +315 -0
- alita_sdk/cli/toolkit.py +330 -0
- alita_sdk/cli/toolkit_loader.py +55 -0
- alita_sdk/cli/tools/__init__.py +9 -0
- alita_sdk/cli/tools/filesystem.py +905 -0
- alita_sdk/configurations/bitbucket.py +95 -0
- alita_sdk/configurations/confluence.py +96 -1
- alita_sdk/configurations/gitlab.py +79 -0
- alita_sdk/configurations/jira.py +103 -0
- alita_sdk/configurations/testrail.py +88 -0
- alita_sdk/configurations/xray.py +93 -0
- alita_sdk/configurations/zephyr_enterprise.py +93 -0
- alita_sdk/configurations/zephyr_essential.py +75 -0
- alita_sdk/runtime/clients/artifact.py +1 -1
- alita_sdk/runtime/clients/client.py +47 -10
- alita_sdk/runtime/clients/mcp_discovery.py +342 -0
- alita_sdk/runtime/clients/mcp_manager.py +262 -0
- alita_sdk/runtime/clients/sandbox_client.py +373 -0
- alita_sdk/runtime/langchain/assistant.py +70 -41
- alita_sdk/runtime/langchain/constants.py +6 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -1
- alita_sdk/runtime/langchain/document_loaders/constants.py +73 -100
- alita_sdk/runtime/langchain/langraph_agent.py +164 -38
- alita_sdk/runtime/langchain/utils.py +43 -7
- alita_sdk/runtime/models/mcp_models.py +61 -0
- alita_sdk/runtime/toolkits/__init__.py +24 -0
- alita_sdk/runtime/toolkits/application.py +8 -1
- alita_sdk/runtime/toolkits/artifact.py +5 -6
- alita_sdk/runtime/toolkits/mcp.py +895 -0
- alita_sdk/runtime/toolkits/tools.py +140 -50
- alita_sdk/runtime/tools/__init__.py +7 -2
- alita_sdk/runtime/tools/application.py +7 -0
- alita_sdk/runtime/tools/function.py +94 -5
- alita_sdk/runtime/tools/graph.py +10 -4
- alita_sdk/runtime/tools/image_generation.py +104 -8
- alita_sdk/runtime/tools/llm.py +204 -114
- alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
- alita_sdk/runtime/tools/mcp_remote_tool.py +166 -0
- alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
- alita_sdk/runtime/tools/sandbox.py +180 -79
- alita_sdk/runtime/tools/vectorstore.py +22 -21
- alita_sdk/runtime/tools/vectorstore_base.py +79 -26
- alita_sdk/runtime/utils/mcp_oauth.py +164 -0
- alita_sdk/runtime/utils/mcp_sse_client.py +405 -0
- alita_sdk/runtime/utils/streamlit.py +34 -3
- alita_sdk/runtime/utils/toolkit_utils.py +14 -4
- alita_sdk/runtime/utils/utils.py +1 -0
- alita_sdk/tools/__init__.py +48 -31
- alita_sdk/tools/ado/repos/__init__.py +1 -0
- alita_sdk/tools/ado/test_plan/__init__.py +1 -1
- alita_sdk/tools/ado/wiki/__init__.py +1 -5
- alita_sdk/tools/ado/work_item/__init__.py +1 -5
- alita_sdk/tools/ado/work_item/ado_wrapper.py +17 -8
- alita_sdk/tools/base_indexer_toolkit.py +194 -112
- alita_sdk/tools/bitbucket/__init__.py +1 -0
- alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
- alita_sdk/tools/code/sonar/__init__.py +1 -1
- alita_sdk/tools/code_indexer_toolkit.py +15 -5
- alita_sdk/tools/confluence/__init__.py +2 -2
- alita_sdk/tools/confluence/api_wrapper.py +110 -63
- alita_sdk/tools/confluence/loader.py +10 -0
- alita_sdk/tools/elitea_base.py +22 -22
- alita_sdk/tools/github/__init__.py +2 -2
- alita_sdk/tools/gitlab/__init__.py +2 -1
- alita_sdk/tools/gitlab/api_wrapper.py +11 -7
- alita_sdk/tools/gitlab_org/__init__.py +1 -2
- alita_sdk/tools/google_places/__init__.py +2 -1
- alita_sdk/tools/jira/__init__.py +1 -0
- alita_sdk/tools/jira/api_wrapper.py +1 -1
- alita_sdk/tools/memory/__init__.py +1 -1
- alita_sdk/tools/non_code_indexer_toolkit.py +2 -2
- alita_sdk/tools/openapi/__init__.py +10 -1
- alita_sdk/tools/pandas/__init__.py +1 -1
- alita_sdk/tools/postman/__init__.py +2 -1
- alita_sdk/tools/postman/api_wrapper.py +18 -8
- alita_sdk/tools/postman/postman_analysis.py +8 -1
- alita_sdk/tools/pptx/__init__.py +2 -2
- alita_sdk/tools/qtest/__init__.py +3 -3
- alita_sdk/tools/qtest/api_wrapper.py +1708 -76
- alita_sdk/tools/rally/__init__.py +1 -2
- alita_sdk/tools/report_portal/__init__.py +1 -0
- alita_sdk/tools/salesforce/__init__.py +1 -0
- alita_sdk/tools/servicenow/__init__.py +2 -3
- alita_sdk/tools/sharepoint/__init__.py +1 -0
- alita_sdk/tools/sharepoint/api_wrapper.py +125 -34
- alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
- alita_sdk/tools/sharepoint/utils.py +8 -2
- alita_sdk/tools/slack/__init__.py +1 -0
- alita_sdk/tools/sql/__init__.py +2 -1
- alita_sdk/tools/sql/api_wrapper.py +71 -23
- alita_sdk/tools/testio/__init__.py +1 -0
- alita_sdk/tools/testrail/__init__.py +1 -3
- alita_sdk/tools/utils/__init__.py +17 -0
- alita_sdk/tools/utils/content_parser.py +35 -24
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +67 -21
- alita_sdk/tools/xray/__init__.py +2 -1
- alita_sdk/tools/zephyr/__init__.py +2 -1
- alita_sdk/tools/zephyr_enterprise/__init__.py +1 -0
- alita_sdk/tools/zephyr_essential/__init__.py +1 -0
- alita_sdk/tools/zephyr_scale/__init__.py +1 -0
- alita_sdk/tools/zephyr_squad/__init__.py +1 -0
- {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/METADATA +8 -2
- {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/RECORD +118 -93
- alita_sdk-0.3.462.dist-info/entry_points.txt +2 -0
- {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Remote Tool for direct HTTP/SSE invocation.
|
|
3
|
+
This tool is used for remote MCP servers accessed via HTTP/SSE.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from .mcp_server_tool import McpServerTool
|
|
15
|
+
from pydantic import Field
|
|
16
|
+
from ..utils.mcp_oauth import (
|
|
17
|
+
McpAuthorizationRequired,
|
|
18
|
+
canonical_resource,
|
|
19
|
+
extract_resource_metadata_url,
|
|
20
|
+
fetch_resource_metadata_async,
|
|
21
|
+
infer_authorization_servers_from_realm,
|
|
22
|
+
)
|
|
23
|
+
from ..utils.mcp_sse_client import McpSseClient
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class McpRemoteTool(McpServerTool):
|
|
29
|
+
"""
|
|
30
|
+
Tool for invoking remote MCP server tools via HTTP/SSE.
|
|
31
|
+
Extends McpServerTool and overrides _run to use direct HTTP calls instead of client.mcp_tool_call.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Remote MCP connection details
|
|
35
|
+
server_url: str = Field(..., description="URL of the remote MCP server")
|
|
36
|
+
server_headers: Optional[Dict[str, str]] = Field(default=None, description="HTTP headers for authentication")
|
|
37
|
+
original_tool_name: Optional[str] = Field(default=None, description="Original tool name from MCP server (before optimization)")
|
|
38
|
+
is_prompt: bool = False # Flag to indicate if this is a prompt tool
|
|
39
|
+
prompt_name: Optional[str] = None # Original prompt name if this is a prompt
|
|
40
|
+
session_id: Optional[str] = Field(default=None, description="MCP session ID for stateful SSE servers")
|
|
41
|
+
|
|
42
|
+
def model_post_init(self, __context: Any) -> None:
|
|
43
|
+
"""Update metadata with session info after model initialization."""
|
|
44
|
+
super().model_post_init(__context)
|
|
45
|
+
self._update_metadata_with_session()
|
|
46
|
+
|
|
47
|
+
def _update_metadata_with_session(self):
|
|
48
|
+
"""Update the metadata dict with current session information."""
|
|
49
|
+
if self.session_id:
|
|
50
|
+
if self.metadata is None:
|
|
51
|
+
self.metadata = {}
|
|
52
|
+
self.metadata.update({
|
|
53
|
+
'mcp_session_id': self.session_id,
|
|
54
|
+
'mcp_server_url': canonical_resource(self.server_url)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
def __getstate__(self):
|
|
58
|
+
"""Custom serialization for pickle compatibility."""
|
|
59
|
+
state = super().__getstate__()
|
|
60
|
+
# Ensure headers are serializable
|
|
61
|
+
if 'server_headers' in state and state['server_headers'] is not None:
|
|
62
|
+
state['server_headers'] = dict(state['server_headers'])
|
|
63
|
+
return state
|
|
64
|
+
|
|
65
|
+
def _run(self, *args, **kwargs):
|
|
66
|
+
"""
|
|
67
|
+
Execute the MCP tool via direct HTTP/SSE call to the remote server.
|
|
68
|
+
Overrides the parent method to avoid using client.mcp_tool_call.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
# Always create a new event loop for sync context
|
|
72
|
+
with ThreadPoolExecutor() as executor:
|
|
73
|
+
future = executor.submit(self._run_in_new_loop, kwargs)
|
|
74
|
+
return future.result(timeout=self.tool_timeout_sec)
|
|
75
|
+
except McpAuthorizationRequired:
|
|
76
|
+
# Bubble up so LangChain can surface a tool error with useful metadata
|
|
77
|
+
raise
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Error executing remote MCP tool '{self.name}': {e}")
|
|
80
|
+
return f"Error executing tool: {e}"
|
|
81
|
+
|
|
82
|
+
def _run_in_new_loop(self, kwargs: Dict[str, Any]) -> str:
|
|
83
|
+
"""Run the async tool invocation in a new event loop."""
|
|
84
|
+
return asyncio.run(self._execute_remote_tool(kwargs))
|
|
85
|
+
|
|
86
|
+
async def _execute_remote_tool(self, kwargs: Dict[str, Any]) -> str:
|
|
87
|
+
"""Execute the actual remote MCP tool call using SSE client."""
|
|
88
|
+
from ...tools.utils import TOOLKIT_SPLITTER
|
|
89
|
+
|
|
90
|
+
# Check for session_id requirement
|
|
91
|
+
if not self.session_id:
|
|
92
|
+
logger.error(f"[MCP Session] Missing session_id for tool '{self.name}'")
|
|
93
|
+
raise Exception("sessionId required. Frontend must generate UUID and send with mcp_tokens.")
|
|
94
|
+
|
|
95
|
+
# Use the original tool name from discovery for MCP server invocation
|
|
96
|
+
tool_name_for_server = self.original_tool_name
|
|
97
|
+
if not tool_name_for_server:
|
|
98
|
+
tool_name_for_server = self.name.rsplit(TOOLKIT_SPLITTER, 1)[-1] if TOOLKIT_SPLITTER in self.name else self.name
|
|
99
|
+
logger.warning(f"original_tool_name not set for '{self.name}', using extracted: {tool_name_for_server}")
|
|
100
|
+
|
|
101
|
+
logger.info(f"[MCP SSE] Executing tool '{tool_name_for_server}' with session {self.session_id}")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# Prepare headers
|
|
105
|
+
headers = {}
|
|
106
|
+
if self.server_headers:
|
|
107
|
+
headers.update(self.server_headers)
|
|
108
|
+
|
|
109
|
+
# Create SSE client
|
|
110
|
+
client = McpSseClient(
|
|
111
|
+
url=self.server_url,
|
|
112
|
+
session_id=self.session_id,
|
|
113
|
+
headers=headers,
|
|
114
|
+
timeout=self.tool_timeout_sec
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Execute tool call via SSE
|
|
118
|
+
result = await client.call_tool(tool_name_for_server, kwargs)
|
|
119
|
+
|
|
120
|
+
# Format the result
|
|
121
|
+
if isinstance(result, dict):
|
|
122
|
+
# Check for content array (common in MCP responses)
|
|
123
|
+
if "content" in result:
|
|
124
|
+
content_items = result["content"]
|
|
125
|
+
if isinstance(content_items, list):
|
|
126
|
+
# Extract text from content items
|
|
127
|
+
text_parts = []
|
|
128
|
+
for item in content_items:
|
|
129
|
+
if isinstance(item, dict):
|
|
130
|
+
if item.get("type") == "text" and "text" in item:
|
|
131
|
+
text_parts.append(item["text"])
|
|
132
|
+
elif "text" in item:
|
|
133
|
+
text_parts.append(item["text"])
|
|
134
|
+
else:
|
|
135
|
+
text_parts.append(json.dumps(item))
|
|
136
|
+
else:
|
|
137
|
+
text_parts.append(str(item))
|
|
138
|
+
return "\n".join(text_parts)
|
|
139
|
+
|
|
140
|
+
# Return formatted JSON if no content field
|
|
141
|
+
return json.dumps(result, indent=2)
|
|
142
|
+
|
|
143
|
+
# Return as string for other types
|
|
144
|
+
return str(result)
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"[MCP SSE] Tool execution failed: {e}", exc_info=True)
|
|
148
|
+
raise
|
|
149
|
+
|
|
150
|
+
def _parse_sse(self, text: str) -> Dict[str, Any]:
|
|
151
|
+
"""Parse Server-Sent Events (SSE) format response."""
|
|
152
|
+
for line in text.split('\n'):
|
|
153
|
+
line = line.strip()
|
|
154
|
+
if line.startswith('data:'):
|
|
155
|
+
json_str = line[5:].strip()
|
|
156
|
+
return json.loads(json_str)
|
|
157
|
+
raise ValueError("No data found in SSE response")
|
|
158
|
+
|
|
159
|
+
def get_session_metadata(self) -> dict:
|
|
160
|
+
"""Return session metadata to be included in tool responses."""
|
|
161
|
+
if self.session_id:
|
|
162
|
+
return {
|
|
163
|
+
'mcp_session_id': self.session_id,
|
|
164
|
+
'mcp_server_url': canonical_resource(self.server_url)
|
|
165
|
+
}
|
|
166
|
+
return {}
|
|
@@ -3,7 +3,7 @@ from logging import getLogger
|
|
|
3
3
|
from typing import Any, Type, Literal, Optional, Union, List
|
|
4
4
|
|
|
5
5
|
from langchain_core.tools import BaseTool
|
|
6
|
-
from pydantic import BaseModel, Field, create_model, EmailStr, constr
|
|
6
|
+
from pydantic import BaseModel, Field, create_model, EmailStr, constr, ConfigDict
|
|
7
7
|
|
|
8
8
|
from ...tools.utils import TOOLKIT_SPLITTER
|
|
9
9
|
|
|
@@ -19,6 +19,7 @@ class McpServerTool(BaseTool):
|
|
|
19
19
|
server: str
|
|
20
20
|
tool_timeout_sec: int = 60
|
|
21
21
|
|
|
22
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
22
23
|
|
|
23
24
|
@staticmethod
|
|
24
25
|
def create_pydantic_model_from_schema(schema: dict, model_name: str = "ArgsSchema"):
|
|
@@ -90,6 +91,7 @@ class McpServerTool(BaseTool):
|
|
|
90
91
|
return create_model(model_name, **fields)
|
|
91
92
|
|
|
92
93
|
def _run(self, *args, **kwargs):
|
|
94
|
+
# Extract the actual tool/prompt name (remove toolkit prefix)
|
|
93
95
|
call_data = {
|
|
94
96
|
"server": self.server,
|
|
95
97
|
"tool_timeout_sec": self.tool_timeout_sec,
|
|
@@ -1,22 +1,61 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
import asyncio
|
|
2
|
+
import logging
|
|
3
3
|
import subprocess
|
|
4
4
|
import os
|
|
5
|
-
from typing import Any, Type, Optional,
|
|
6
|
-
from
|
|
7
|
-
from
|
|
5
|
+
from typing import Any, Type, Optional, Dict, List, Literal, Union
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from langchain_core.tools import BaseTool, BaseToolkit
|
|
10
|
+
from langchain_core.messages import ToolCall
|
|
11
|
+
from pydantic import BaseModel, create_model, ConfigDict, Field
|
|
8
12
|
from pydantic.fields import FieldInfo
|
|
9
13
|
|
|
10
14
|
logger = logging.getLogger(__name__)
|
|
11
15
|
|
|
16
|
+
name = "pyodide"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_tools(tools_list: list, alita_client=None, llm=None, memory_store=None):
|
|
20
|
+
"""
|
|
21
|
+
Get sandbox tools for the provided tool configurations.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
tools_list: List of tool configurations
|
|
25
|
+
alita_client: Alita client instance for sandbox tools
|
|
26
|
+
llm: LLM client instance (unused for sandbox)
|
|
27
|
+
memory_store: Optional memory store instance (unused for sandbox)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of sandbox tools
|
|
31
|
+
"""
|
|
32
|
+
all_tools = []
|
|
33
|
+
|
|
34
|
+
for tool in tools_list:
|
|
35
|
+
if tool.get('type') == 'sandbox' or tool.get('toolkit_name') == 'sandbox':
|
|
36
|
+
try:
|
|
37
|
+
toolkit_instance = SandboxToolkit.get_toolkit(
|
|
38
|
+
stateful=tool['settings'].get('stateful', False),
|
|
39
|
+
allow_net=tool['settings'].get('allow_net', True),
|
|
40
|
+
alita_client=alita_client,
|
|
41
|
+
toolkit_name=tool.get('toolkit_name', '')
|
|
42
|
+
)
|
|
43
|
+
all_tools.extend(toolkit_instance.get_tools())
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logger.error(f"Error in sandbox toolkit get_tools: {e}")
|
|
46
|
+
logger.error(f"Tool config: {tool}")
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
return all_tools
|
|
50
|
+
|
|
12
51
|
|
|
13
52
|
def _is_deno_available() -> bool:
|
|
14
53
|
"""Check if Deno is available in the PATH"""
|
|
15
54
|
try:
|
|
16
55
|
result = subprocess.run(
|
|
17
|
-
["deno", "--version"],
|
|
18
|
-
capture_output=True,
|
|
19
|
-
text=True,
|
|
56
|
+
["deno", "--version"],
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
20
59
|
timeout=10
|
|
21
60
|
)
|
|
22
61
|
return result.returncode == 0
|
|
@@ -25,43 +64,17 @@ def _is_deno_available() -> bool:
|
|
|
25
64
|
|
|
26
65
|
|
|
27
66
|
def _setup_pyodide_cache_env() -> None:
|
|
28
|
-
"""Setup Pyodide caching environment variables for performance optimization"""
|
|
67
|
+
"""Setup Pyodide caching environment variables for performance optimization [NO-OP]"""
|
|
29
68
|
try:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if os.path.exists(cache_env_file):
|
|
33
|
-
with open(cache_env_file, 'r') as f:
|
|
34
|
-
for line in f:
|
|
35
|
-
line = line.strip()
|
|
36
|
-
if line.startswith('export ') and '=' in line:
|
|
37
|
-
# Parse export VAR=value format
|
|
38
|
-
var_assignment = line[7:] # Remove 'export '
|
|
39
|
-
if '=' in var_assignment:
|
|
40
|
-
key, value = var_assignment.split('=', 1)
|
|
41
|
-
# Remove quotes if present
|
|
42
|
-
value = value.strip('"').strip("'")
|
|
43
|
-
os.environ[key] = value
|
|
44
|
-
logger.debug(f"Set Pyodide cache env: {key}={value}")
|
|
45
|
-
|
|
46
|
-
# Set default caching environment variables if not already set
|
|
47
|
-
cache_defaults = {
|
|
48
|
-
'PYODIDE_PACKAGES_PATH': os.path.expanduser('~/.cache/pyodide'),
|
|
49
|
-
'DENO_DIR': os.path.expanduser('~/.cache/deno'),
|
|
50
|
-
'PYODIDE_CACHE_DIR': os.path.expanduser('~/.cache/pyodide'),
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
for key, default_value in cache_defaults.items():
|
|
54
|
-
if key not in os.environ:
|
|
55
|
-
os.environ[key] = default_value
|
|
56
|
-
logger.debug(f"Set default Pyodide env: {key}={default_value}")
|
|
57
|
-
|
|
69
|
+
for key in ["SANDBOX_BASE", "DENO_DIR"]:
|
|
70
|
+
logger.info("Sandbox env: %s -> %s", key, os.environ.get(key, "n/a"))
|
|
58
71
|
except Exception as e:
|
|
59
72
|
logger.warning(f"Could not setup Pyodide cache environment: {e}")
|
|
60
73
|
|
|
61
74
|
|
|
62
75
|
# Create input schema for the sandbox tool
|
|
63
76
|
sandbox_tool_input = create_model(
|
|
64
|
-
"SandboxToolInput",
|
|
77
|
+
"SandboxToolInput",
|
|
65
78
|
code=(str, FieldInfo(description="Python code to execute in the sandbox environment"))
|
|
66
79
|
)
|
|
67
80
|
|
|
@@ -72,7 +85,7 @@ class PyodideSandboxTool(BaseTool):
|
|
|
72
85
|
This tool leverages langchain-sandbox to provide a safe environment for running untrusted Python code.
|
|
73
86
|
Optimized for performance with caching and stateless execution by default.
|
|
74
87
|
"""
|
|
75
|
-
|
|
88
|
+
|
|
76
89
|
name: str = "pyodide_sandbox"
|
|
77
90
|
description: str = """Execute Python code in a secure sandbox environment using Pyodide.
|
|
78
91
|
This tool allows safe execution of Python code without access to the host system.
|
|
@@ -81,7 +94,7 @@ class PyodideSandboxTool(BaseTool):
|
|
|
81
94
|
- Perform calculations or data analysis
|
|
82
95
|
- Test Python algorithms
|
|
83
96
|
- Run code that requires isolation from the host system
|
|
84
|
-
|
|
97
|
+
|
|
85
98
|
The sandbox supports most Python standard library modules and can install additional packages.
|
|
86
99
|
Note: File access and some system operations are restricted for security.
|
|
87
100
|
Optimized for performance with local caching (stateless by default for faster execution).
|
|
@@ -91,14 +104,37 @@ class PyodideSandboxTool(BaseTool):
|
|
|
91
104
|
allow_net: bool = True
|
|
92
105
|
session_bytes: Optional[bytes] = None
|
|
93
106
|
session_metadata: Optional[Dict] = None
|
|
94
|
-
|
|
107
|
+
alita_client: Optional[Any] = None
|
|
108
|
+
|
|
95
109
|
def __init__(self, **kwargs: Any) -> None:
|
|
96
110
|
super().__init__(**kwargs)
|
|
97
111
|
self._sandbox = None
|
|
98
112
|
# Setup caching environment for optimal performance
|
|
99
113
|
_setup_pyodide_cache_env()
|
|
100
114
|
self._initialize_sandbox()
|
|
101
|
-
|
|
115
|
+
|
|
116
|
+
def _prepare_pyodide_input(self, code: str) -> str:
|
|
117
|
+
"""Prepare input for PyodideSandboxTool by injecting state and alita_client into the code block."""
|
|
118
|
+
pyodide_predata = ""
|
|
119
|
+
|
|
120
|
+
# Add alita_client if available
|
|
121
|
+
if self.alita_client:
|
|
122
|
+
try:
|
|
123
|
+
# Get the directory of the current file and construct the path to sandbox_client.py
|
|
124
|
+
current_dir = Path(__file__).parent
|
|
125
|
+
sandbox_client_path = current_dir.parent / 'clients' / 'sandbox_client.py'
|
|
126
|
+
|
|
127
|
+
with open(sandbox_client_path, 'r') as f:
|
|
128
|
+
sandbox_client_code = f.read()
|
|
129
|
+
pyodide_predata += f"{sandbox_client_code}\n"
|
|
130
|
+
pyodide_predata += (f"alita_client = SandboxClient(base_url='{self.alita_client.base_url}',"
|
|
131
|
+
f"project_id={self.alita_client.project_id},"
|
|
132
|
+
f"auth_token='{self.alita_client.auth_token}')\n")
|
|
133
|
+
except FileNotFoundError:
|
|
134
|
+
logger.error(f"sandbox_client.py not found. Ensure the file exists.")
|
|
135
|
+
|
|
136
|
+
return f"#elitea simplified client\n{pyodide_predata}{code}"
|
|
137
|
+
|
|
102
138
|
def _initialize_sandbox(self) -> None:
|
|
103
139
|
"""Initialize the PyodideSandbox instance with optimized settings"""
|
|
104
140
|
try:
|
|
@@ -110,12 +146,22 @@ class PyodideSandboxTool(BaseTool):
|
|
|
110
146
|
)
|
|
111
147
|
logger.error(error_msg)
|
|
112
148
|
raise RuntimeError(error_msg)
|
|
113
|
-
|
|
149
|
+
|
|
114
150
|
from langchain_sandbox import PyodideSandbox
|
|
115
|
-
|
|
151
|
+
|
|
152
|
+
# Air-gapped settings
|
|
153
|
+
sandbox_base = os.environ.get("SANDBOX_BASE", os.path.expanduser('~/.cache/pyodide'))
|
|
154
|
+
sandbox_tmp = os.path.join(sandbox_base, "tmp")
|
|
155
|
+
deno_cache = os.environ.get("DENO_DIR", os.path.expanduser('~/.cache/deno'))
|
|
156
|
+
|
|
116
157
|
# Configure sandbox with performance optimizations
|
|
117
158
|
self._sandbox = PyodideSandbox(
|
|
118
159
|
stateful=self.stateful,
|
|
160
|
+
#
|
|
161
|
+
allow_env=["SANDBOX_BASE"],
|
|
162
|
+
allow_read=[sandbox_base, sandbox_tmp, deno_cache],
|
|
163
|
+
allow_write=[sandbox_tmp, deno_cache],
|
|
164
|
+
#
|
|
119
165
|
allow_net=self.allow_net,
|
|
120
166
|
# Use auto node_modules_dir for better caching
|
|
121
167
|
node_modules_dir="auto"
|
|
@@ -135,7 +181,7 @@ class PyodideSandboxTool(BaseTool):
|
|
|
135
181
|
except Exception as e:
|
|
136
182
|
logger.error(f"Failed to initialize PyodideSandbox: {e}")
|
|
137
183
|
raise
|
|
138
|
-
|
|
184
|
+
|
|
139
185
|
def _run(self, code: str) -> str:
|
|
140
186
|
"""
|
|
141
187
|
Synchronous version - runs the async method in a new event loop
|
|
@@ -144,7 +190,10 @@ class PyodideSandboxTool(BaseTool):
|
|
|
144
190
|
# Check if sandbox is initialized, if not try to initialize
|
|
145
191
|
if self._sandbox is None:
|
|
146
192
|
self._initialize_sandbox()
|
|
147
|
-
|
|
193
|
+
|
|
194
|
+
# Prepare code with state and client injection
|
|
195
|
+
prepared_code = self._prepare_pyodide_input(code)
|
|
196
|
+
|
|
148
197
|
# Check if we're already in an async context
|
|
149
198
|
try:
|
|
150
199
|
loop = asyncio.get_running_loop()
|
|
@@ -152,11 +201,11 @@ class PyodideSandboxTool(BaseTool):
|
|
|
152
201
|
# We'll need to use a different approach
|
|
153
202
|
import concurrent.futures
|
|
154
203
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
155
|
-
future = executor.submit(asyncio.run, self._arun(
|
|
204
|
+
future = executor.submit(asyncio.run, self._arun(prepared_code))
|
|
156
205
|
return future.result()
|
|
157
206
|
except RuntimeError:
|
|
158
207
|
# No running loop, safe to use asyncio.run
|
|
159
|
-
return asyncio.run(self._arun(
|
|
208
|
+
return asyncio.run(self._arun(prepared_code))
|
|
160
209
|
except (ImportError, RuntimeError) as e:
|
|
161
210
|
# Handle specific dependency errors gracefully
|
|
162
211
|
error_msg = str(e)
|
|
@@ -169,7 +218,7 @@ class PyodideSandboxTool(BaseTool):
|
|
|
169
218
|
except Exception as e:
|
|
170
219
|
logger.error(f"Error executing code in sandbox: {e}")
|
|
171
220
|
return f"Error executing code: {str(e)}"
|
|
172
|
-
|
|
221
|
+
|
|
173
222
|
async def _arun(self, code: str) -> str:
|
|
174
223
|
"""
|
|
175
224
|
Execute Python code in the Pyodide sandbox
|
|
@@ -177,47 +226,45 @@ class PyodideSandboxTool(BaseTool):
|
|
|
177
226
|
try:
|
|
178
227
|
if self._sandbox is None:
|
|
179
228
|
self._initialize_sandbox()
|
|
180
|
-
|
|
229
|
+
|
|
181
230
|
# Execute the code with session state if available
|
|
182
231
|
result = await self._sandbox.execute(
|
|
183
232
|
code,
|
|
184
233
|
session_bytes=self.session_bytes,
|
|
185
234
|
session_metadata=self.session_metadata
|
|
186
235
|
)
|
|
187
|
-
|
|
236
|
+
|
|
188
237
|
# Update session state for stateful execution
|
|
189
238
|
if self.stateful:
|
|
190
239
|
self.session_bytes = result.session_bytes
|
|
191
240
|
self.session_metadata = result.session_metadata
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
241
|
+
|
|
242
|
+
result_dict = {}
|
|
243
|
+
|
|
196
244
|
if result.result is not None:
|
|
197
|
-
|
|
198
|
-
|
|
245
|
+
result_dict["result"] = result.result
|
|
246
|
+
|
|
199
247
|
if result.stdout:
|
|
200
|
-
|
|
201
|
-
|
|
248
|
+
result_dict["output"] = result.stdout
|
|
249
|
+
|
|
202
250
|
if result.stderr:
|
|
203
|
-
|
|
204
|
-
|
|
251
|
+
result_dict["error"] = result.stderr
|
|
252
|
+
|
|
205
253
|
if result.status == 'error':
|
|
206
|
-
|
|
207
|
-
|
|
254
|
+
result_dict["status"] = "Execution failed"
|
|
255
|
+
|
|
208
256
|
execution_info = f"Execution time: {result.execution_time:.2f}s"
|
|
209
257
|
if result.session_metadata and 'packages' in result.session_metadata:
|
|
210
258
|
packages = result.session_metadata.get('packages', [])
|
|
211
259
|
if packages:
|
|
212
260
|
execution_info += f", Packages: {', '.join(packages)}"
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
261
|
+
|
|
262
|
+
result_dict["execution_info"] = execution_info
|
|
263
|
+
return result_dict
|
|
264
|
+
|
|
218
265
|
except Exception as e:
|
|
219
266
|
logger.error(f"Error executing code in sandbox: {e}")
|
|
220
|
-
return f"Error executing code: {str(e)}"
|
|
267
|
+
return {"error": f"Error executing code: {str(e)}"}
|
|
221
268
|
|
|
222
269
|
|
|
223
270
|
class StatefulPyodideSandboxTool(PyodideSandboxTool):
|
|
@@ -225,7 +272,7 @@ class StatefulPyodideSandboxTool(PyodideSandboxTool):
|
|
|
225
272
|
A stateful version of the PyodideSandboxTool that maintains state between executions.
|
|
226
273
|
This version preserves variables, imports, and function definitions across multiple tool calls.
|
|
227
274
|
"""
|
|
228
|
-
|
|
275
|
+
|
|
229
276
|
name: str = "stateful_pyodide_sandbox"
|
|
230
277
|
description: str = """Execute Python code in a stateful sandbox environment using Pyodide.
|
|
231
278
|
This tool maintains state between executions, preserving variables, imports, and function definitions.
|
|
@@ -234,41 +281,95 @@ class StatefulPyodideSandboxTool(PyodideSandboxTool):
|
|
|
234
281
|
- Maintain variables across multiple calls
|
|
235
282
|
- Develop complex programs step by step
|
|
236
283
|
- Preserve imported libraries and defined functions
|
|
237
|
-
|
|
284
|
+
|
|
238
285
|
The sandbox supports most Python standard library modules and can install additional packages.
|
|
239
286
|
Note: File access and some system operations are restricted for security.
|
|
240
287
|
"""
|
|
241
|
-
|
|
288
|
+
|
|
242
289
|
def __init__(self, **kwargs: Any) -> None:
|
|
243
290
|
kwargs['stateful'] = True # Force stateful mode
|
|
244
291
|
super().__init__(**kwargs)
|
|
245
292
|
|
|
246
293
|
|
|
247
294
|
# Factory function for creating sandbox tools
|
|
248
|
-
def create_sandbox_tool(stateful: bool = False, allow_net: bool = True) -> BaseTool:
|
|
295
|
+
def create_sandbox_tool(stateful: bool = False, allow_net: bool = True, alita_client: Optional[Any] = None) -> BaseTool:
|
|
249
296
|
"""
|
|
250
297
|
Factory function to create sandbox tools with specified configuration.
|
|
251
|
-
|
|
298
|
+
|
|
252
299
|
Note: This tool requires Deno to be installed and available in PATH.
|
|
253
300
|
For installation and optimization, run the bootstrap.sh script.
|
|
254
|
-
|
|
301
|
+
|
|
255
302
|
Args:
|
|
256
303
|
stateful: Whether to maintain state between executions (default: False for better performance)
|
|
257
304
|
allow_net: Whether to allow network access (for package installation)
|
|
258
|
-
|
|
305
|
+
|
|
259
306
|
Returns:
|
|
260
307
|
Configured sandbox tool instance
|
|
261
|
-
|
|
308
|
+
|
|
262
309
|
Raises:
|
|
263
310
|
ImportError: If langchain-sandbox is not installed
|
|
264
311
|
RuntimeError: If Deno is not found in PATH
|
|
265
|
-
|
|
312
|
+
|
|
266
313
|
Performance Notes:
|
|
267
314
|
- Stateless mode (default) is faster and avoids session state overhead
|
|
268
315
|
- Run bootstrap.sh script to enable local caching and reduce initialization time
|
|
269
316
|
- Cached wheels reduce package download time from ~4.76s to near-instant
|
|
270
317
|
"""
|
|
271
318
|
if stateful:
|
|
272
|
-
return StatefulPyodideSandboxTool(allow_net=allow_net)
|
|
319
|
+
return StatefulPyodideSandboxTool(allow_net=allow_net, alita_client=alita_client)
|
|
273
320
|
else:
|
|
274
|
-
return PyodideSandboxTool(stateful=False, allow_net=allow_net)
|
|
321
|
+
return PyodideSandboxTool(stateful=False, allow_net=allow_net, alita_client=alita_client)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class SandboxToolkit(BaseToolkit):
|
|
325
|
+
tools: List[BaseTool] = []
|
|
326
|
+
|
|
327
|
+
@staticmethod
|
|
328
|
+
def toolkit_config_schema() -> Type[BaseModel]:
|
|
329
|
+
# Create sample tools to get their schemas
|
|
330
|
+
sample_tools = [
|
|
331
|
+
PyodideSandboxTool(),
|
|
332
|
+
StatefulPyodideSandboxTool()
|
|
333
|
+
]
|
|
334
|
+
selected_tools = {x.name: x.args_schema.model_json_schema() for x in sample_tools}
|
|
335
|
+
|
|
336
|
+
return create_model(
|
|
337
|
+
'sandbox',
|
|
338
|
+
stateful=(bool, Field(default=False, description="Whether to maintain state between executions")),
|
|
339
|
+
allow_net=(bool, Field(default=True, description="Whether to allow network access for package installation")),
|
|
340
|
+
selected_tools=(List[Literal[tuple(selected_tools)]],
|
|
341
|
+
Field(default=[], json_schema_extra={'args_schemas': selected_tools})),
|
|
342
|
+
|
|
343
|
+
__config__=ConfigDict(json_schema_extra={
|
|
344
|
+
'metadata': {
|
|
345
|
+
"label": "Python Sandbox",
|
|
346
|
+
"icon_url": "sandbox.svg",
|
|
347
|
+
"hidden": False,
|
|
348
|
+
"categories": ["code", "execution", "internal_tool"],
|
|
349
|
+
"extra_categories": ["python", "pyodide", "sandbox", "code execution"],
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
@classmethod
|
|
355
|
+
def get_toolkit(cls, stateful: bool = False, allow_net: bool = True, alita_client=None, **kwargs):
|
|
356
|
+
"""
|
|
357
|
+
Get toolkit with sandbox tools.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
stateful: Whether to maintain state between executions
|
|
361
|
+
allow_net: Whether to allow network access
|
|
362
|
+
alita_client: Alita client instance for sandbox tools
|
|
363
|
+
**kwargs: Additional arguments
|
|
364
|
+
"""
|
|
365
|
+
tools = []
|
|
366
|
+
|
|
367
|
+
if stateful:
|
|
368
|
+
tools.append(StatefulPyodideSandboxTool(allow_net=allow_net, alita_client=alita_client))
|
|
369
|
+
else:
|
|
370
|
+
tools.append(PyodideSandboxTool(stateful=False, allow_net=allow_net, alita_client=alita_client))
|
|
371
|
+
|
|
372
|
+
return cls(tools=tools)
|
|
373
|
+
|
|
374
|
+
def get_tools(self):
|
|
375
|
+
return self.tools
|