camel-ai 0.2.64__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.

@@ -16,6 +16,10 @@ from typing import Callable
16
16
 
17
17
  from pydantic import BaseModel, Field
18
18
 
19
+ from camel.logger import get_logger
20
+
21
+ logger = get_logger(__name__)
22
+
19
23
 
20
24
  class WorkerConf(BaseModel):
21
25
  r"""The configuration of a worker."""
@@ -71,3 +75,50 @@ def check_if_running(running: bool) -> Callable:
71
75
  return wrapper
72
76
 
73
77
  return decorator
78
+
79
+
80
+ def validate_task_content(
81
+ content: str, task_id: str = "unknown", min_length: int = 10
82
+ ) -> bool:
83
+ r"""Validates task result content to avoid silent failures.
84
+ It performs basic checks to ensure the content meets minimum
85
+ quality standards.
86
+
87
+ Args:
88
+ content (str): The task result content to validate.
89
+ task_id (str): Task ID for logging purposes.
90
+ (default: :obj:`"unknown"`)
91
+ min_length (int): Minimum content length after stripping whitespace.
92
+ (default: :obj:`10`)
93
+
94
+ Returns:
95
+ bool: True if content passes validation, False otherwise.
96
+ """
97
+ # 1: Content must not be None
98
+ if content is None:
99
+ logger.warning(f"Task {task_id}: None content rejected")
100
+ return False
101
+
102
+ # 2: Content must not be empty after stripping whitespace
103
+ stripped_content = content.strip()
104
+ if not stripped_content:
105
+ logger.warning(
106
+ f"Task {task_id}: Empty or whitespace-only content rejected."
107
+ )
108
+ return False
109
+
110
+ # 3: Content must meet minimum meaningful length
111
+ if len(stripped_content) < min_length:
112
+ logger.warning(
113
+ f"Task {task_id}: Content too short ({len(stripped_content)} "
114
+ f"chars < {min_length} minimum). Content preview: "
115
+ f"'{stripped_content[:50]}...'"
116
+ )
117
+ return False
118
+
119
+ # All validation checks passed
120
+ logger.debug(
121
+ f"Task {task_id}: Content validation passed "
122
+ f"({len(stripped_content)} chars)"
123
+ )
124
+ return True
@@ -681,8 +681,8 @@ class Workforce(BaseNode):
681
681
  task_id (str, optional): Unique identifier for the task. If
682
682
  None, a UUID will be automatically generated.
683
683
  (default: :obj:`None`)
684
- additional_info (str, optional): Additional information or
685
- context for the task. (default: :obj:`None`)
684
+ additional_info (Optional[Dict[str, Any]]): Additional
685
+ information or context for the task. (default: :obj:`None`)
686
686
 
687
687
  Returns:
688
688
  Dict[str, Any]: A dictionary containing the processing result
@@ -701,7 +701,7 @@ class Workforce(BaseNode):
701
701
  task = Task(
702
702
  content=task_content,
703
703
  id=task_id or str(uuid.uuid4()),
704
- additional_info=additional_info or "",
704
+ additional_info=additional_info,
705
705
  )
706
706
 
707
707
  try:
camel/tasks/task.py CHANGED
@@ -106,6 +106,14 @@ class Task(BaseModel):
106
106
 
107
107
  additional_info: Optional[Dict[str, Any]] = None
108
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
+ )
116
+
109
117
  @classmethod
110
118
  def from_message(cls, message: BaseMessage) -> "Task":
111
119
  r"""Create a task from a message.
@@ -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
@@ -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,
@@ -27,17 +27,29 @@ class PlaywrightMCPToolkit(BaseToolkit):
27
27
  Attributes:
28
28
  timeout (Optional[float]): Connection timeout in seconds.
29
29
  (default: :obj:`None`)
30
+ additional_args (Optional[List[str]]): Additional command-line
31
+ arguments to pass to the Playwright MCP server. For example,
32
+ `["--cdp-endpoint=http://localhost:9222"]`.
33
+ (default: :obj:`None`)
30
34
 
31
35
  Note:
32
36
  Currently only supports asynchronous operation mode.
33
37
  """
34
38
 
35
- def __init__(self, timeout: Optional[float] = None) -> None:
36
- r"""Initializes the PlaywrightMCPToolkit with the specified timeout.
39
+ def __init__(
40
+ self,
41
+ timeout: Optional[float] = None,
42
+ additional_args: Optional[List[str]] = None,
43
+ ) -> None:
44
+ r"""Initializes the PlaywrightMCPToolkit.
37
45
 
38
46
  Args:
39
47
  timeout (Optional[float]): Connection timeout in seconds.
40
48
  (default: :obj:`None`)
49
+ additional_args (Optional[List[str]]): Additional command-line
50
+ arguments to pass to the Playwright MCP server. For example,
51
+ `["--cdp-endpoint=http://localhost:9222"]`.
52
+ (default: :obj:`None`)
41
53
  """
42
54
  super().__init__(timeout=timeout)
43
55
 
@@ -46,7 +58,8 @@ class PlaywrightMCPToolkit(BaseToolkit):
46
58
  "mcpServers": {
47
59
  "playwright": {
48
60
  "command": "npx",
49
- "args": ["@playwright/mcp@latest"],
61
+ "args": ["@playwright/mcp@latest"]
62
+ + (additional_args or []),
50
63
  }
51
64
  }
52
65
  }