camel-ai 0.2.56__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/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
- 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,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 docx, pdf, pptx, etc. It cannot
33
- process excel files.
32
+ This class provides method for processing excel files.
34
33
  """
35
34
 
36
35
  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`)
@@ -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`)
@@ -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
+ ]