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/toolkits/base.py CHANGED
@@ -12,7 +12,7 @@
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
 
15
- from typing import List, Optional
15
+ from typing import List, Literal, Optional
16
16
 
17
17
  from camel.toolkits import FunctionTool
18
18
  from camel.utils import AgentOpsMeta, with_timeout
@@ -52,3 +52,14 @@ class BaseToolkit(metaclass=AgentOpsMeta):
52
52
  representing the functions in the toolkit.
53
53
  """
54
54
  raise NotImplementedError("Subclasses must implement this method.")
55
+
56
+ def run_mcp_server(
57
+ self, mode: Literal["stdio", "sse", "streamable-http"]
58
+ ) -> None:
59
+ r"""Run the MCP server in the specified mode.
60
+
61
+ Args:
62
+ mode (Literal["stdio", "sse", "streamable-http"]): The mode to run
63
+ the MCP server in.
64
+ """
65
+ self.mcp.run(mode)
@@ -57,6 +57,7 @@ logger = get_logger(__name__)
57
57
 
58
58
  TOP_NO_LABEL_ZONE = 20
59
59
 
60
+
60
61
  AVAILABLE_ACTIONS_PROMPT = """
61
62
  1. `fill_input_id(identifier: Union[str, int], text: str)`: Fill an input
62
63
  field (e.g. search box) with the given text and press Enter.
@@ -523,6 +524,7 @@ class BaseBrowser:
523
524
  self.page.mouse.click(0, 0)
524
525
  self._wait_for_load()
525
526
 
527
+ @retry_on_error()
526
528
  def visit_page(self, url: str) -> None:
527
529
  r"""Visit a page with the given URL."""
528
530
 
@@ -540,10 +542,24 @@ class BaseBrowser:
540
542
  Returns:
541
543
  str: The answer to the question.
542
544
  """
543
- video_analyzer = VideoAnalysisToolkit()
544
- result = video_analyzer.ask_question_about_video(
545
- self.page_url, question
545
+ current_url = self.get_url()
546
+
547
+ # Confirm with user before proceeding due to potential slow processing time
548
+ confirmation_message = (
549
+ f"Do you want to analyze the video on the current "
550
+ f"page({current_url})? This operation may take a long time.(y/n): "
546
551
  )
552
+ user_confirmation = input(confirmation_message)
553
+
554
+ if user_confirmation.lower() not in ['y', 'yes']:
555
+ return "User cancelled the video analysis."
556
+
557
+ model = None
558
+ if hasattr(self, 'web_agent_model'):
559
+ model = self.web_agent_model
560
+
561
+ video_analyzer = VideoAnalysisToolkit(model=model)
562
+ result = video_analyzer.ask_question_about_video(current_url, question)
547
563
  return result
548
564
 
549
565
  @retry_on_error()
@@ -29,8 +29,9 @@ class ExcelToolkit(BaseToolkit):
29
29
  r"""A class representing a toolkit for extract detailed cell information
30
30
  from an Excel file.
31
31
 
32
- This class provides method for processing docx, pdf, pptx, etc. It cannot
33
- process excel files.
32
+ This class provides methods extracting detailed content from Excel files
33
+ (including .xls, .xlsx,.csv), and converting the data into
34
+ Markdown formatted table.
34
35
  """
35
36
 
36
37
  def __init__(
@@ -409,7 +409,7 @@ class FunctionTool:
409
409
 
410
410
  @property
411
411
  def is_async(self) -> bool:
412
- return inspect.iscoroutinefunction(self.func)
412
+ return inspect.iscoroutinefunction(inspect.unwrap(self.func))
413
413
 
414
414
  @staticmethod
415
415
  def validate_openai_tool_schema(
@@ -42,7 +42,7 @@ logger = get_logger(__name__)
42
42
 
43
43
  class MCPClient(BaseToolkit):
44
44
  r"""Internal class that provides an abstraction layer to interact with
45
- external tools using the Model Context Protocol (MCP). It supports two
45
+ external tools using the Model Context Protocol (MCP). It supports three
46
46
  modes of connection:
47
47
 
48
48
  1. stdio mode: Connects via standard input/output streams for local
@@ -51,6 +51,40 @@ class MCPClient(BaseToolkit):
51
51
  2. SSE mode (HTTP Server-Sent Events): Connects via HTTP for persistent,
52
52
  event-based interactions.
53
53
 
54
+ 3. streamable-http mode: Connects via HTTP for persistent, streamable
55
+ interactions.
56
+
57
+ Connection Lifecycle:
58
+ There are three ways to manage the connection lifecycle:
59
+
60
+ 1. Using the async context manager:
61
+ ```python
62
+ async with MCPClient(command_or_url="...") as client:
63
+ # Client is connected here
64
+ result = await client.some_tool()
65
+ # Client is automatically disconnected here
66
+ ```
67
+
68
+ 2. Using the factory method:
69
+ ```python
70
+ client = await MCPClient.create(command_or_url="...")
71
+ # Client is connected here
72
+ result = await client.some_tool()
73
+ # Don't forget to disconnect when done!
74
+ await client.disconnect()
75
+ ```
76
+
77
+ 3. Using explicit connect/disconnect:
78
+ ```python
79
+ client = MCPClient(command_or_url="...")
80
+ await client.connect()
81
+ # Client is connected here
82
+ result = await client.some_tool()
83
+ # Don't forget to disconnect when done!
84
+ await client.disconnect()
85
+ ```
86
+
87
+
54
88
  Attributes:
55
89
  command_or_url (str): URL for SSE mode or command executable for stdio
56
90
  mode. (default: :obj:`None`)
@@ -160,8 +194,13 @@ class MCPClient(BaseToolkit):
160
194
 
161
195
  async def disconnect(self):
162
196
  r"""Explicitly disconnect from the MCP server."""
197
+ # If the server is not connected, do nothing
198
+ if not self._is_connected:
199
+ return
163
200
  self._is_connected = False
164
201
  await self._exit_stack.aclose()
202
+ # Reset the exit stack and session for future reuse purposes
203
+ self._exit_stack = AsyncExitStack()
165
204
  self._session = None
166
205
 
167
206
  @asynccontextmanager
@@ -328,8 +367,6 @@ class MCPClient(BaseToolkit):
328
367
  "additionalProperties": False,
329
368
  }
330
369
 
331
- # Because certain parameters in MCP may include keywords that are not
332
- # supported by OpenAI, it is essential to set "strict" to False.
333
370
  return {
334
371
  "type": "function",
335
372
  "function": {
@@ -395,6 +432,70 @@ class MCPClient(BaseToolkit):
395
432
  def session(self) -> Optional["ClientSession"]:
396
433
  return self._session
397
434
 
435
+ @classmethod
436
+ async def create(
437
+ cls,
438
+ command_or_url: str,
439
+ args: Optional[List[str]] = None,
440
+ env: Optional[Dict[str, str]] = None,
441
+ timeout: Optional[float] = None,
442
+ headers: Optional[Dict[str, str]] = None,
443
+ ) -> "MCPClient":
444
+ r"""Factory method that creates and connects to the MCP server.
445
+
446
+ This async factory method ensures the connection to the MCP server is
447
+ established before the client object is fully constructed.
448
+
449
+ Args:
450
+ command_or_url (str): URL for SSE mode or command executable
451
+ for stdio mode.
452
+ args (Optional[List[str]]): List of command-line arguments if
453
+ stdio mode is used. (default: :obj:`None`)
454
+ env (Optional[Dict[str, str]]): Environment variables for
455
+ the stdio mode command. (default: :obj:`None`)
456
+ timeout (Optional[float]): Connection timeout.
457
+ (default: :obj:`None`)
458
+ headers (Optional[Dict[str, str]]): Headers for the HTTP request.
459
+ (default: :obj:`None`)
460
+
461
+ Returns:
462
+ MCPClient: A fully initialized and connected MCPClient instance.
463
+
464
+ Raises:
465
+ RuntimeError: If connection to the MCP server fails.
466
+ """
467
+ client = cls(
468
+ command_or_url=command_or_url,
469
+ args=args,
470
+ env=env,
471
+ timeout=timeout,
472
+ headers=headers,
473
+ )
474
+ try:
475
+ await client.connect()
476
+ return client
477
+ except Exception as e:
478
+ # Ensure cleanup on initialization failure
479
+ await client.disconnect()
480
+ logger.error(f"Failed to initialize MCPClient: {e}")
481
+ raise RuntimeError(f"Failed to initialize MCPClient: {e}") from e
482
+
483
+ async def __aenter__(self) -> "MCPClient":
484
+ r"""Async context manager entry point. Automatically connects to the
485
+ MCP server when used in an async with statement.
486
+
487
+ Returns:
488
+ MCPClient: Self with active connection.
489
+ """
490
+ await self.connect()
491
+ return self
492
+
493
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
494
+ r"""Async context manager exit point. Automatically disconnects from
495
+ the MCP server when exiting an async with statement.
496
+ """
497
+ await self.disconnect()
498
+
398
499
 
399
500
  class MCPToolkit(BaseToolkit):
400
501
  r"""MCPToolkit provides a unified interface for managing multiple
@@ -404,6 +505,36 @@ class MCPToolkit(BaseToolkit):
404
505
  offers a centralized configuration mechanism for both local and remote
405
506
  MCP services.
406
507
 
508
+ Connection Lifecycle:
509
+ There are three ways to manage the connection lifecycle:
510
+
511
+ 1. Using the async context manager:
512
+ ```python
513
+ async with MCPToolkit(config_path="config.json") as toolkit:
514
+ # Toolkit is connected here
515
+ tools = toolkit.get_tools()
516
+ # Toolkit is automatically disconnected here
517
+ ```
518
+
519
+ 2. Using the factory method:
520
+ ```python
521
+ toolkit = await MCPToolkit.create(config_path="config.json")
522
+ # Toolkit is connected here
523
+ tools = toolkit.get_tools()
524
+ # Don't forget to disconnect when done!
525
+ await toolkit.disconnect()
526
+ ```
527
+
528
+ 3. Using explicit connect/disconnect:
529
+ ```python
530
+ toolkit = MCPToolkit(config_path="config.json")
531
+ await toolkit.connect()
532
+ # Toolkit is connected here
533
+ tools = toolkit.get_tools()
534
+ # Don't forget to disconnect when done!
535
+ await toolkit.disconnect()
536
+ ```
537
+
407
538
  Args:
408
539
  servers (Optional[List[MCPClient]]): List of MCPClient
409
540
  instances to manage. (default: :obj:`None`)
@@ -473,7 +604,6 @@ class MCPToolkit(BaseToolkit):
473
604
  if config_dict:
474
605
  self.servers.extend(self._load_servers_from_dict(config_dict))
475
606
 
476
- self._exit_stack = AsyncExitStack()
477
607
  self._connected = False
478
608
 
479
609
  def _load_servers_from_config(
@@ -565,7 +695,6 @@ class MCPToolkit(BaseToolkit):
565
695
  logger.warning("MCPToolkit is already connected")
566
696
  return self
567
697
 
568
- self._exit_stack = AsyncExitStack()
569
698
  try:
570
699
  # Sequentially connect to each server
571
700
  for server in self.servers:
@@ -586,7 +715,6 @@ class MCPToolkit(BaseToolkit):
586
715
  for server in self.servers:
587
716
  await server.disconnect()
588
717
  self._connected = False
589
- await self._exit_stack.aclose()
590
718
 
591
719
  @asynccontextmanager
592
720
  async def connection(self) -> AsyncGenerator["MCPToolkit", None]:
@@ -12,7 +12,6 @@
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
  import os
15
- import xml.etree.ElementTree as ET
16
15
  from typing import Any, Dict, List, Literal, Optional, TypeAlias, Union, cast
17
16
 
18
17
  import requests
@@ -489,173 +488,6 @@ class SearchToolkit(BaseToolkit):
489
488
  # If no answer found, return an empty list
490
489
  return responses
491
490
 
492
- @dependencies_required("wolframalpha")
493
- def query_wolfram_alpha(
494
- self, query: str, is_detailed: bool = False
495
- ) -> Union[str, Dict[str, Any]]:
496
- r"""Queries Wolfram|Alpha and returns the result. Wolfram|Alpha is an
497
- answer engine developed by Wolfram Research. It is offered as an online
498
- service that answers factual queries by computing answers from
499
- externally sourced data.
500
-
501
- Args:
502
- query (str): The query to send to Wolfram Alpha.
503
- is_detailed (bool): Whether to include additional details
504
- including step by step information in the result.
505
- (default: :obj:`False`)
506
-
507
- Returns:
508
- Union[str, Dict[str, Any]]: The result from Wolfram Alpha.
509
- Returns a string if `is_detailed` is False, otherwise returns
510
- a dictionary with detailed information.
511
- """
512
- import wolframalpha
513
-
514
- WOLFRAMALPHA_APP_ID = os.environ.get("WOLFRAMALPHA_APP_ID")
515
- if not WOLFRAMALPHA_APP_ID:
516
- raise ValueError(
517
- "`WOLFRAMALPHA_APP_ID` not found in environment "
518
- "variables. Get `WOLFRAMALPHA_APP_ID` here: `https://products.wolframalpha.com/api/`."
519
- )
520
-
521
- try:
522
- client = wolframalpha.Client(WOLFRAMALPHA_APP_ID)
523
- res = client.query(query)
524
-
525
- except Exception as e:
526
- return f"Wolfram Alpha wasn't able to answer it. Error: {e}"
527
-
528
- pased_result = self._parse_wolfram_result(res)
529
-
530
- if is_detailed:
531
- step_info = self._get_wolframalpha_step_by_step_solution(
532
- WOLFRAMALPHA_APP_ID, query
533
- )
534
- pased_result["steps"] = step_info
535
- return pased_result
536
-
537
- return pased_result["final_answer"]
538
-
539
- def _parse_wolfram_result(self, result) -> Dict[str, Any]:
540
- r"""Parses a Wolfram Alpha API result into a structured dictionary
541
- format.
542
-
543
- Args:
544
- result: The API result returned from a Wolfram Alpha
545
- query, structured with multiple pods, each containing specific
546
- information related to the query.
547
-
548
- Returns:
549
- dict: A structured dictionary with the original query and the
550
- final answer.
551
- """
552
-
553
- # Extract the original query
554
- query = result.get("@inputstring", "")
555
-
556
- # Initialize a dictionary to hold structured output
557
- output = {"query": query, "pod_info": [], "final_answer": None}
558
-
559
- # Loop through each pod to extract the details
560
- for pod in result.get("pod", []):
561
- # Handle the case where subpod might be a list
562
- subpod_data = pod.get("subpod", {})
563
- if isinstance(subpod_data, list):
564
- # If it's a list, get the first item for 'plaintext' and 'img'
565
- description, image_url = next(
566
- (
567
- (data["plaintext"], data["img"])
568
- for data in subpod_data
569
- if "plaintext" in data and "img" in data
570
- ),
571
- ("", ""),
572
- )
573
- else:
574
- # Otherwise, handle it as a dictionary
575
- description = subpod_data.get("plaintext", "")
576
- image_url = subpod_data.get("img", {}).get("@src", "")
577
-
578
- pod_info = {
579
- "title": pod.get("@title", ""),
580
- "description": description,
581
- "image_url": image_url,
582
- }
583
-
584
- # For Results pod, collect all plaintext values from subpods
585
- if pod.get("@title") == "Results":
586
- results_text = []
587
- if isinstance(subpod_data, list):
588
- for subpod in subpod_data:
589
- if subpod.get("plaintext"):
590
- results_text.append(subpod["plaintext"])
591
- else:
592
- if description:
593
- results_text.append(description)
594
- pod_info["description"] = "\n".join(results_text)
595
-
596
- # Add to steps list
597
- output["pod_info"].append(pod_info)
598
-
599
- # Get final answer
600
- if pod.get("@primary", False):
601
- output["final_answer"] = description
602
-
603
- return output
604
-
605
- def _get_wolframalpha_step_by_step_solution(
606
- self, app_id: str, query: str
607
- ) -> dict:
608
- r"""Retrieve a step-by-step solution from the Wolfram Alpha API for a
609
- given query.
610
-
611
- Args:
612
- app_id (str): Your Wolfram Alpha API application ID.
613
- query (str): The mathematical or computational query to solve.
614
-
615
- Returns:
616
- dict: The step-by-step solution response text from the Wolfram
617
- Alpha API.
618
- """
619
- # Define the base URL
620
- url = "https://api.wolframalpha.com/v2/query"
621
-
622
- # Set up the query parameters
623
- params = {
624
- "appid": app_id,
625
- "input": query,
626
- "podstate": ["Result__Step-by-step solution", "Show all steps"],
627
- "format": "plaintext",
628
- }
629
-
630
- # Send the request
631
- response = requests.get(url, params=params)
632
- root = ET.fromstring(response.text)
633
-
634
- # Extracting step-by-step steps, including 'SBSStep' and 'SBSHintStep'
635
- steps = []
636
- # Find all subpods within the 'Results' pod
637
- for subpod in root.findall(".//pod[@title='Results']//subpod"):
638
- # Check if the subpod has the desired stepbystepcontenttype
639
- content_type = subpod.find("stepbystepcontenttype")
640
- if content_type is not None and content_type.text in [
641
- "SBSStep",
642
- "SBSHintStep",
643
- ]:
644
- plaintext = subpod.find("plaintext")
645
- if plaintext is not None and plaintext.text:
646
- step_text = plaintext.text.strip()
647
- cleaned_step = step_text.replace(
648
- "Hint: |", ""
649
- ).strip() # Remove 'Hint: |' if present
650
- steps.append(cleaned_step)
651
-
652
- # Structuring the steps into a dictionary
653
- structured_steps = {}
654
- for i, step in enumerate(steps, start=1):
655
- structured_steps[f"step{i}"] = step
656
-
657
- return structured_steps
658
-
659
491
  def tavily_search(
660
492
  self, query: str, num_results: int = 5, **kwargs
661
493
  ) -> List[Dict[str, Any]]:
@@ -1243,7 +1075,6 @@ class SearchToolkit(BaseToolkit):
1243
1075
  FunctionTool(self.search_linkup),
1244
1076
  FunctionTool(self.search_google),
1245
1077
  FunctionTool(self.search_duckduckgo),
1246
- FunctionTool(self.query_wolfram_alpha),
1247
1078
  FunctionTool(self.tavily_search),
1248
1079
  FunctionTool(self.search_brave),
1249
1080
  FunctionTool(self.search_bocha),