camel-ai 0.2.57__py3-none-any.whl → 0.2.59__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/agents/mcp_agent.py CHANGED
@@ -12,18 +12,26 @@
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
 
15
+ import asyncio
15
16
  import json
17
+ import platform
16
18
  import re
17
- from typing import Optional
19
+ from typing import Any, Callable, Dict, List, Optional, Union, cast
18
20
 
19
21
  from camel.agents import ChatAgent
20
22
  from camel.logger import get_logger
21
23
  from camel.messages import BaseMessage
22
- from camel.models import BaseModelBackend
24
+ from camel.models import BaseModelBackend, ModelFactory
23
25
  from camel.prompts import TextPrompt
24
26
  from camel.responses import ChatAgentResponse
25
- from camel.toolkits import MCPToolkit
26
- from camel.types import RoleType
27
+ from camel.toolkits import FunctionTool, MCPToolkit
28
+ from camel.types import (
29
+ BaseMCPRegistryConfig,
30
+ MCPRegistryType,
31
+ ModelPlatformType,
32
+ ModelType,
33
+ RoleType,
34
+ )
27
35
 
28
36
  # AgentOps decorator setting
29
37
  try:
@@ -38,6 +46,7 @@ except (ImportError, AttributeError):
38
46
 
39
47
  logger = get_logger(__name__)
40
48
 
49
+
41
50
  SYS_MSG_CONTENT = """
42
51
  You are a helpful assistant, and you prefer to use tools provided by the user
43
52
  to solve problems.
@@ -80,22 +89,62 @@ Please answer me according to the result directly.
80
89
 
81
90
  @track_agent(name="MCPAgent")
82
91
  class MCPAgent(ChatAgent):
92
+ r"""A specialized agent designed to interact with MCP registries.
93
+ The MCPAgent enhances a base ChatAgent by integrating MCP tools from
94
+ various registries for search capabilities.
95
+
96
+ Attributes:
97
+ system_message (Optional[str]): The system message for the chat agent.
98
+ (default: :str:`"You are an assistant with search capabilities
99
+ using MCP tools."`)
100
+ model (BaseModelBackend): The model backend to use for generating
101
+ responses. (default: :obj:`ModelPlatformType.DEFAULT` with
102
+ `ModelType.DEFAULT`)
103
+ registry_configs (List[BaseMCPRegistryConfig]): List of registry
104
+ configurations (default: :obj:`None`)
105
+ local_config (Optional[Dict[str, Any]]): The local configuration for
106
+ the MCP agent. (default: :obj:`None`)
107
+ local_config_path (Optional[str]): The path to the local configuration
108
+ file for the MCP agent. (default: :obj:`None`)
109
+ function_calling_available (bool): Flag indicating whether the
110
+ model is equipped with the function calling ability.
111
+ (default: :obj:`True`)
112
+ **kwargs: Inherited from ChatAgent
113
+ """
114
+
83
115
  def __init__(
84
116
  self,
85
- config_path: str,
117
+ system_message: Optional[Union[str, BaseMessage]] = (
118
+ "You are an assistant with search capabilities using MCP tools."
119
+ ),
86
120
  model: Optional[BaseModelBackend] = None,
87
- function_calling_available: bool = False,
88
- ) -> None:
89
- r"""A class for the MCP agent that assists using MCP tools.
121
+ registry_configs: Optional[
122
+ Union[List[BaseMCPRegistryConfig], BaseMCPRegistryConfig]
123
+ ] = None,
124
+ local_config: Optional[Dict[str, Any]] = None,
125
+ local_config_path: Optional[str] = None,
126
+ tools: Optional[List[Union[FunctionTool, Callable]]] = None,
127
+ function_calling_available: bool = True,
128
+ **kwargs,
129
+ ):
130
+ if model is None:
131
+ model = ModelFactory.create(
132
+ model_platform=ModelPlatformType.DEFAULT,
133
+ model_type=ModelType.DEFAULT,
134
+ )
135
+
136
+ if isinstance(registry_configs, BaseMCPRegistryConfig):
137
+ self.registry_configs = [registry_configs]
138
+ else:
139
+ self.registry_configs = registry_configs or []
140
+
141
+ if local_config_path:
142
+ with open(local_config_path, 'r') as f:
143
+ local_config = json.load(f)
144
+
145
+ self.local_config = local_config
146
+ self.function_calling_available = function_calling_available
90
147
 
91
- Args:
92
- config_path (str): Path to the MCP configuration file.
93
- model (Optional[BaseModelBackend]): Model backend for the agent.
94
- (default: :obj:`None`)
95
- function_calling_available (bool): Flag indicating whether the
96
- model is equipped with the function calling ability.
97
- (default: :obj:`False`)
98
- """
99
148
  if function_calling_available:
100
149
  sys_msg_content = "You are a helpful assistant, and you prefer "
101
150
  "to use tools provided by the user to solve problems."
@@ -109,63 +158,182 @@ class MCPAgent(ChatAgent):
109
158
  content=sys_msg_content,
110
159
  )
111
160
 
112
- super().__init__(system_message, model=model)
161
+ # Initialize the toolkit if configuration is provided
162
+ self.mcp_toolkit = self._initialize_mcp_toolkit()
163
+
164
+ super().__init__(
165
+ system_message=system_message,
166
+ model=model,
167
+ tools=tools,
168
+ **kwargs,
169
+ )
170
+
171
+ def _initialize_mcp_toolkit(self) -> MCPToolkit:
172
+ r"""Initialize the MCP toolkit from the provided configuration."""
173
+ config_dict = {}
174
+ for registry_config in self.registry_configs:
175
+ config_dict.update(registry_config.get_config())
113
176
 
114
- self._mcp_toolkit = MCPToolkit(config_path=config_path)
115
- self._function_calling_available = function_calling_available
116
- self._text_tools = None
177
+ if self.local_config:
178
+ config_dict.update(self.local_config)
117
179
 
118
- async def connect(self):
119
- r"""Explicitly connect to all MCP servers."""
120
- await self._mcp_toolkit.connect()
180
+ return MCPToolkit(config_dict=config_dict)
121
181
 
122
- async def close(self):
123
- r"""Explicitly disconnect from all MCP servers."""
124
- await self._mcp_toolkit.disconnect()
182
+ def add_registry(self, registry_config: BaseMCPRegistryConfig) -> None:
183
+ r"""Add a new registry configuration to the agent.
125
184
 
126
- def add_mcp_tools(self):
127
- r"""Get the MCP tools and wrap into the models"""
185
+ Args:
186
+ registry_config (BaseMCPRegistryConfig): The registry
187
+ configuration to add.
188
+ """
189
+ self.registry_configs.append(registry_config)
190
+ # Reinitialize the toolkit with the updated configurations
191
+ self.mcp_toolkit = self._initialize_mcp_toolkit()
192
+
193
+ # If already connected, reconnect to apply changes
194
+ if self.mcp_toolkit and self.mcp_toolkit.is_connected():
195
+ try:
196
+ asyncio.run(self.disconnect())
197
+ asyncio.run(self.connect())
198
+ except RuntimeError as e:
199
+ # Handle case where we're already in an event loop
200
+ logger.warning(
201
+ f"Could not reconnect synchronously: {e}. "
202
+ f"Manual reconnection may be required."
203
+ )
204
+
205
+ @classmethod
206
+ async def create(
207
+ cls,
208
+ config_path: Optional[str] = None,
209
+ registry_configs: Optional[
210
+ Union[List[BaseMCPRegistryConfig], BaseMCPRegistryConfig]
211
+ ] = None,
212
+ model: Optional[BaseModelBackend] = None,
213
+ function_calling_available: bool = False,
214
+ **kwargs,
215
+ ) -> "MCPAgent":
216
+ r"""Create and connect an MCPAgent instance.
217
+
218
+ Args:
219
+ config_path (Optional[str]): Path to the MCP configuration file.
220
+ If provided, will load registry configs from this file.
221
+ (default: :obj:`None`)
222
+ registry_configs (Optional[Union[List[BaseMCPRegistryConfig],
223
+ BaseMCPRegistryConfig]]): Registry configurations to use.
224
+ Can be a single config or list of configs. If both config_path
225
+ and registry_configs are provided, configs from both sources
226
+ will be combined. (default: :obj:`None`)
227
+ model (Optional[BaseModelBackend]): The model backend to use.
228
+ If None, will use the default model. (default: :obj:`None`)
229
+ function_calling_available (bool): Whether the model supports
230
+ function calling. (default: :obj:`False`)
231
+ **kwargs: Additional arguments to pass to MCPAgent constructor.
128
232
 
129
- if not self._mcp_toolkit.is_connected():
130
- raise RuntimeError("The MCP server is not connected")
233
+ Returns:
234
+ MCPAgent: A connected MCPAgent instance ready to use.
235
+
236
+ Example:
237
+ >>> agent = await MCPAgent.create(
238
+ ... config_path="path/to/config.json",
239
+ ... function_calling_available=True
240
+ ... )
241
+ >>> response = await agent.run("Hello!")
242
+ """
243
+ # Initialize registry_configs list
244
+ final_registry_configs = []
131
245
 
132
- prompt = TextPrompt(TOOLS_PROMPT)
133
- self._text_tools = prompt.format(
134
- tools=self._mcp_toolkit.get_text_tools()
246
+ # Add configs from registry_configs argument if provided
247
+ if registry_configs is not None:
248
+ if isinstance(registry_configs, BaseMCPRegistryConfig):
249
+ final_registry_configs.append(registry_configs)
250
+ else:
251
+ final_registry_configs.extend(registry_configs)
252
+
253
+ # Load additional configs from file if provided
254
+ if config_path:
255
+ try:
256
+ with open(config_path, 'r') as f:
257
+ config_data = json.load(f)
258
+
259
+ # Create registry configs from the loaded data
260
+ for _, server_config in config_data.get(
261
+ "mcpServers", {}
262
+ ).items():
263
+ # Create a custom registry config for each server
264
+ registry_config = BaseMCPRegistryConfig(
265
+ type=MCPRegistryType.CUSTOM,
266
+ os=platform.system().lower(), # type: ignore [arg-type]
267
+ **server_config,
268
+ )
269
+ final_registry_configs.append(registry_config)
270
+ except Exception as e:
271
+ logger.error(f"Failed to load config from {config_path}: {e}")
272
+ raise
273
+
274
+ # Create the agent instance
275
+ agent = cls(
276
+ registry_configs=final_registry_configs,
277
+ model=model,
278
+ function_calling_available=function_calling_available,
279
+ **kwargs,
135
280
  )
136
- if self._function_calling_available:
137
- tools = self._mcp_toolkit.get_tools()
138
- for tool in tools:
139
- self.add_tool(tool)
140
281
 
141
- async def run(
142
- self,
143
- prompt: str,
282
+ # Connect to MCP servers
283
+ try:
284
+ await agent.connect()
285
+ except Exception as e:
286
+ logger.error(f"Failed to connect to MCP servers: {e}")
287
+ await agent.disconnect() # Clean up if connection fails
288
+ raise
289
+
290
+ return agent
291
+
292
+ async def connect(self) -> None:
293
+ r"""Connect to the MCP servers."""
294
+ if self.mcp_toolkit:
295
+ await self.mcp_toolkit.connect()
296
+ if self.function_calling_available:
297
+ self.add_tools(
298
+ cast(
299
+ list[FunctionTool | Callable[..., Any]],
300
+ self.mcp_toolkit.get_tools(),
301
+ )
302
+ )
303
+ else:
304
+ prompt = TextPrompt(TOOLS_PROMPT)
305
+ self._text_tools = prompt.format(
306
+ tools=self.mcp_toolkit.get_text_tools()
307
+ )
308
+
309
+ async def disconnect(self) -> None:
310
+ r"""Disconnect from the MCP servers."""
311
+ if self.mcp_toolkit:
312
+ await self.mcp_toolkit.disconnect()
313
+
314
+ async def astep(
315
+ self, input_message: Union[BaseMessage, str], *args, **kwargs
144
316
  ) -> ChatAgentResponse:
145
- r"""Run the agent to interact with the MCP tools.
317
+ r"""Asynchronous step function. Make sure MCP toolkit is connected
318
+ before proceeding.
146
319
 
147
320
  Args:
148
- prompt (str): The user's input prompt or query to be processed by
149
- the agent.
321
+ input_message (Union[BaseMessage, str]): The input message.
322
+ *args: Additional arguments.
323
+ **kwargs: Additional keyword arguments.
150
324
 
151
325
  Returns:
152
- ChatAgentResponse: The agent's response after processing the
153
- prompt and potentially executing MCP tool calls.
154
-
155
- Raises:
156
- RuntimeError: If the MCP server is not connected when attempting
157
- to run.
326
+ ChatAgentResponse: The response from the agent.
158
327
  """
328
+ if self.mcp_toolkit and not self.mcp_toolkit.is_connected():
329
+ await self.connect()
159
330
 
160
- if not self._mcp_toolkit.is_connected():
161
- raise RuntimeError("The MCP server is not connected")
162
-
163
- if self._function_calling_available:
164
- response = await self.astep(prompt)
165
- return response
331
+ if self.function_calling_available:
332
+ return await super().astep(input_message, *args, **kwargs)
166
333
  else:
167
- task = f"## Task:\n {prompt}"
168
- response = await self.astep(str(self._text_tools) + task)
334
+ task = f"## Task:\n {input_message}"
335
+ input_message = str(self._text_tools) + task
336
+ response = await super().astep(input_message, *args, **kwargs)
169
337
  content = response.msgs[0].content.lower()
170
338
 
171
339
  tool_calls = []
@@ -193,12 +361,45 @@ class MCPAgent(ChatAgent):
193
361
  else:
194
362
  tools_results = []
195
363
  for tool_call in tool_calls:
196
- server_idx = tool_call['server_idx']
197
- tool_name = tool_call['tool_name']
198
- tool_args = tool_call['tool_args']
199
- server = self._mcp_toolkit.servers[server_idx]
200
- result = await server.call_tool(tool_name, tool_args)
201
- tools_results.append({tool_name: result.content[0].text})
364
+ try:
365
+ server_idx = tool_call.get('server_idx')
366
+ tool_name = tool_call.get('tool_name')
367
+ tool_args = tool_call.get('tool_args', {})
368
+
369
+ # Validate required fields
370
+ if server_idx is None or tool_name is None:
371
+ logger.warning(
372
+ f"Missing required fields in tool "
373
+ f"call: {tool_call}"
374
+ )
375
+ continue
376
+
377
+ # Check server index is valid
378
+ if (
379
+ not isinstance(server_idx, int)
380
+ or server_idx < 0
381
+ or server_idx >= len(self.mcp_toolkit.servers)
382
+ ):
383
+ logger.warning(
384
+ f"Invalid server index: {server_idx}"
385
+ )
386
+ continue
387
+
388
+ server = self.mcp_toolkit.servers[server_idx]
389
+ result = await server.call_tool(tool_name, tool_args)
390
+
391
+ # Safely access content
392
+ if result.content and len(result.content) > 0:
393
+ tools_results.append(
394
+ {tool_name: result.content[0].text}
395
+ )
396
+ else:
397
+ tools_results.append(
398
+ {tool_name: "No result content available"}
399
+ )
400
+ except Exception as e:
401
+ logger.error(f"Error processing tool call: {e}")
402
+ tools_results.append({"error": str(e)})
202
403
  results = json.dumps(tools_results)
203
404
  final_prompt = TextPrompt(FINAL_RESPONSE_PROMPT).format(
204
405
  results=results
@@ -206,28 +407,40 @@ class MCPAgent(ChatAgent):
206
407
  response = await self.astep(final_prompt)
207
408
  return response
208
409
 
209
- @classmethod
210
- async def create(
211
- cls,
212
- config_path: str,
213
- model: Optional[BaseModelBackend] = None,
214
- function_calling_available: bool = False,
215
- ) -> "MCPAgent":
216
- r"""Factory method to create and initialize an MCPAgent.
410
+ def step(
411
+ self, input_message: Union[BaseMessage, str], *args, **kwargs
412
+ ) -> ChatAgentResponse:
413
+ r"""Synchronous step function. Make sure MCP toolkit is connected
414
+ before proceeding.
217
415
 
218
416
  Args:
219
- config_path (str): Path to the MCP configuration file that contains
220
- server settings and other configuration parameters.
221
- model (Optional[BaseModelBackend]): Model backend for the agent.
222
- If None, the default model will be used. (default: :obj:`None`)
223
- function_calling_available (bool): Flag indicating whether the
224
- model is equipped with function calling ability. This affects
225
- the system message content. (default: :obj:`False`)
417
+ input_message (Union[BaseMessage, str]): The input message.
418
+ *args: Additional arguments.
419
+ **kwargs: Additional keyword arguments.
226
420
 
227
421
  Returns:
228
- MCPAgent: A fully initialized MCPAgent instance ready for use.
422
+ ChatAgentResponse: The response from the agent.
229
423
  """
230
- agent = cls(config_path, model, function_calling_available)
231
- await agent.connect()
232
- agent.add_mcp_tools()
233
- return agent
424
+ try:
425
+ loop = asyncio.get_running_loop()
426
+ except RuntimeError:
427
+ loop = None
428
+
429
+ if loop and loop.is_running():
430
+ # Running inside an existing loop (e.g., Jupyter/FastAPI)
431
+ # Use create_task and run with a future
432
+ coro = self.astep(input_message, *args, **kwargs)
433
+ future = asyncio.ensure_future(coro)
434
+ return asyncio.run_coroutine_threadsafe(future, loop).result()
435
+ else:
436
+ # Safe to run normally
437
+ return asyncio.run(self.astep(input_message, *args, **kwargs))
438
+
439
+ async def __aenter__(self):
440
+ r"""Async context manager entry."""
441
+ await self.connect()
442
+ return self
443
+
444
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
445
+ r"""Async context manager exit."""
446
+ await self.disconnect()
@@ -15,12 +15,14 @@
15
15
  from .apibank import APIBankBenchmark
16
16
  from .apibench import APIBenchBenchmark
17
17
  from .base import BaseBenchmark
18
+ from .browsecomp import BrowseCompBenchmark
18
19
  from .gaia import DefaultGAIARetriever, GAIABenchmark
19
20
  from .nexus import NexusBenchmark
20
21
  from .ragbench import RAGBenchBenchmark
21
22
 
22
23
  __all__ = [
23
24
  "BaseBenchmark",
25
+ "BrowseCompBenchmark",
24
26
  "GAIABenchmark",
25
27
  "DefaultGAIARetriever",
26
28
  "NexusBenchmark",