camel-ai 0.2.57__py3-none-any.whl → 0.2.58__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 -2
- camel/agents/chat_agent.py +10 -0
- camel/agents/mcp_agent.py +296 -83
- camel/loaders/__init__.py +2 -0
- camel/loaders/markitdown.py +204 -0
- camel/models/openai_compatible_model.py +3 -3
- camel/societies/workforce/workforce.py +4 -4
- camel/toolkits/__init__.py +4 -0
- camel/toolkits/async_browser_toolkit.py +1800 -0
- camel/toolkits/base.py +10 -1
- camel/toolkits/browser_toolkit.py +19 -3
- camel/toolkits/excel_toolkit.py +1 -2
- camel/toolkits/function_tool.py +1 -1
- camel/toolkits/mcp_toolkit.py +129 -3
- camel/toolkits/search_toolkit.py +0 -169
- camel/toolkits/wolfram_alpha_toolkit.py +237 -0
- camel/types/__init__.py +10 -0
- camel/types/enums.py +3 -3
- camel/types/mcp_registries.py +157 -0
- {camel_ai-0.2.57.dist-info → camel_ai-0.2.58.dist-info}/METADATA +4 -2
- {camel_ai-0.2.57.dist-info → camel_ai-0.2.58.dist-info}/RECORD +23 -19
- {camel_ai-0.2.57.dist-info → camel_ai-0.2.58.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.57.dist-info → camel_ai-0.2.58.dist-info}/licenses/LICENSE +0 -0
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,12 @@ 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(self, mode: Literal["stdio", "sse"]) -> None:
|
|
57
|
+
r"""Run the MCP server in the specified mode.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
mode (Literal["stdio", "sse"]): The mode to run
|
|
61
|
+
the MCP server in.
|
|
62
|
+
"""
|
|
63
|
+
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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()
|
camel/toolkits/excel_toolkit.py
CHANGED
|
@@ -29,8 +29,7 @@ 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
|
|
33
|
-
process excel files.
|
|
32
|
+
This class provides method for processing excel files.
|
|
34
33
|
"""
|
|
35
34
|
|
|
36
35
|
def __init__(
|
camel/toolkits/function_tool.py
CHANGED
camel/toolkits/mcp_toolkit.py
CHANGED
|
@@ -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
|
|
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`)
|
|
@@ -328,8 +362,6 @@ class MCPClient(BaseToolkit):
|
|
|
328
362
|
"additionalProperties": False,
|
|
329
363
|
}
|
|
330
364
|
|
|
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
365
|
return {
|
|
334
366
|
"type": "function",
|
|
335
367
|
"function": {
|
|
@@ -395,6 +427,70 @@ class MCPClient(BaseToolkit):
|
|
|
395
427
|
def session(self) -> Optional["ClientSession"]:
|
|
396
428
|
return self._session
|
|
397
429
|
|
|
430
|
+
@classmethod
|
|
431
|
+
async def create(
|
|
432
|
+
cls,
|
|
433
|
+
command_or_url: str,
|
|
434
|
+
args: Optional[List[str]] = None,
|
|
435
|
+
env: Optional[Dict[str, str]] = None,
|
|
436
|
+
timeout: Optional[float] = None,
|
|
437
|
+
headers: Optional[Dict[str, str]] = None,
|
|
438
|
+
) -> "MCPClient":
|
|
439
|
+
r"""Factory method that creates and connects to the MCP server.
|
|
440
|
+
|
|
441
|
+
This async factory method ensures the connection to the MCP server is
|
|
442
|
+
established before the client object is fully constructed.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
command_or_url (str): URL for SSE mode or command executable
|
|
446
|
+
for stdio mode.
|
|
447
|
+
args (Optional[List[str]]): List of command-line arguments if
|
|
448
|
+
stdio mode is used. (default: :obj:`None`)
|
|
449
|
+
env (Optional[Dict[str, str]]): Environment variables for
|
|
450
|
+
the stdio mode command. (default: :obj:`None`)
|
|
451
|
+
timeout (Optional[float]): Connection timeout.
|
|
452
|
+
(default: :obj:`None`)
|
|
453
|
+
headers (Optional[Dict[str, str]]): Headers for the HTTP request.
|
|
454
|
+
(default: :obj:`None`)
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
MCPClient: A fully initialized and connected MCPClient instance.
|
|
458
|
+
|
|
459
|
+
Raises:
|
|
460
|
+
RuntimeError: If connection to the MCP server fails.
|
|
461
|
+
"""
|
|
462
|
+
client = cls(
|
|
463
|
+
command_or_url=command_or_url,
|
|
464
|
+
args=args,
|
|
465
|
+
env=env,
|
|
466
|
+
timeout=timeout,
|
|
467
|
+
headers=headers,
|
|
468
|
+
)
|
|
469
|
+
try:
|
|
470
|
+
await client.connect()
|
|
471
|
+
return client
|
|
472
|
+
except Exception as e:
|
|
473
|
+
# Ensure cleanup on initialization failure
|
|
474
|
+
await client.disconnect()
|
|
475
|
+
logger.error(f"Failed to initialize MCPClient: {e}")
|
|
476
|
+
raise RuntimeError(f"Failed to initialize MCPClient: {e}") from e
|
|
477
|
+
|
|
478
|
+
async def __aenter__(self) -> "MCPClient":
|
|
479
|
+
r"""Async context manager entry point. Automatically connects to the
|
|
480
|
+
MCP server when used in an async with statement.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
MCPClient: Self with active connection.
|
|
484
|
+
"""
|
|
485
|
+
await self.connect()
|
|
486
|
+
return self
|
|
487
|
+
|
|
488
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
489
|
+
r"""Async context manager exit point. Automatically disconnects from
|
|
490
|
+
the MCP server when exiting an async with statement.
|
|
491
|
+
"""
|
|
492
|
+
await self.disconnect()
|
|
493
|
+
|
|
398
494
|
|
|
399
495
|
class MCPToolkit(BaseToolkit):
|
|
400
496
|
r"""MCPToolkit provides a unified interface for managing multiple
|
|
@@ -404,6 +500,36 @@ class MCPToolkit(BaseToolkit):
|
|
|
404
500
|
offers a centralized configuration mechanism for both local and remote
|
|
405
501
|
MCP services.
|
|
406
502
|
|
|
503
|
+
Connection Lifecycle:
|
|
504
|
+
There are three ways to manage the connection lifecycle:
|
|
505
|
+
|
|
506
|
+
1. Using the async context manager:
|
|
507
|
+
```python
|
|
508
|
+
async with MCPToolkit(config_path="config.json") as toolkit:
|
|
509
|
+
# Toolkit is connected here
|
|
510
|
+
tools = toolkit.get_tools()
|
|
511
|
+
# Toolkit is automatically disconnected here
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
2. Using the factory method:
|
|
515
|
+
```python
|
|
516
|
+
toolkit = await MCPToolkit.create(config_path="config.json")
|
|
517
|
+
# Toolkit is connected here
|
|
518
|
+
tools = toolkit.get_tools()
|
|
519
|
+
# Don't forget to disconnect when done!
|
|
520
|
+
await toolkit.disconnect()
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
3. Using explicit connect/disconnect:
|
|
524
|
+
```python
|
|
525
|
+
toolkit = MCPToolkit(config_path="config.json")
|
|
526
|
+
await toolkit.connect()
|
|
527
|
+
# Toolkit is connected here
|
|
528
|
+
tools = toolkit.get_tools()
|
|
529
|
+
# Don't forget to disconnect when done!
|
|
530
|
+
await toolkit.disconnect()
|
|
531
|
+
```
|
|
532
|
+
|
|
407
533
|
Args:
|
|
408
534
|
servers (Optional[List[MCPClient]]): List of MCPClient
|
|
409
535
|
instances to manage. (default: :obj:`None`)
|
camel/toolkits/search_toolkit.py
CHANGED
|
@@ -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),
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
import os
|
|
15
|
+
import xml.etree.ElementTree as ET
|
|
16
|
+
from typing import Any, Dict, List, Union
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
|
|
20
|
+
from camel.toolkits.base import BaseToolkit
|
|
21
|
+
from camel.toolkits.function_tool import FunctionTool
|
|
22
|
+
from camel.utils import MCPServer, api_keys_required, dependencies_required
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@MCPServer()
|
|
26
|
+
class WolframAlphaToolkit(BaseToolkit):
|
|
27
|
+
r"""A class representing a toolkit for WolframAlpha.
|
|
28
|
+
|
|
29
|
+
Wolfram|Alpha is an answer engine developed by Wolfram Research.
|
|
30
|
+
It is offered as an online service that answers factual queries
|
|
31
|
+
by computing answers from externally sourced data.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@api_keys_required(
|
|
35
|
+
[
|
|
36
|
+
(None, "WOLFRAMALPHA_APP_ID"),
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
@dependencies_required("wolframalpha")
|
|
40
|
+
def query_wolfram_alpha(
|
|
41
|
+
self, query: str, is_detailed: bool = False
|
|
42
|
+
) -> Union[str, Dict[str, Any]]:
|
|
43
|
+
r"""Queries Wolfram|Alpha and returns the result.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
query (str): The query to send to Wolfram Alpha.
|
|
47
|
+
is_detailed (bool): Whether to include additional details
|
|
48
|
+
including step by step information in the result.
|
|
49
|
+
(default: :obj:`False`)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Union[str, Dict[str, Any]]: The result from Wolfram Alpha.
|
|
53
|
+
Returns a string if `is_detailed` is False, otherwise returns
|
|
54
|
+
a dictionary with detailed information.
|
|
55
|
+
"""
|
|
56
|
+
import wolframalpha
|
|
57
|
+
|
|
58
|
+
WOLFRAMALPHA_APP_ID = os.environ.get("WOLFRAMALPHA_APP_ID", "")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
client = wolframalpha.Client(WOLFRAMALPHA_APP_ID)
|
|
62
|
+
res = client.query(query)
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return f"Wolfram Alpha wasn't able to answer it. Error: {e}"
|
|
66
|
+
|
|
67
|
+
pased_result = self._parse_wolfram_result(res)
|
|
68
|
+
|
|
69
|
+
if is_detailed:
|
|
70
|
+
step_info = self._get_wolframalpha_step_by_step_solution(
|
|
71
|
+
WOLFRAMALPHA_APP_ID, query
|
|
72
|
+
)
|
|
73
|
+
pased_result["steps"] = step_info
|
|
74
|
+
return pased_result
|
|
75
|
+
|
|
76
|
+
return pased_result["final_answer"]
|
|
77
|
+
|
|
78
|
+
@api_keys_required(
|
|
79
|
+
[
|
|
80
|
+
(None, "WOLFRAMALPHA_APP_ID"),
|
|
81
|
+
]
|
|
82
|
+
)
|
|
83
|
+
def query_wolfram_llm(self, query: str) -> str:
|
|
84
|
+
r"""Queries Wolfram|Alpha LLM API and returns the result.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
query (str): The query to send to Wolfram Alpha LLM.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
str: The result from Wolfram Alpha as a string.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
WOLFRAMALPHA_APP_ID = os.environ.get("WOLFRAMALPHA_APP_ID", "")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
url = "https://www.wolframalpha.com/api/v1/llm-api"
|
|
97
|
+
params = {"input": query, "appid": WOLFRAMALPHA_APP_ID}
|
|
98
|
+
|
|
99
|
+
response = requests.get(url, params=params)
|
|
100
|
+
response.raise_for_status()
|
|
101
|
+
return response.text
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
return f"Wolfram Alpha wasn't able to answer it. Error: {e}"
|
|
105
|
+
|
|
106
|
+
def _parse_wolfram_result(self, result) -> Dict[str, Any]:
|
|
107
|
+
r"""Parses a Wolfram Alpha API result into a structured dictionary
|
|
108
|
+
format.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
result: The API result returned from a Wolfram Alpha
|
|
112
|
+
query, structured with multiple pods, each containing specific
|
|
113
|
+
information related to the query.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dict[str, Any]: A structured dictionary with the original query
|
|
117
|
+
and the final answer.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
# Extract the original query
|
|
121
|
+
query = result.get("@inputstring", "")
|
|
122
|
+
|
|
123
|
+
# Initialize a dictionary to hold structured output
|
|
124
|
+
output = {"query": query, "pod_info": [], "final_answer": None}
|
|
125
|
+
|
|
126
|
+
# Loop through each pod to extract the details
|
|
127
|
+
for pod in result.get("pod", []):
|
|
128
|
+
# Handle the case where subpod might be a list
|
|
129
|
+
subpod_data = pod.get("subpod", {})
|
|
130
|
+
if isinstance(subpod_data, list):
|
|
131
|
+
# If it's a list, get the first item for 'plaintext' and 'img'
|
|
132
|
+
description, image_url = next(
|
|
133
|
+
(
|
|
134
|
+
(data["plaintext"], data["img"])
|
|
135
|
+
for data in subpod_data
|
|
136
|
+
if "plaintext" in data and "img" in data
|
|
137
|
+
),
|
|
138
|
+
("", ""),
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
# Otherwise, handle it as a dictionary
|
|
142
|
+
description = subpod_data.get("plaintext", "")
|
|
143
|
+
image_url = subpod_data.get("img", {}).get("@src", "")
|
|
144
|
+
|
|
145
|
+
pod_info = {
|
|
146
|
+
"title": pod.get("@title", ""),
|
|
147
|
+
"description": description,
|
|
148
|
+
"image_url": image_url,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# For Results pod, collect all plaintext values from subpods
|
|
152
|
+
if pod.get("@title") == "Results":
|
|
153
|
+
results_text = []
|
|
154
|
+
if isinstance(subpod_data, list):
|
|
155
|
+
for subpod in subpod_data:
|
|
156
|
+
if subpod.get("plaintext"):
|
|
157
|
+
results_text.append(subpod["plaintext"])
|
|
158
|
+
else:
|
|
159
|
+
if description:
|
|
160
|
+
results_text.append(description)
|
|
161
|
+
pod_info["description"] = "\n".join(results_text)
|
|
162
|
+
|
|
163
|
+
# Add to steps list
|
|
164
|
+
output["pod_info"].append(pod_info)
|
|
165
|
+
|
|
166
|
+
# Get final answer
|
|
167
|
+
if pod.get("@primary", False):
|
|
168
|
+
output["final_answer"] = description
|
|
169
|
+
|
|
170
|
+
return output
|
|
171
|
+
|
|
172
|
+
def _get_wolframalpha_step_by_step_solution(
|
|
173
|
+
self, app_id: str, query: str
|
|
174
|
+
) -> dict:
|
|
175
|
+
r"""Retrieve a step-by-step solution from the Wolfram Alpha API for a
|
|
176
|
+
given query.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
app_id (str): Your Wolfram Alpha API application ID.
|
|
180
|
+
query (str): The mathematical or computational query to solve.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
dict: The step-by-step solution response text from the Wolfram
|
|
184
|
+
Alpha API.
|
|
185
|
+
"""
|
|
186
|
+
# Define the base URL
|
|
187
|
+
url = "https://api.wolframalpha.com/v2/query"
|
|
188
|
+
|
|
189
|
+
# Set up the query parameters
|
|
190
|
+
params = {
|
|
191
|
+
"appid": app_id,
|
|
192
|
+
"input": query,
|
|
193
|
+
"podstate": ["Result__Step-by-step solution", "Show all steps"],
|
|
194
|
+
"format": "plaintext",
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Send the request
|
|
198
|
+
response = requests.get(url, params=params)
|
|
199
|
+
root = ET.fromstring(response.text)
|
|
200
|
+
|
|
201
|
+
# Extracting step-by-step steps, including 'SBSStep' and 'SBSHintStep'
|
|
202
|
+
steps = []
|
|
203
|
+
# Find all subpods within the 'Results' pod
|
|
204
|
+
for subpod in root.findall(".//pod[@title='Results']//subpod"):
|
|
205
|
+
# Check if the subpod has the desired stepbystepcontenttype
|
|
206
|
+
content_type = subpod.find("stepbystepcontenttype")
|
|
207
|
+
if content_type is not None and content_type.text in [
|
|
208
|
+
"SBSStep",
|
|
209
|
+
"SBSHintStep",
|
|
210
|
+
]:
|
|
211
|
+
plaintext = subpod.find("plaintext")
|
|
212
|
+
if plaintext is not None and plaintext.text:
|
|
213
|
+
step_text = plaintext.text.strip()
|
|
214
|
+
cleaned_step = step_text.replace(
|
|
215
|
+
"Hint: |", ""
|
|
216
|
+
).strip() # Remove 'Hint: |' if present
|
|
217
|
+
steps.append(cleaned_step)
|
|
218
|
+
|
|
219
|
+
# Structuring the steps into a dictionary
|
|
220
|
+
structured_steps = {}
|
|
221
|
+
for i, step in enumerate(steps, start=1):
|
|
222
|
+
structured_steps[f"step{i}"] = step
|
|
223
|
+
|
|
224
|
+
return structured_steps
|
|
225
|
+
|
|
226
|
+
def get_tools(self) -> List[FunctionTool]:
|
|
227
|
+
r"""Returns a list of FunctionTool objects representing the
|
|
228
|
+
functions in the toolkit.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
List[FunctionTool]: A list of FunctionTool objects
|
|
232
|
+
representing the functions in the toolkit.
|
|
233
|
+
"""
|
|
234
|
+
return [
|
|
235
|
+
FunctionTool(self.query_wolfram_alpha),
|
|
236
|
+
FunctionTool(self.query_wolfram_llm),
|
|
237
|
+
]
|