camel-ai 0.2.75a6__py3-none-any.whl → 0.2.76a0__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 camel-ai might be problematic. Click here for more details.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +151 -37
- camel/configs/__init__.py +3 -0
- camel/configs/amd_config.py +70 -0
- camel/interpreters/__init__.py +2 -0
- camel/interpreters/microsandbox_interpreter.py +395 -0
- camel/models/__init__.py +2 -0
- camel/models/amd_model.py +101 -0
- camel/models/model_factory.py +2 -0
- camel/models/openai_model.py +0 -6
- camel/runtimes/daytona_runtime.py +11 -12
- camel/toolkits/__init__.py +5 -3
- camel/toolkits/code_execution.py +28 -1
- camel/toolkits/function_tool.py +6 -1
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +8 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +12 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +33 -14
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +135 -40
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +2 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +43 -207
- camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +231 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +39 -6
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +241 -56
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +5 -1
- camel/toolkits/{openai_image_toolkit.py → image_generation_toolkit.py} +98 -31
- camel/toolkits/mcp_toolkit.py +39 -14
- camel/toolkits/minimax_mcp_toolkit.py +195 -0
- camel/toolkits/terminal_toolkit.py +12 -2
- camel/toolkits/video_analysis_toolkit.py +16 -10
- camel/types/enums.py +11 -0
- camel/utils/commons.py +2 -0
- camel/utils/mcp.py +136 -2
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76a0.dist-info}/METADATA +5 -3
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76a0.dist-info}/RECORD +38 -31
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76a0.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.75a6.dist-info → camel_ai-0.2.76a0.dist-info}/licenses/LICENSE +0 -0
camel/toolkits/mcp_toolkit.py
CHANGED
|
@@ -220,26 +220,34 @@ class MCPToolkit(BaseToolkit):
|
|
|
220
220
|
self._exit_stack = AsyncExitStack()
|
|
221
221
|
|
|
222
222
|
try:
|
|
223
|
-
#
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
except Exception as e:
|
|
231
|
-
logger.error(f"Failed to connect to client {i+1}: {e}")
|
|
232
|
-
# AsyncExitStack will handle cleanup of already connected
|
|
233
|
-
await self._exit_stack.aclose()
|
|
234
|
-
self._exit_stack = None
|
|
235
|
-
error_msg = f"Failed to connect to client {i+1}: {e}"
|
|
236
|
-
raise MCPConnectionError(error_msg) from e
|
|
223
|
+
# Apply timeout to the entire connection process
|
|
224
|
+
import asyncio
|
|
225
|
+
|
|
226
|
+
timeout_seconds = self.timeout or 30.0
|
|
227
|
+
await asyncio.wait_for(
|
|
228
|
+
self._connect_all_clients(), timeout=timeout_seconds
|
|
229
|
+
)
|
|
237
230
|
|
|
238
231
|
self._is_connected = True
|
|
239
232
|
msg = f"Successfully connected to {len(self.clients)} MCP servers"
|
|
240
233
|
logger.info(msg)
|
|
241
234
|
return self
|
|
242
235
|
|
|
236
|
+
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
237
|
+
self._is_connected = False
|
|
238
|
+
if self._exit_stack:
|
|
239
|
+
await self._exit_stack.aclose()
|
|
240
|
+
self._exit_stack = None
|
|
241
|
+
|
|
242
|
+
timeout_seconds = self.timeout or 30.0
|
|
243
|
+
error_msg = (
|
|
244
|
+
f"Connection timeout after {timeout_seconds}s. "
|
|
245
|
+
f"One or more MCP servers are not responding. "
|
|
246
|
+
f"Please check if the servers are running and accessible."
|
|
247
|
+
)
|
|
248
|
+
logger.error(error_msg)
|
|
249
|
+
raise MCPConnectionError(error_msg)
|
|
250
|
+
|
|
243
251
|
except Exception:
|
|
244
252
|
self._is_connected = False
|
|
245
253
|
if self._exit_stack:
|
|
@@ -247,6 +255,23 @@ class MCPToolkit(BaseToolkit):
|
|
|
247
255
|
self._exit_stack = None
|
|
248
256
|
raise
|
|
249
257
|
|
|
258
|
+
async def _connect_all_clients(self):
|
|
259
|
+
r"""Connect to all clients sequentially."""
|
|
260
|
+
# Connect to all clients using AsyncExitStack
|
|
261
|
+
for i, client in enumerate(self.clients):
|
|
262
|
+
try:
|
|
263
|
+
# Use MCPClient directly as async context manager
|
|
264
|
+
await self._exit_stack.enter_async_context(client)
|
|
265
|
+
msg = f"Connected to client {i+1}/{len(self.clients)}"
|
|
266
|
+
logger.debug(msg)
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.error(f"Failed to connect to client {i+1}: {e}")
|
|
269
|
+
# AsyncExitStack will cleanup already connected clients
|
|
270
|
+
await self._exit_stack.aclose()
|
|
271
|
+
self._exit_stack = None
|
|
272
|
+
error_msg = f"Failed to connect to client {i+1}: {e}"
|
|
273
|
+
raise MCPConnectionError(error_msg) from e
|
|
274
|
+
|
|
250
275
|
async def disconnect(self):
|
|
251
276
|
r"""Disconnect from all MCP servers."""
|
|
252
277
|
if not self._is_connected:
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from camel.toolkits import BaseToolkit, FunctionTool
|
|
19
|
+
|
|
20
|
+
from .mcp_toolkit import MCPToolkit
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MinimaxMCPToolkit(BaseToolkit):
|
|
24
|
+
r"""MinimaxMCPToolkit provides an interface for interacting with
|
|
25
|
+
MiniMax AI services using the MiniMax MCP server.
|
|
26
|
+
|
|
27
|
+
This toolkit enables access to MiniMax's multimedia generation
|
|
28
|
+
capabilities including text-to-audio, voice cloning, video generation,
|
|
29
|
+
image generation, music generation, and voice design.
|
|
30
|
+
|
|
31
|
+
This toolkit can be used as an async context manager for automatic
|
|
32
|
+
connection management:
|
|
33
|
+
|
|
34
|
+
# Using explicit API key
|
|
35
|
+
async with MinimaxMCPToolkit(api_key="your-key") as toolkit:
|
|
36
|
+
tools = toolkit.get_tools()
|
|
37
|
+
# Toolkit is automatically disconnected when exiting
|
|
38
|
+
|
|
39
|
+
# Using environment variables (recommended for security)
|
|
40
|
+
# Set MINIMAX_API_KEY=your-key in environment
|
|
41
|
+
async with MinimaxMCPToolkit() as toolkit:
|
|
42
|
+
tools = toolkit.get_tools()
|
|
43
|
+
|
|
44
|
+
Environment Variables:
|
|
45
|
+
MINIMAX_API_KEY: MiniMax API key for authentication
|
|
46
|
+
MINIMAX_API_HOST: API host URL (default: https://api.minimax.io)
|
|
47
|
+
MINIMAX_MCP_BASE_PATH: Base path for output files
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
timeout (Optional[float]): Connection timeout in seconds.
|
|
51
|
+
(default: :obj:`None`)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
api_key: Optional[str] = None,
|
|
57
|
+
api_host: str = "https://api.minimax.io",
|
|
58
|
+
base_path: Optional[str] = None,
|
|
59
|
+
timeout: Optional[float] = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
r"""Initializes the MinimaxMCPToolkit.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
api_key (Optional[str]): MiniMax API key for authentication.
|
|
65
|
+
If None, will attempt to read from MINIMAX_API_KEY
|
|
66
|
+
environment variable. (default: :obj:`None`)
|
|
67
|
+
api_host (str): MiniMax API host URL. Can be either
|
|
68
|
+
"https://api.minimax.io" (global) or
|
|
69
|
+
"https://api.minimaxi.com" (mainland China).
|
|
70
|
+
Can also be read from MINIMAX_API_HOST environment variable.
|
|
71
|
+
(default: :obj:`"https://api.minimax.io"`)
|
|
72
|
+
base_path (Optional[str]): Base path for output files.
|
|
73
|
+
If None, uses current working directory. Can also be read
|
|
74
|
+
from MINIMAX_MCP_BASE_PATH environment variable.
|
|
75
|
+
(default: :obj:`None`)
|
|
76
|
+
timeout (Optional[float]): Connection timeout in seconds.
|
|
77
|
+
(default: :obj:`None`)
|
|
78
|
+
"""
|
|
79
|
+
super().__init__(timeout=timeout)
|
|
80
|
+
|
|
81
|
+
# Read API key from parameter or environment variable
|
|
82
|
+
if api_key is None:
|
|
83
|
+
api_key = os.getenv("MINIMAX_API_KEY")
|
|
84
|
+
|
|
85
|
+
if not api_key:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"api_key must be provided either as a parameter or through "
|
|
88
|
+
"the MINIMAX_API_KEY environment variable"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Read API host from environment variable if not overridden
|
|
92
|
+
env_api_host = os.getenv("MINIMAX_API_HOST")
|
|
93
|
+
if env_api_host:
|
|
94
|
+
api_host = env_api_host
|
|
95
|
+
|
|
96
|
+
# Read base path from environment variable if not provided
|
|
97
|
+
if base_path is None:
|
|
98
|
+
base_path = os.getenv("MINIMAX_MCP_BASE_PATH")
|
|
99
|
+
|
|
100
|
+
# Set up environment variables for the MCP server
|
|
101
|
+
env = {
|
|
102
|
+
"MINIMAX_API_KEY": api_key,
|
|
103
|
+
"MINIMAX_API_HOST": api_host,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if base_path:
|
|
107
|
+
env["MINIMAX_MCP_BASE_PATH"] = base_path
|
|
108
|
+
|
|
109
|
+
self._mcp_toolkit = MCPToolkit(
|
|
110
|
+
config_dict={
|
|
111
|
+
"mcpServers": {
|
|
112
|
+
"minimax": {
|
|
113
|
+
"command": "uvx",
|
|
114
|
+
"args": ["minimax-mcp", "-y"],
|
|
115
|
+
"env": env,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
timeout=timeout,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
async def connect(self):
|
|
123
|
+
r"""Explicitly connect to the MiniMax MCP server."""
|
|
124
|
+
await self._mcp_toolkit.connect()
|
|
125
|
+
|
|
126
|
+
async def disconnect(self):
|
|
127
|
+
r"""Explicitly disconnect from the MiniMax MCP server."""
|
|
128
|
+
await self._mcp_toolkit.disconnect()
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def is_connected(self) -> bool:
|
|
132
|
+
r"""Check if the toolkit is connected to the MCP server.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
bool: True if connected, False otherwise.
|
|
136
|
+
"""
|
|
137
|
+
return self._mcp_toolkit.is_connected
|
|
138
|
+
|
|
139
|
+
async def __aenter__(self) -> "MinimaxMCPToolkit":
|
|
140
|
+
r"""Async context manager entry point.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
MinimaxMCPToolkit: The connected toolkit instance.
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
async with MinimaxMCPToolkit(api_key="your-key") as toolkit:
|
|
147
|
+
tools = toolkit.get_tools()
|
|
148
|
+
"""
|
|
149
|
+
await self.connect()
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
async def __aexit__(self, _exc_type, _exc_val, _exc_tb) -> None:
|
|
153
|
+
r"""Async context manager exit point.
|
|
154
|
+
|
|
155
|
+
Automatically disconnects from the MiniMax MCP server.
|
|
156
|
+
"""
|
|
157
|
+
await self.disconnect()
|
|
158
|
+
|
|
159
|
+
def get_tools(self) -> List[FunctionTool]:
|
|
160
|
+
r"""Returns a list of tools provided by the MiniMax MCP server.
|
|
161
|
+
|
|
162
|
+
This includes tools for:
|
|
163
|
+
- Text-to-audio conversion
|
|
164
|
+
- Voice cloning
|
|
165
|
+
- Video generation
|
|
166
|
+
- Image generation
|
|
167
|
+
- Music generation
|
|
168
|
+
- Voice design
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
List[FunctionTool]: List of available MiniMax AI tools.
|
|
172
|
+
"""
|
|
173
|
+
return self._mcp_toolkit.get_tools()
|
|
174
|
+
|
|
175
|
+
def get_text_tools(self) -> str:
|
|
176
|
+
r"""Returns a string containing the descriptions of the tools.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
str: A string containing the descriptions of all MiniMax tools.
|
|
180
|
+
"""
|
|
181
|
+
return self._mcp_toolkit.get_text_tools()
|
|
182
|
+
|
|
183
|
+
async def call_tool(
|
|
184
|
+
self, tool_name: str, tool_args: Dict[str, Any]
|
|
185
|
+
) -> Any:
|
|
186
|
+
r"""Call a MiniMax tool by name.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
tool_name (str): Name of the tool to call.
|
|
190
|
+
tool_args (Dict[str, Any]): Arguments to pass to the tool.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Any: The result of the tool call.
|
|
194
|
+
"""
|
|
195
|
+
return await self._mcp_toolkit.call_tool(tool_name, tool_args)
|
|
@@ -66,6 +66,9 @@ class TerminalToolkit(BaseToolkit):
|
|
|
66
66
|
connecting them to the terminal's standard input. This is useful
|
|
67
67
|
for commands that require user input, like `ssh`. Interactive mode
|
|
68
68
|
is only supported on macOS and Linux. (default: :obj:`False`)
|
|
69
|
+
log_dir (Optional[str]): Custom directory path for log files.
|
|
70
|
+
If None, logs are saved to the current working directory.
|
|
71
|
+
(default: :obj:`None`)
|
|
69
72
|
|
|
70
73
|
Note:
|
|
71
74
|
Most functions are compatible with Unix-based systems (macOS, Linux).
|
|
@@ -83,6 +86,7 @@ class TerminalToolkit(BaseToolkit):
|
|
|
83
86
|
clone_current_env: bool = False,
|
|
84
87
|
safe_mode: bool = True,
|
|
85
88
|
interactive: bool = False,
|
|
89
|
+
log_dir: Optional[str] = None,
|
|
86
90
|
):
|
|
87
91
|
# Store timeout before calling super().__init__
|
|
88
92
|
self._timeout = timeout
|
|
@@ -99,6 +103,7 @@ class TerminalToolkit(BaseToolkit):
|
|
|
99
103
|
self.use_shell_mode = use_shell_mode
|
|
100
104
|
self._human_takeover_active = False
|
|
101
105
|
self.interactive = interactive
|
|
106
|
+
self.log_dir = log_dir
|
|
102
107
|
|
|
103
108
|
self.python_executable = sys.executable
|
|
104
109
|
self.is_macos = platform.system() == 'Darwin'
|
|
@@ -153,8 +158,13 @@ class TerminalToolkit(BaseToolkit):
|
|
|
153
158
|
r"""Set up file output to replace GUI, using a fixed file to simulate
|
|
154
159
|
terminal.
|
|
155
160
|
"""
|
|
156
|
-
|
|
157
|
-
self.
|
|
161
|
+
# Use custom log directory if provided, otherwise use current directory
|
|
162
|
+
if self.log_dir:
|
|
163
|
+
# Create the log directory if it doesn't exist
|
|
164
|
+
os.makedirs(self.log_dir, exist_ok=True)
|
|
165
|
+
self.log_file = os.path.join(self.log_dir, "camel_terminal.txt")
|
|
166
|
+
else:
|
|
167
|
+
self.log_file = os.path.join(os.getcwd(), "camel_terminal.txt")
|
|
158
168
|
|
|
159
169
|
# Inform the user
|
|
160
170
|
logger.info(f"Terminal output will be redirected to: {self.log_file}")
|
|
@@ -195,18 +195,24 @@ class VideoAnalysisToolkit(BaseToolkit):
|
|
|
195
195
|
destroyed.
|
|
196
196
|
"""
|
|
197
197
|
# Clean up temporary files
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
198
|
+
if hasattr(self, '_temp_files'):
|
|
199
|
+
for temp_file in self._temp_files:
|
|
200
|
+
if os.path.exists(temp_file):
|
|
201
|
+
try:
|
|
202
|
+
os.remove(temp_file)
|
|
203
|
+
logger.debug(f"Removed temporary file: {temp_file}")
|
|
204
|
+
except OSError as e:
|
|
205
|
+
logger.warning(
|
|
206
|
+
f"Failed to remove temporary file {temp_file}: {e}"
|
|
207
|
+
)
|
|
207
208
|
|
|
208
209
|
# Clean up temporary directory if needed
|
|
209
|
-
if
|
|
210
|
+
if (
|
|
211
|
+
hasattr(self, '_cleanup')
|
|
212
|
+
and self._cleanup
|
|
213
|
+
and hasattr(self, '_working_directory')
|
|
214
|
+
and os.path.exists(self._working_directory)
|
|
215
|
+
):
|
|
210
216
|
try:
|
|
211
217
|
import sys
|
|
212
218
|
|
camel/types/enums.py
CHANGED
|
@@ -64,6 +64,8 @@ class ModelType(UnifiedModelType, Enum):
|
|
|
64
64
|
AWS_CLAUDE_OPUS_4 = "anthropic.claude-opus-4-20250514-v1:0"
|
|
65
65
|
AWS_CLAUDE_OPUS_4_1 = "anthropic.claude-opus-4-1-20250805-v1:0"
|
|
66
66
|
|
|
67
|
+
AMD_GPT4 = "dvue-aoai-001-gpt-4.1"
|
|
68
|
+
|
|
67
69
|
GLM_4 = "glm-4"
|
|
68
70
|
GLM_4V = "glm-4v"
|
|
69
71
|
GLM_4V_FLASH = "glm-4v-flash"
|
|
@@ -516,6 +518,13 @@ class ModelType(UnifiedModelType, Enum):
|
|
|
516
518
|
ModelType.O3,
|
|
517
519
|
}
|
|
518
520
|
|
|
521
|
+
@property
|
|
522
|
+
def is_amd(self) -> bool:
|
|
523
|
+
r"""Returns whether this type of models is a AMD model."""
|
|
524
|
+
return self in {
|
|
525
|
+
ModelType.AMD_GPT4,
|
|
526
|
+
}
|
|
527
|
+
|
|
519
528
|
@property
|
|
520
529
|
def is_aws_bedrock(self) -> bool:
|
|
521
530
|
r"""Returns whether this type of models is an AWS Bedrock model."""
|
|
@@ -1349,6 +1358,7 @@ class ModelType(UnifiedModelType, Enum):
|
|
|
1349
1358
|
ModelType.GLM_4_LONG,
|
|
1350
1359
|
ModelType.TOGETHER_LLAMA_4_MAVERICK,
|
|
1351
1360
|
ModelType.OPENROUTER_LLAMA_4_MAVERICK,
|
|
1361
|
+
ModelType.AMD_GPT4,
|
|
1352
1362
|
ModelType.GPT_4_1,
|
|
1353
1363
|
ModelType.GPT_4_1_MINI,
|
|
1354
1364
|
ModelType.GPT_4_1_NANO,
|
|
@@ -1579,6 +1589,7 @@ class ModelPlatformType(Enum):
|
|
|
1579
1589
|
COHERE = "cohere"
|
|
1580
1590
|
YI = "lingyiwanwu"
|
|
1581
1591
|
QWEN = "tongyi-qianwen"
|
|
1592
|
+
AMD = "amd"
|
|
1582
1593
|
NVIDIA = "nvidia"
|
|
1583
1594
|
DEEPSEEK = "deepseek"
|
|
1584
1595
|
PPIO = "ppio"
|
camel/utils/commons.py
CHANGED
|
@@ -354,6 +354,8 @@ def api_keys_required(
|
|
|
354
354
|
key_way = "https://www.zhipuai.cn/"
|
|
355
355
|
elif env_var_name == 'KLAVIS_API_KEY':
|
|
356
356
|
key_way = "https://www.klavis.ai/docs"
|
|
357
|
+
elif env_var_name == 'XAI_API_KEY':
|
|
358
|
+
key_way = "https://api.x.ai/v1"
|
|
357
359
|
|
|
358
360
|
if missing_keys:
|
|
359
361
|
raise ValueError(
|
camel/utils/mcp.py
CHANGED
|
@@ -13,7 +13,121 @@
|
|
|
13
13
|
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
14
|
import functools
|
|
15
15
|
import inspect
|
|
16
|
-
|
|
16
|
+
import warnings
|
|
17
|
+
from typing import (
|
|
18
|
+
Any,
|
|
19
|
+
Callable,
|
|
20
|
+
List,
|
|
21
|
+
Optional,
|
|
22
|
+
Tuple,
|
|
23
|
+
Union,
|
|
24
|
+
get_args,
|
|
25
|
+
get_origin,
|
|
26
|
+
get_type_hints,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from pydantic import create_model
|
|
30
|
+
from pydantic.errors import PydanticSchemaGenerationError
|
|
31
|
+
|
|
32
|
+
from camel.logger import get_logger
|
|
33
|
+
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_pydantic_serializable(type_annotation: Any) -> Tuple[bool, str]:
|
|
38
|
+
r"""Check if a type annotation is Pydantic serializable.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
type_annotation: The type annotation to check
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Tuple[bool, str]: (is_serializable, error_message)
|
|
45
|
+
"""
|
|
46
|
+
# Handle None type
|
|
47
|
+
if type_annotation is type(None) or type_annotation is None:
|
|
48
|
+
return True, ""
|
|
49
|
+
|
|
50
|
+
# Handle generic types (List, Dict, Optional, etc.)
|
|
51
|
+
origin = get_origin(type_annotation)
|
|
52
|
+
if origin is not None:
|
|
53
|
+
args = get_args(type_annotation)
|
|
54
|
+
|
|
55
|
+
# For Union types (including Optional), check all args
|
|
56
|
+
if origin is Union:
|
|
57
|
+
for arg in args:
|
|
58
|
+
is_serializable, error_msg = _is_pydantic_serializable(arg)
|
|
59
|
+
if not is_serializable:
|
|
60
|
+
return False, error_msg
|
|
61
|
+
return True, ""
|
|
62
|
+
|
|
63
|
+
# For List, Set, Tuple, etc., check the contained types
|
|
64
|
+
if origin in (list, set, tuple, frozenset):
|
|
65
|
+
for arg in args:
|
|
66
|
+
is_serializable, error_msg = _is_pydantic_serializable(arg)
|
|
67
|
+
if not is_serializable:
|
|
68
|
+
return False, error_msg
|
|
69
|
+
return True, ""
|
|
70
|
+
|
|
71
|
+
# For Dict, check both key and value types
|
|
72
|
+
if origin is dict:
|
|
73
|
+
for arg in args:
|
|
74
|
+
is_serializable, error_msg = _is_pydantic_serializable(arg)
|
|
75
|
+
if not is_serializable:
|
|
76
|
+
return False, error_msg
|
|
77
|
+
return True, ""
|
|
78
|
+
|
|
79
|
+
# Try to create a simple pydantic model with this type
|
|
80
|
+
try:
|
|
81
|
+
create_model("TestModel", test_field=(type_annotation, ...))
|
|
82
|
+
# If model creation succeeds, the type is serializable
|
|
83
|
+
return True, ""
|
|
84
|
+
except (PydanticSchemaGenerationError, TypeError, ValueError) as e:
|
|
85
|
+
error_msg = (
|
|
86
|
+
f"Type '{type_annotation}' is not Pydantic serializable. "
|
|
87
|
+
f"Consider using a custom serializable type or converting "
|
|
88
|
+
f"to bytes/base64. Error: {e!s}"
|
|
89
|
+
)
|
|
90
|
+
return False, error_msg
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _validate_function_types(func: Callable[..., Any]) -> List[str]:
|
|
94
|
+
r"""Validate function parameter and return types are Pydantic serializable.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
func (Callable[..., Any]): The function to validate.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List[str]: List of error messages for incompatible types.
|
|
101
|
+
"""
|
|
102
|
+
errors = []
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
type_hints = get_type_hints(func)
|
|
106
|
+
except (NameError, AttributeError) as e:
|
|
107
|
+
# If we can't get type hints, skip validation
|
|
108
|
+
logger.warning(f"Could not get type hints for {func.__name__}: {e}")
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
# Check return type
|
|
112
|
+
return_type = type_hints.get('return', Any)
|
|
113
|
+
if return_type != Any:
|
|
114
|
+
is_serializable, error_msg = _is_pydantic_serializable(return_type)
|
|
115
|
+
if not is_serializable:
|
|
116
|
+
errors.append(f"Return type: {error_msg}")
|
|
117
|
+
|
|
118
|
+
# Check parameter types
|
|
119
|
+
sig = inspect.signature(func)
|
|
120
|
+
for param_name, _param in sig.parameters.items():
|
|
121
|
+
if param_name == 'self':
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
param_type = type_hints.get(param_name, Any)
|
|
125
|
+
if param_type != Any:
|
|
126
|
+
is_serializable, error_msg = _is_pydantic_serializable(param_type)
|
|
127
|
+
if not is_serializable:
|
|
128
|
+
errors.append(f"Parameter '{param_name}': {error_msg}")
|
|
129
|
+
|
|
130
|
+
return errors
|
|
17
131
|
|
|
18
132
|
|
|
19
133
|
class MCPServer:
|
|
@@ -55,7 +169,7 @@ class MCPServer:
|
|
|
55
169
|
|
|
56
170
|
def __init__(
|
|
57
171
|
self,
|
|
58
|
-
function_names: Optional[
|
|
172
|
+
function_names: Optional[List[str]] = None,
|
|
59
173
|
server_name: Optional[str] = None,
|
|
60
174
|
):
|
|
61
175
|
self.function_names = function_names
|
|
@@ -135,6 +249,26 @@ class MCPServer:
|
|
|
135
249
|
f"Method {name} not found in class {cls.__name__} or "
|
|
136
250
|
"cannot be called."
|
|
137
251
|
)
|
|
252
|
+
|
|
253
|
+
# Validate function types for Pydantic compatibility
|
|
254
|
+
type_errors = _validate_function_types(func)
|
|
255
|
+
if type_errors:
|
|
256
|
+
error_message = (
|
|
257
|
+
f"Method '{name}' in class '{cls.__name__}' has "
|
|
258
|
+
f"non-Pydantic-serializable types:\n"
|
|
259
|
+
+ "\n".join(f" - {error}" for error in type_errors)
|
|
260
|
+
+ "\n\nSuggestions:"
|
|
261
|
+
+ "\n - Use standard Python types (str, int, float, bool, bytes)" # noqa: E501
|
|
262
|
+
+ "\n - Convert complex objects to JSON strings or bytes" # noqa: E501
|
|
263
|
+
+ "\n - Create custom Pydantic models for complex data" # noqa: E501
|
|
264
|
+
+ "\n - Use base64 encoding for binary data like images" # noqa: E501
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# For now, issue a warning instead of raising an error
|
|
268
|
+
# This allows gradual migration while alerting developers
|
|
269
|
+
warnings.warn(error_message, UserWarning, stacklevel=3)
|
|
270
|
+
logger.warning(error_message)
|
|
271
|
+
|
|
138
272
|
wrapper = self.make_wrapper(func)
|
|
139
273
|
instance.mcp.tool(name=name)(wrapper)
|
|
140
274
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: camel-ai
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.76a0
|
|
4
4
|
Summary: Communicative Agents for AI Society Study
|
|
5
5
|
Project-URL: Homepage, https://www.camel-ai.org/
|
|
6
6
|
Project-URL: Repository, https://github.com/camel-ai/camel
|
|
@@ -39,7 +39,7 @@ Requires-Dist: dappier<0.4,>=0.3.3; extra == 'all'
|
|
|
39
39
|
Requires-Dist: datacommons-pandas<0.0.4,>=0.0.3; extra == 'all'
|
|
40
40
|
Requires-Dist: datacommons<2,>=1.4.3; extra == 'all'
|
|
41
41
|
Requires-Dist: datasets<4,>=3; extra == 'all'
|
|
42
|
-
Requires-Dist: daytona-sdk
|
|
42
|
+
Requires-Dist: daytona-sdk>=0.20.0; extra == 'all'
|
|
43
43
|
Requires-Dist: diffusers<0.26,>=0.25.0; extra == 'all'
|
|
44
44
|
Requires-Dist: discord-py<3,>=2.3.2; extra == 'all'
|
|
45
45
|
Requires-Dist: docker<8,>=7.1.0; extra == 'all'
|
|
@@ -73,6 +73,7 @@ Requires-Dist: markitdown>=0.1.1; extra == 'all'
|
|
|
73
73
|
Requires-Dist: math-verify<0.8,>=0.7.0; extra == 'all'
|
|
74
74
|
Requires-Dist: mcp>=1.3.0; extra == 'all'
|
|
75
75
|
Requires-Dist: mem0ai>=0.1.67; extra == 'all'
|
|
76
|
+
Requires-Dist: microsandbox>=0.1.8; extra == 'all'
|
|
76
77
|
Requires-Dist: mistralai<2,>=1.1.0; extra == 'all'
|
|
77
78
|
Requires-Dist: mock<6,>=5; extra == 'all'
|
|
78
79
|
Requires-Dist: mypy<2,>=1.5.1; extra == 'all'
|
|
@@ -186,13 +187,14 @@ Requires-Dist: uv<0.8,>=0.7.0; extra == 'dev'
|
|
|
186
187
|
Provides-Extra: dev-tools
|
|
187
188
|
Requires-Dist: aci-sdk>=1.0.0b1; extra == 'dev-tools'
|
|
188
189
|
Requires-Dist: agentops<0.4,>=0.3.21; extra == 'dev-tools'
|
|
189
|
-
Requires-Dist: daytona-sdk
|
|
190
|
+
Requires-Dist: daytona-sdk>=0.20.0; extra == 'dev-tools'
|
|
190
191
|
Requires-Dist: docker<8,>=7.1.0; extra == 'dev-tools'
|
|
191
192
|
Requires-Dist: e2b-code-interpreter<2,>=1.0.3; extra == 'dev-tools'
|
|
192
193
|
Requires-Dist: ipykernel<7,>=6.0.0; extra == 'dev-tools'
|
|
193
194
|
Requires-Dist: jupyter-client<9,>=8.6.2; extra == 'dev-tools'
|
|
194
195
|
Requires-Dist: langfuse>=2.60.5; extra == 'dev-tools'
|
|
195
196
|
Requires-Dist: mcp>=1.3.0; extra == 'dev-tools'
|
|
197
|
+
Requires-Dist: microsandbox>=0.1.8; extra == 'dev-tools'
|
|
196
198
|
Requires-Dist: tree-sitter-python<0.24,>=0.23.6; extra == 'dev-tools'
|
|
197
199
|
Requires-Dist: tree-sitter<0.24,>=0.23.2; extra == 'dev-tools'
|
|
198
200
|
Requires-Dist: typer>=0.15.2; extra == 'dev-tools'
|