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.

Files changed (118) hide show
  1. alita_sdk/cli/__init__.py +10 -0
  2. alita_sdk/cli/__main__.py +17 -0
  3. alita_sdk/cli/agent_executor.py +144 -0
  4. alita_sdk/cli/agent_loader.py +197 -0
  5. alita_sdk/cli/agent_ui.py +166 -0
  6. alita_sdk/cli/agents.py +1069 -0
  7. alita_sdk/cli/callbacks.py +576 -0
  8. alita_sdk/cli/cli.py +159 -0
  9. alita_sdk/cli/config.py +153 -0
  10. alita_sdk/cli/formatting.py +182 -0
  11. alita_sdk/cli/mcp_loader.py +315 -0
  12. alita_sdk/cli/toolkit.py +330 -0
  13. alita_sdk/cli/toolkit_loader.py +55 -0
  14. alita_sdk/cli/tools/__init__.py +9 -0
  15. alita_sdk/cli/tools/filesystem.py +905 -0
  16. alita_sdk/configurations/bitbucket.py +95 -0
  17. alita_sdk/configurations/confluence.py +96 -1
  18. alita_sdk/configurations/gitlab.py +79 -0
  19. alita_sdk/configurations/jira.py +103 -0
  20. alita_sdk/configurations/testrail.py +88 -0
  21. alita_sdk/configurations/xray.py +93 -0
  22. alita_sdk/configurations/zephyr_enterprise.py +93 -0
  23. alita_sdk/configurations/zephyr_essential.py +75 -0
  24. alita_sdk/runtime/clients/artifact.py +1 -1
  25. alita_sdk/runtime/clients/client.py +47 -10
  26. alita_sdk/runtime/clients/mcp_discovery.py +342 -0
  27. alita_sdk/runtime/clients/mcp_manager.py +262 -0
  28. alita_sdk/runtime/clients/sandbox_client.py +373 -0
  29. alita_sdk/runtime/langchain/assistant.py +70 -41
  30. alita_sdk/runtime/langchain/constants.py +6 -1
  31. alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
  32. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -1
  33. alita_sdk/runtime/langchain/document_loaders/constants.py +73 -100
  34. alita_sdk/runtime/langchain/langraph_agent.py +164 -38
  35. alita_sdk/runtime/langchain/utils.py +43 -7
  36. alita_sdk/runtime/models/mcp_models.py +61 -0
  37. alita_sdk/runtime/toolkits/__init__.py +24 -0
  38. alita_sdk/runtime/toolkits/application.py +8 -1
  39. alita_sdk/runtime/toolkits/artifact.py +5 -6
  40. alita_sdk/runtime/toolkits/mcp.py +895 -0
  41. alita_sdk/runtime/toolkits/tools.py +140 -50
  42. alita_sdk/runtime/tools/__init__.py +7 -2
  43. alita_sdk/runtime/tools/application.py +7 -0
  44. alita_sdk/runtime/tools/function.py +94 -5
  45. alita_sdk/runtime/tools/graph.py +10 -4
  46. alita_sdk/runtime/tools/image_generation.py +104 -8
  47. alita_sdk/runtime/tools/llm.py +204 -114
  48. alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
  49. alita_sdk/runtime/tools/mcp_remote_tool.py +166 -0
  50. alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
  51. alita_sdk/runtime/tools/sandbox.py +180 -79
  52. alita_sdk/runtime/tools/vectorstore.py +22 -21
  53. alita_sdk/runtime/tools/vectorstore_base.py +79 -26
  54. alita_sdk/runtime/utils/mcp_oauth.py +164 -0
  55. alita_sdk/runtime/utils/mcp_sse_client.py +405 -0
  56. alita_sdk/runtime/utils/streamlit.py +34 -3
  57. alita_sdk/runtime/utils/toolkit_utils.py +14 -4
  58. alita_sdk/runtime/utils/utils.py +1 -0
  59. alita_sdk/tools/__init__.py +48 -31
  60. alita_sdk/tools/ado/repos/__init__.py +1 -0
  61. alita_sdk/tools/ado/test_plan/__init__.py +1 -1
  62. alita_sdk/tools/ado/wiki/__init__.py +1 -5
  63. alita_sdk/tools/ado/work_item/__init__.py +1 -5
  64. alita_sdk/tools/ado/work_item/ado_wrapper.py +17 -8
  65. alita_sdk/tools/base_indexer_toolkit.py +194 -112
  66. alita_sdk/tools/bitbucket/__init__.py +1 -0
  67. alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
  68. alita_sdk/tools/code/sonar/__init__.py +1 -1
  69. alita_sdk/tools/code_indexer_toolkit.py +15 -5
  70. alita_sdk/tools/confluence/__init__.py +2 -2
  71. alita_sdk/tools/confluence/api_wrapper.py +110 -63
  72. alita_sdk/tools/confluence/loader.py +10 -0
  73. alita_sdk/tools/elitea_base.py +22 -22
  74. alita_sdk/tools/github/__init__.py +2 -2
  75. alita_sdk/tools/gitlab/__init__.py +2 -1
  76. alita_sdk/tools/gitlab/api_wrapper.py +11 -7
  77. alita_sdk/tools/gitlab_org/__init__.py +1 -2
  78. alita_sdk/tools/google_places/__init__.py +2 -1
  79. alita_sdk/tools/jira/__init__.py +1 -0
  80. alita_sdk/tools/jira/api_wrapper.py +1 -1
  81. alita_sdk/tools/memory/__init__.py +1 -1
  82. alita_sdk/tools/non_code_indexer_toolkit.py +2 -2
  83. alita_sdk/tools/openapi/__init__.py +10 -1
  84. alita_sdk/tools/pandas/__init__.py +1 -1
  85. alita_sdk/tools/postman/__init__.py +2 -1
  86. alita_sdk/tools/postman/api_wrapper.py +18 -8
  87. alita_sdk/tools/postman/postman_analysis.py +8 -1
  88. alita_sdk/tools/pptx/__init__.py +2 -2
  89. alita_sdk/tools/qtest/__init__.py +3 -3
  90. alita_sdk/tools/qtest/api_wrapper.py +1708 -76
  91. alita_sdk/tools/rally/__init__.py +1 -2
  92. alita_sdk/tools/report_portal/__init__.py +1 -0
  93. alita_sdk/tools/salesforce/__init__.py +1 -0
  94. alita_sdk/tools/servicenow/__init__.py +2 -3
  95. alita_sdk/tools/sharepoint/__init__.py +1 -0
  96. alita_sdk/tools/sharepoint/api_wrapper.py +125 -34
  97. alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
  98. alita_sdk/tools/sharepoint/utils.py +8 -2
  99. alita_sdk/tools/slack/__init__.py +1 -0
  100. alita_sdk/tools/sql/__init__.py +2 -1
  101. alita_sdk/tools/sql/api_wrapper.py +71 -23
  102. alita_sdk/tools/testio/__init__.py +1 -0
  103. alita_sdk/tools/testrail/__init__.py +1 -3
  104. alita_sdk/tools/utils/__init__.py +17 -0
  105. alita_sdk/tools/utils/content_parser.py +35 -24
  106. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +67 -21
  107. alita_sdk/tools/xray/__init__.py +2 -1
  108. alita_sdk/tools/zephyr/__init__.py +2 -1
  109. alita_sdk/tools/zephyr_enterprise/__init__.py +1 -0
  110. alita_sdk/tools/zephyr_essential/__init__.py +1 -0
  111. alita_sdk/tools/zephyr_scale/__init__.py +1 -0
  112. alita_sdk/tools/zephyr_squad/__init__.py +1 -0
  113. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/METADATA +8 -2
  114. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/RECORD +118 -93
  115. alita_sdk-0.3.462.dist-info/entry_points.txt +2 -0
  116. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/WHEEL +0 -0
  117. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/licenses/LICENSE +0 -0
  118. {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, Union, Dict
6
- from langchain_core.tools import BaseTool
7
- from pydantic import BaseModel, Field, create_model
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
- # Check if cache environment file exists and source it
31
- cache_env_file = os.path.expanduser("~/.pyodide_cache_env")
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(code))
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(code))
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
- # Format the output
194
- output_parts = []
195
-
241
+
242
+ result_dict = {}
243
+
196
244
  if result.result is not None:
197
- output_parts.append(f"Result: {result.result}")
198
-
245
+ result_dict["result"] = result.result
246
+
199
247
  if result.stdout:
200
- output_parts.append(f"Output: {result.stdout}")
201
-
248
+ result_dict["output"] = result.stdout
249
+
202
250
  if result.stderr:
203
- output_parts.append(f"Error: {result.stderr}")
204
-
251
+ result_dict["error"] = result.stderr
252
+
205
253
  if result.status == 'error':
206
- output_parts.append(f"Execution failed with status: {result.status}")
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
- output_parts.append(execution_info)
215
-
216
- return "\n".join(output_parts) if output_parts else "Code executed successfully (no output)"
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