camel-ai 0.2.62__py3-none-any.whl → 0.2.65__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.

Files changed (59) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +95 -24
  3. camel/agents/mcp_agent.py +5 -1
  4. camel/benchmarks/mock_website/README.md +96 -0
  5. camel/benchmarks/mock_website/mock_web.py +299 -0
  6. camel/benchmarks/mock_website/requirements.txt +3 -0
  7. camel/benchmarks/mock_website/shopping_mall/app.py +465 -0
  8. camel/benchmarks/mock_website/task.json +104 -0
  9. camel/configs/__init__.py +3 -0
  10. camel/configs/crynux_config.py +94 -0
  11. camel/datasets/models.py +1 -1
  12. camel/datasets/static_dataset.py +6 -0
  13. camel/interpreters/base.py +14 -1
  14. camel/interpreters/docker/Dockerfile +63 -7
  15. camel/interpreters/docker_interpreter.py +65 -7
  16. camel/interpreters/e2b_interpreter.py +23 -8
  17. camel/interpreters/internal_python_interpreter.py +30 -2
  18. camel/interpreters/ipython_interpreter.py +21 -3
  19. camel/interpreters/subprocess_interpreter.py +34 -2
  20. camel/memories/records.py +5 -3
  21. camel/models/__init__.py +2 -0
  22. camel/models/azure_openai_model.py +101 -25
  23. camel/models/cohere_model.py +65 -0
  24. camel/models/crynux_model.py +94 -0
  25. camel/models/deepseek_model.py +43 -1
  26. camel/models/gemini_model.py +50 -4
  27. camel/models/litellm_model.py +38 -0
  28. camel/models/mistral_model.py +66 -0
  29. camel/models/model_factory.py +10 -1
  30. camel/models/openai_compatible_model.py +81 -17
  31. camel/models/openai_model.py +87 -16
  32. camel/models/reka_model.py +69 -0
  33. camel/models/samba_model.py +69 -2
  34. camel/models/sglang_model.py +74 -2
  35. camel/models/watsonx_model.py +62 -0
  36. camel/societies/workforce/role_playing_worker.py +11 -3
  37. camel/societies/workforce/single_agent_worker.py +31 -1
  38. camel/societies/workforce/utils.py +51 -0
  39. camel/societies/workforce/workforce.py +409 -7
  40. camel/storages/__init__.py +2 -0
  41. camel/storages/vectordb_storages/__init__.py +2 -0
  42. camel/storages/vectordb_storages/weaviate.py +714 -0
  43. camel/tasks/task.py +27 -10
  44. camel/toolkits/async_browser_toolkit.py +97 -54
  45. camel/toolkits/browser_toolkit.py +65 -18
  46. camel/toolkits/code_execution.py +37 -8
  47. camel/toolkits/function_tool.py +2 -2
  48. camel/toolkits/mcp_toolkit.py +13 -2
  49. camel/toolkits/playwright_mcp_toolkit.py +16 -3
  50. camel/toolkits/task_planning_toolkit.py +134 -0
  51. camel/types/enums.py +61 -2
  52. camel/types/unified_model_type.py +5 -0
  53. camel/utils/__init__.py +16 -0
  54. camel/utils/langfuse.py +258 -0
  55. camel/utils/mcp_client.py +84 -17
  56. {camel_ai-0.2.62.dist-info → camel_ai-0.2.65.dist-info}/METADATA +9 -12
  57. {camel_ai-0.2.62.dist-info → camel_ai-0.2.65.dist-info}/RECORD +59 -49
  58. {camel_ai-0.2.62.dist-info → camel_ai-0.2.65.dist-info}/WHEEL +0 -0
  59. {camel_ai-0.2.62.dist-info → camel_ai-0.2.65.dist-info}/licenses/LICENSE +0 -0
camel/tasks/task.py CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  import re
16
16
  from enum import Enum
17
- from typing import Callable, Dict, List, Literal, Optional, Union
17
+ from typing import Any, Callable, Dict, List, Literal, Optional, Union
18
18
 
19
19
  from pydantic import BaseModel
20
20
 
@@ -69,14 +69,23 @@ class Task(BaseModel):
69
69
  r"""Task is specific assignment that can be passed to a agent.
70
70
 
71
71
  Attributes:
72
- content: string content for task.
73
- id: An unique string identifier for the task. This should
74
- ideally be provided by the provider/model which created the task.
75
- state: The state which should be OPEN, RUNNING, DONE or DELETED.
76
- type: task type
77
- parent: The parent task, None for root task.
78
- subtasks: The childrent sub-tasks for the task.
79
- result: The answer for the task.
72
+ content (str): string content for task.
73
+ id (str): An unique string identifier for the task. This should
74
+ ideally be provided by the provider/model which created the task.
75
+ (default: :obj: `""`)
76
+ state (TaskState): The state which should be OPEN, RUNNING, DONE or
77
+ DELETED. (default: :obj: `TaskState.OPEN`)
78
+ type (Optional[str]): task type. (default: :obj: `None`)
79
+ parent (Optional[Task]): The parent task, None for root task.
80
+ (default: :obj: `None`)
81
+ subtasks (List[Task]): The childrent sub-tasks for the task.
82
+ (default: :obj: `[]`)
83
+ result (Optional[str]): The answer for the task.
84
+ (default: :obj: `""`)
85
+ failure_count (int): The failure count for the task.
86
+ (default: :obj: `0`)
87
+ additional_info (Optional[Dict[str, Any]]): Additional information for
88
+ the task. (default: :obj: `None`)
80
89
  """
81
90
 
82
91
  content: str
@@ -95,7 +104,15 @@ class Task(BaseModel):
95
104
 
96
105
  failure_count: int = 0
97
106
 
98
- additional_info: Optional[str] = None
107
+ additional_info: Optional[Dict[str, Any]] = None
108
+
109
+ def __repr__(self) -> str:
110
+ r"""Return a string representation of the task."""
111
+ content_preview = self.content
112
+ return (
113
+ f"Task(id='{self.id}', content='{content_preview}', "
114
+ f"state='{self.state.value}')"
115
+ )
99
116
 
100
117
  @classmethod
101
118
  def from_message(cls, message: BaseMessage) -> "Task":
@@ -123,6 +123,7 @@ class AsyncBaseBrowser:
123
123
  cache_dir: Optional[str] = None,
124
124
  channel: Literal["chrome", "msedge", "chromium"] = "chromium",
125
125
  cookie_json_path: Optional[str] = None,
126
+ user_data_dir: Optional[str] = None,
126
127
  ):
127
128
  r"""
128
129
  Initialize the asynchronous browser core.
@@ -136,7 +137,11 @@ class AsyncBaseBrowser:
136
137
  cookie_json_path (Optional[str]): Path to a JSON file containing
137
138
  authentication cookies and browser storage state. If provided
138
139
  and the file exists, the browser will load this state to
139
- maintain authenticated sessions without requiring manual login.
140
+ maintain authenticated sessions. This is primarily used when
141
+ `user_data_dir` is not set.
142
+ user_data_dir (Optional[str]): The directory to store user data
143
+ for persistent context. If None, a fresh browser instance
144
+ is used without saving data. (default: :obj:`None`)
140
145
 
141
146
  Returns:
142
147
  None
@@ -151,6 +156,7 @@ class AsyncBaseBrowser:
151
156
  self.playwright = async_playwright()
152
157
  self.page_history: list[Any] = []
153
158
  self.cookie_json_path = cookie_json_path
159
+ self.user_data_dir = user_data_dir
154
160
  self.playwright_server: Any = None
155
161
  self.playwright_started: bool = False
156
162
  self.browser: Any = None
@@ -163,6 +169,10 @@ class AsyncBaseBrowser:
163
169
  self.cache_dir = "tmp/" if cache_dir is None else cache_dir
164
170
  os.makedirs(self.cache_dir, exist_ok=True)
165
171
 
172
+ # Create user data directory only if specified
173
+ if self.user_data_dir:
174
+ os.makedirs(self.user_data_dir, exist_ok=True)
175
+
166
176
  # Load the page script
167
177
  abs_dir_path = os.path.dirname(os.path.abspath(__file__))
168
178
  page_script_path = os.path.join(abs_dir_path, "page_script.js")
@@ -183,23 +193,56 @@ class AsyncBaseBrowser:
183
193
  await self._ensure_browser_installed()
184
194
  self.playwright_server = await self.playwright.start()
185
195
  self.playwright_started = True
186
- # Launch the browser asynchronously.
187
- self.browser = await self.playwright_server.chromium.launch(
188
- headless=self.headless, channel=self.channel
196
+
197
+ browser_launch_args = [
198
+ "--disable-blink-features=AutomationControlled", # Basic stealth
199
+ ]
200
+
201
+ user_agent_string = (
202
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
203
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
204
+ "Chrome/91.0.4472.124 Safari/537.36"
189
205
  )
190
- # Check if cookie file exists before using it to maintain
191
- # authenticated sessions. This prevents errors when the cookie file
192
- # doesn't exist
193
- if self.cookie_json_path and os.path.exists(self.cookie_json_path):
194
- self.context = await self.browser.new_context(
195
- accept_downloads=True, storage_state=self.cookie_json_path
206
+
207
+ if self.user_data_dir:
208
+ self.context = await (
209
+ self.playwright_server.chromium.launch_persistent_context(
210
+ user_data_dir=self.user_data_dir,
211
+ headless=self.headless,
212
+ channel=self.channel,
213
+ accept_downloads=True,
214
+ user_agent=user_agent_string,
215
+ java_script_enabled=True,
216
+ args=browser_launch_args,
217
+ )
196
218
  )
219
+ self.browser = None # Not using a separate browser instance
220
+ if len(self.context.pages) > 0: # Persistent context might
221
+ # reopen pages
222
+ self.page = self.context.pages[0]
223
+ else:
224
+ self.page = await self.context.new_page()
197
225
  else:
198
- self.context = await self.browser.new_context(
199
- accept_downloads=True,
226
+ # Launch a fresh browser instance
227
+ self.browser = await self.playwright_server.chromium.launch(
228
+ headless=self.headless,
229
+ channel=self.channel,
230
+ args=browser_launch_args,
200
231
  )
201
- # Create a new page asynchronously.
202
- self.page = await self.context.new_page()
232
+
233
+ new_context_kwargs: Dict[str, Any] = {
234
+ "accept_downloads": True,
235
+ "user_agent": user_agent_string,
236
+ "java_script_enabled": True,
237
+ }
238
+ if self.cookie_json_path and os.path.exists(self.cookie_json_path):
239
+ new_context_kwargs["storage_state"] = self.cookie_json_path
240
+
241
+ self.context = await self.browser.new_context(**new_context_kwargs)
242
+ self.page = await self.context.new_page()
243
+
244
+ assert self.context is not None
245
+ assert self.page is not None
203
246
 
204
247
  def init(self) -> Coroutine[Any, Any, None]:
205
248
  r"""Initialize the browser asynchronously."""
@@ -827,7 +870,14 @@ class AsyncBaseBrowser:
827
870
 
828
871
  async def async_close(self) -> None:
829
872
  r"""Asynchronously close the browser."""
830
- await self.browser.close()
873
+ if self.context is not None:
874
+ await self.context.close()
875
+ if self.browser is not None: # Only close browser if it was
876
+ # launched separately
877
+ await self.browser.close()
878
+ if self.playwright_server and self.playwright_started:
879
+ await self.playwright_server.stop()
880
+ self.playwright_started = False
831
881
 
832
882
  def close(self) -> Coroutine[Any, Any, None]:
833
883
  r"""Close the browser."""
@@ -943,6 +993,7 @@ class AsyncBrowserToolkit(BaseToolkit):
943
993
  planning_agent_model: Optional[BaseModelBackend] = None,
944
994
  output_language: str = "en",
945
995
  cookie_json_path: Optional[str] = None,
996
+ user_data_dir: Optional[str] = None,
946
997
  ):
947
998
  r"""Initialize the BrowserToolkit instance.
948
999
 
@@ -966,6 +1017,8 @@ class AsyncBrowserToolkit(BaseToolkit):
966
1017
  maintain authenticated sessions without requiring manual
967
1018
  login.
968
1019
  (default: :obj:`None`)
1020
+ user_data_dir (Optional[str]): The directory to store user data
1021
+ for persistent context. (default: :obj:`"user_data_dir/"`)
969
1022
  """
970
1023
  super().__init__()
971
1024
  self.browser = AsyncBaseBrowser(
@@ -973,6 +1026,7 @@ class AsyncBrowserToolkit(BaseToolkit):
973
1026
  cache_dir=cache_dir,
974
1027
  channel=channel,
975
1028
  cookie_json_path=cookie_json_path,
1029
+ user_data_dir=user_data_dir,
976
1030
  )
977
1031
 
978
1032
  self.history_window = history_window
@@ -991,7 +1045,7 @@ class AsyncBrowserToolkit(BaseToolkit):
991
1045
  os.makedirs(self.browser.cache_dir, exist_ok=True)
992
1046
 
993
1047
  def _initialize_agent(self) -> Tuple["ChatAgent", "ChatAgent"]:
994
- r"""Initialize the agent."""
1048
+ r"""Initialize the planning and web agents."""
995
1049
  from camel.agents.chat_agent import ChatAgent
996
1050
 
997
1051
  if self.web_agent_model is None:
@@ -1060,7 +1114,7 @@ Here is a plan about how to solve the task step-by-step which you must follow:
1060
1114
  )
1061
1115
  # Reset the history message of web_agent.
1062
1116
  self.web_agent.reset()
1063
- resp = self.web_agent.step(message)
1117
+ resp = await self.web_agent.astep(message)
1064
1118
 
1065
1119
  resp_content = resp.msgs[0].content
1066
1120
 
@@ -1196,43 +1250,29 @@ Here is a plan about how to solve the task step-by-step which you must follow:
1196
1250
  f"correct identifier.",
1197
1251
  )
1198
1252
 
1199
- def _get_final_answer(self, task_prompt: str) -> str:
1200
- r"""Get the final answer based on the task prompt and current browser
1201
- state. It is used when the agent thinks that the task can be completed
1202
- without any further action, and answer can be directly found in the
1203
- current viewport.
1204
- """
1205
-
1206
- prompt = GET_FINAL_ANSWER_PROMPT_TEMPLATE.format(
1207
- history=self.history, task_prompt=task_prompt
1208
- )
1209
-
1210
- message = BaseMessage.make_user_message(
1211
- role_name='user',
1212
- content=prompt,
1253
+ async def _async_get_final_answer(self, task_prompt: str) -> str:
1254
+ r"""Generate the final answer based on the task prompt."""
1255
+ final_answer_prompt = GET_FINAL_ANSWER_PROMPT_TEMPLATE.format(
1256
+ task_prompt=task_prompt, history=self.history
1213
1257
  )
1258
+ response = await self.planning_agent.astep(final_answer_prompt)
1259
+ if response.msgs is None or len(response.msgs) == 0:
1260
+ raise RuntimeError("Got empty final answer from planning agent.")
1261
+ return response.msgs[0].content
1214
1262
 
1215
- resp = self.web_agent.step(message)
1216
- return resp.msgs[0].content
1217
-
1218
- def _task_planning(self, task_prompt: str, start_url: str) -> str:
1219
- r"""Plan the task based on the given task prompt."""
1220
-
1221
- # Here are the available browser functions we can
1222
- # use: {AVAILABLE_ACTIONS_PROMPT}
1223
-
1263
+ async def _async_task_planning(
1264
+ self, task_prompt: str, start_url: str
1265
+ ) -> str:
1266
+ r"""Generate a detailed plan for the given task."""
1224
1267
  planning_prompt = TASK_PLANNING_PROMPT_TEMPLATE.format(
1225
1268
  task_prompt=task_prompt, start_url=start_url
1226
1269
  )
1270
+ response = await self.planning_agent.astep(planning_prompt)
1271
+ if response.msgs is None or len(response.msgs) == 0:
1272
+ raise RuntimeError("Got empty plan from planning agent.")
1273
+ return response.msgs[0].content
1227
1274
 
1228
- message = BaseMessage.make_user_message(
1229
- role_name='user', content=planning_prompt
1230
- )
1231
-
1232
- resp = self.planning_agent.step(message)
1233
- return resp.msgs[0].content
1234
-
1235
- def _task_replanning(
1275
+ async def _async_task_replanning(
1236
1276
  self, task_prompt: str, detailed_plan: str
1237
1277
  ) -> Tuple[bool, str]:
1238
1278
  r"""Replan the task based on the given task prompt.
@@ -1252,12 +1292,11 @@ Here is a plan about how to solve the task step-by-step which you must follow:
1252
1292
  replanning_prompt = TASK_REPLANNING_PROMPT_TEMPLATE.format(
1253
1293
  task_prompt=task_prompt,
1254
1294
  detailed_plan=detailed_plan,
1255
- history_window=self.history_window,
1256
1295
  history=self.history[-self.history_window :],
1257
1296
  )
1258
1297
  # Reset the history message of planning_agent.
1259
1298
  self.planning_agent.reset()
1260
- resp = self.planning_agent.step(replanning_prompt)
1299
+ resp = await self.planning_agent.astep(replanning_prompt)
1261
1300
  resp_dict = _parse_json_output(resp.msgs[0].content, logger)
1262
1301
 
1263
1302
  if_need_replan = resp_dict.get("if_need_replan", False)
@@ -1287,7 +1326,7 @@ Here is a plan about how to solve the task step-by-step which you must follow:
1287
1326
 
1288
1327
  self._reset()
1289
1328
  task_completed = False
1290
- detailed_plan = self._task_planning(task_prompt, start_url)
1329
+ detailed_plan = await self._async_task_planning(task_prompt, start_url)
1291
1330
  logger.debug(f"Detailed plan: {detailed_plan}")
1292
1331
 
1293
1332
  await self.browser.async_init()
@@ -1331,7 +1370,11 @@ Here is a plan about how to solve the task step-by-step which you must follow:
1331
1370
  self.history.append(trajectory_info)
1332
1371
 
1333
1372
  # replan the task if necessary
1334
- if_need_replan, replanned_schema = self._task_replanning(
1373
+ (
1374
+ if_need_replan,
1375
+ replanned_schema,
1376
+ # ruff: noqa: E501
1377
+ ) = await self._async_task_replanning(
1335
1378
  task_prompt, detailed_plan
1336
1379
  )
1337
1380
  if if_need_replan:
@@ -1343,11 +1386,11 @@ Here is a plan about how to solve the task step-by-step which you must follow:
1343
1386
  The task is not completed within the round limit. Please check
1344
1387
  the last round {self.history_window} information to see if
1345
1388
  there is any useful information:
1346
- <history>{self.history[-self.history_window :]}</history>
1389
+ <history>{self.history[-self.history_window:]}</history>
1347
1390
  """
1348
1391
 
1349
1392
  else:
1350
- simulation_result = self._get_final_answer(task_prompt)
1393
+ simulation_result = await self._async_get_final_answer(task_prompt)
1351
1394
 
1352
1395
  await self.browser.close()
1353
1396
  return simulation_result
@@ -125,6 +125,7 @@ class BaseBrowser:
125
125
  cache_dir: Optional[str] = None,
126
126
  channel: Literal["chrome", "msedge", "chromium"] = "chromium",
127
127
  cookie_json_path: Optional[str] = None,
128
+ user_data_dir: Optional[str] = None,
128
129
  ):
129
130
  r"""Initialize the WebBrowser instance.
130
131
 
@@ -137,8 +138,11 @@ class BaseBrowser:
137
138
  cookie_json_path (Optional[str]): Path to a JSON file containing
138
139
  authentication cookies and browser storage state. If provided
139
140
  and the file exists, the browser will load this state to
140
- maintain
141
- authenticated sessions without requiring manual login.
141
+ maintain authenticated sessions. This is primarily used when
142
+ `user_data_dir` is not set.
143
+ user_data_dir (Optional[str]): The directory to store user data
144
+ for persistent context. If None, a fresh browser instance
145
+ is used without saving data. (default: :obj:`None`)
142
146
 
143
147
  Returns:
144
148
  None
@@ -156,11 +160,16 @@ class BaseBrowser:
156
160
  str
157
161
  ] = [] # stores the history of visited pages
158
162
  self.cookie_json_path = cookie_json_path
163
+ self.user_data_dir = user_data_dir
159
164
 
160
165
  # Set the cache directory
161
166
  self.cache_dir = "tmp/" if cache_dir is None else cache_dir
162
167
  os.makedirs(self.cache_dir, exist_ok=True)
163
168
 
169
+ # Create user data directory only if specified
170
+ if self.user_data_dir:
171
+ os.makedirs(self.user_data_dir, exist_ok=True)
172
+
164
173
  # Load the page script
165
174
  abs_dir_path = os.path.dirname(os.path.abspath(__file__))
166
175
  page_script_path = os.path.join(abs_dir_path, "page_script.js")
@@ -183,27 +192,56 @@ class BaseBrowser:
183
192
 
184
193
  def init(self) -> None:
185
194
  r"""Initialize the browser."""
186
- # Launch the browser, if headless is False, the browser will display
187
195
  assert self.playwright is not None
188
- self.browser = self.playwright.chromium.launch(
189
- headless=self.headless, channel=self.channel
196
+
197
+ browser_launch_args = [
198
+ "--disable-blink-features=AutomationControlled", # Basic stealth
199
+ ]
200
+
201
+ user_agent_string = (
202
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
203
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
204
+ "Chrome/91.0.4472.124 Safari/537.36"
190
205
  )
191
206
 
192
- # Check if cookie file exists before using it to maintain
193
- # authenticated sessions. This prevents errors when the cookie file
194
- # doesn't exist
195
- assert self.browser is not None
196
- if self.cookie_json_path and os.path.exists(self.cookie_json_path):
197
- self.context = self.browser.new_context(
198
- accept_downloads=True, storage_state=self.cookie_json_path
207
+ if self.user_data_dir:
208
+ self.context = self.playwright.chromium.launch_persistent_context(
209
+ user_data_dir=self.user_data_dir,
210
+ headless=self.headless,
211
+ channel=self.channel,
212
+ accept_downloads=True,
213
+ user_agent=user_agent_string,
214
+ java_script_enabled=True,
215
+ args=browser_launch_args,
199
216
  )
217
+ self.browser = None # Not using a separate browser instance
218
+ if (
219
+ len(self.context.pages) > 0
220
+ ): # Persistent context might reopen pages
221
+ self.page = self.context.pages[0]
222
+ else:
223
+ self.page = self.context.new_page()
200
224
  else:
201
- self.context = self.browser.new_context(
202
- accept_downloads=True,
225
+ # Launch a fresh browser instance
226
+ self.browser = self.playwright.chromium.launch(
227
+ headless=self.headless,
228
+ channel=self.channel,
229
+ args=browser_launch_args,
203
230
  )
204
- # Create a new page
231
+
232
+ new_context_kwargs: Dict[str, Any] = {
233
+ "accept_downloads": True,
234
+ "user_agent": user_agent_string,
235
+ "java_script_enabled": True,
236
+ }
237
+ if self.cookie_json_path and os.path.exists(self.cookie_json_path):
238
+ new_context_kwargs["storage_state"] = self.cookie_json_path
239
+
240
+ self.context = self.browser.new_context(**new_context_kwargs)
241
+ self.page = self.context.new_page()
242
+
205
243
  assert self.context is not None
206
- self.page = self.context.new_page()
244
+ assert self.page is not None
207
245
 
208
246
  def clean_cache(self) -> None:
209
247
  r"""Delete the cache directory and its contents."""
@@ -690,8 +728,12 @@ class BaseBrowser:
690
728
  self._wait_for_load()
691
729
 
692
730
  def close(self):
693
- assert self.browser is not None
694
- self.browser.close()
731
+ if self.context is not None:
732
+ self.context.close()
733
+ if (
734
+ self.browser is not None
735
+ ): # Only close browser if it was launched separately
736
+ self.browser.close()
695
737
  if self.playwright:
696
738
  self.playwright.stop() # Stop playwright instance
697
739
 
@@ -781,6 +823,7 @@ class BrowserToolkit(BaseToolkit):
781
823
  planning_agent_model: Optional[BaseModelBackend] = None,
782
824
  output_language: str = "en",
783
825
  cookie_json_path: Optional[str] = None,
826
+ user_data_dir: Optional[str] = None,
784
827
  ):
785
828
  r"""Initialize the BrowserToolkit instance.
786
829
 
@@ -804,6 +847,9 @@ class BrowserToolkit(BaseToolkit):
804
847
  maintain
805
848
  authenticated sessions without requiring manual login.
806
849
  (default: :obj:`None`)
850
+ user_data_dir (Optional[str]): The directory to store user data
851
+ for persistent context. If None, a fresh browser instance
852
+ is used without saving data. (default: :obj:`None`)
807
853
  """
808
854
  super().__init__() # Call to super().__init__() added
809
855
  self.browser = BaseBrowser(
@@ -811,6 +857,7 @@ class BrowserToolkit(BaseToolkit):
811
857
  cache_dir=cache_dir,
812
858
  channel=channel,
813
859
  cookie_json_path=cookie_json_path,
860
+ user_data_dir=user_data_dir,
814
861
  )
815
862
  self.browser.web_agent_model = web_agent_model # Pass model to
816
863
  # BaseBrowser instance
@@ -20,10 +20,13 @@ from camel.interpreters import (
20
20
  JupyterKernelInterpreter,
21
21
  SubprocessInterpreter,
22
22
  )
23
+ from camel.logger import get_logger
23
24
  from camel.toolkits import FunctionTool
24
25
  from camel.toolkits.base import BaseToolkit
25
26
  from camel.utils import MCPServer
26
27
 
28
+ logger = get_logger(__name__)
29
+
27
30
 
28
31
  @MCPServer()
29
32
  class CodeExecutionToolkit(BaseToolkit):
@@ -36,10 +39,10 @@ class CodeExecutionToolkit(BaseToolkit):
36
39
  (default: :obj:`False`)
37
40
  unsafe_mode (bool): If `True`, the interpreter runs the code
38
41
  by `eval()` without any security check. (default: :obj:`False`)
39
- import_white_list ( Optional[List[str]]): A list of allowed imports.
42
+ import_white_list (Optional[List[str]]): A list of allowed imports.
40
43
  (default: :obj:`None`)
41
- require_confirm (bool): Whether to require confirmation before executing code.
42
- (default: :obj:`False`)
44
+ require_confirm (bool): Whether to require confirmation before
45
+ executing code. (default: :obj:`False`)
43
46
  """
44
47
 
45
48
  def __init__(
@@ -97,18 +100,41 @@ class CodeExecutionToolkit(BaseToolkit):
97
100
  f"The sandbox type `{sandbox}` is not supported."
98
101
  )
99
102
 
100
- def execute_code(self, code: str) -> str:
103
+ def execute_code(self, code: str, code_type: str = "python") -> str:
101
104
  r"""Execute a given code snippet.
102
105
 
103
106
  Args:
104
107
  code (str): The input code to the Code Interpreter tool call.
108
+ code_type (str): The type of the code to be executed
109
+ (e.g. node.js, python, etc). (default: obj:`python`)
105
110
 
106
111
  Returns:
107
112
  str: The text output from the Code Interpreter tool call.
108
113
  """
109
- output = self.interpreter.run(code, "python")
110
- # ruff: noqa: E501
111
- content = f"Executed the code below:\n```py\n{code}\n```\n> Executed Results:\n{output}"
114
+ output = self.interpreter.run(code, code_type)
115
+ content = (
116
+ f"Executed the code below:\n```{code_type}\n{code}\n```\n"
117
+ f"> Executed Results:\n{output}"
118
+ )
119
+ if self.verbose:
120
+ print(content)
121
+ return content
122
+
123
+ def execute_command(self, command: str) -> Union[str, tuple[str, str]]:
124
+ r"""Execute a command can be used to resolve the dependency of the
125
+ code.
126
+
127
+ Args:
128
+ command (str): The command to execute.
129
+
130
+ Returns:
131
+ Union[str, tuple[str, str]]: The output of the command.
132
+ """
133
+ output = self.interpreter.execute_command(command)
134
+ content = (
135
+ f"Executed the command below:\n```sh\n{command}\n```\n"
136
+ f"> Executed Results:\n{output}"
137
+ )
112
138
  if self.verbose:
113
139
  print(content)
114
140
  return content
@@ -121,4 +147,7 @@ class CodeExecutionToolkit(BaseToolkit):
121
147
  List[FunctionTool]: A list of FunctionTool objects
122
148
  representing the functions in the toolkit.
123
149
  """
124
- return [FunctionTool(self.execute_code)]
150
+ return [
151
+ FunctionTool(self.execute_code),
152
+ FunctionTool(self.execute_command),
153
+ ]
@@ -547,7 +547,7 @@ class FunctionTool:
547
547
  """
548
548
  self.openai_tool_schema["function"]["description"] = description
549
549
 
550
- def get_paramter_description(self, param_name: str) -> str:
550
+ def get_parameter_description(self, param_name: str) -> str:
551
551
  r"""Gets the description of a specific parameter from the function
552
552
  schema.
553
553
 
@@ -563,7 +563,7 @@ class FunctionTool:
563
563
  param_name
564
564
  ]["description"]
565
565
 
566
- def set_paramter_description(
566
+ def set_parameter_description(
567
567
  self,
568
568
  param_name: str,
569
569
  description: str,
@@ -98,6 +98,8 @@ class MCPToolkit(BaseToolkit):
98
98
  timeout (Optional[float], optional): Timeout for connection attempts
99
99
  in seconds. This timeout applies to individual client connections.
100
100
  (default: :obj:`None`)
101
+ strict (Optional[bool], optional): Flag to indicate strict mode.
102
+ (default: :obj:`False`)
101
103
 
102
104
  Note:
103
105
  At least one of :obj:`clients`, :obj:`config_path`, or
@@ -146,6 +148,7 @@ class MCPToolkit(BaseToolkit):
146
148
  config_path: Optional[str] = None,
147
149
  config_dict: Optional[Dict[str, Any]] = None,
148
150
  timeout: Optional[float] = None,
151
+ strict: Optional[bool] = False,
149
152
  ):
150
153
  # Call parent constructor first
151
154
  super().__init__(timeout=timeout)
@@ -162,6 +165,7 @@ class MCPToolkit(BaseToolkit):
162
165
  raise ValueError(error_msg)
163
166
 
164
167
  self.clients: List[MCPClient] = clients or []
168
+ self.strict = strict # Store strict parameter
165
169
  self._is_connected = False
166
170
  self._exit_stack: Optional[AsyncExitStack] = None
167
171
 
@@ -309,6 +313,7 @@ class MCPToolkit(BaseToolkit):
309
313
  config_path: Optional[str] = None,
310
314
  config_dict: Optional[Dict[str, Any]] = None,
311
315
  timeout: Optional[float] = None,
316
+ strict: Optional[bool] = False,
312
317
  ) -> "MCPToolkit":
313
318
  r"""Factory method that creates and connects to all MCP servers.
314
319
 
@@ -326,6 +331,8 @@ class MCPToolkit(BaseToolkit):
326
331
  config file. (default: :obj:`None`)
327
332
  timeout (Optional[float], optional): Timeout for connection
328
333
  attempts in seconds. (default: :obj:`None`)
334
+ strict (Optional[bool], optional): Flag to indicate strict mode.
335
+ (default: :obj:`False`)
329
336
 
330
337
  Returns:
331
338
  MCPToolkit: A fully initialized and connected :obj:`MCPToolkit`
@@ -354,6 +361,7 @@ class MCPToolkit(BaseToolkit):
354
361
  config_path=config_path,
355
362
  config_dict=config_dict,
356
363
  timeout=timeout,
364
+ strict=strict,
357
365
  )
358
366
  try:
359
367
  await toolkit.connect()
@@ -373,10 +381,11 @@ class MCPToolkit(BaseToolkit):
373
381
  config_path: Optional[str] = None,
374
382
  config_dict: Optional[Dict[str, Any]] = None,
375
383
  timeout: Optional[float] = None,
384
+ strict: Optional[bool] = False,
376
385
  ) -> "MCPToolkit":
377
386
  r"""Synchronously create and connect to all MCP servers."""
378
387
  return run_async(cls.create)(
379
- clients, config_path, config_dict, timeout
388
+ clients, config_path, config_dict, timeout, strict
380
389
  )
381
390
 
382
391
  def _load_clients_from_config(self, config_path: str) -> List[MCPClient]:
@@ -433,10 +442,12 @@ class MCPToolkit(BaseToolkit):
433
442
 
434
443
  try:
435
444
  # Use the new mcp_client factory function
436
- # Pass timeout from toolkit if available
445
+ # Pass timeout and strict from toolkit if available
437
446
  kwargs = {}
438
447
  if hasattr(self, "timeout") and self.timeout is not None:
439
448
  kwargs["timeout"] = self.timeout
449
+ if hasattr(self, "strict") and self.strict is not None:
450
+ kwargs["strict"] = self.strict
440
451
 
441
452
  client = create_mcp_client(cfg, **kwargs)
442
453
  return client