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

@@ -0,0 +1,456 @@
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
+
15
+ import os
16
+ from typing import TYPE_CHECKING, Dict, List, Optional, Union
17
+
18
+ if TYPE_CHECKING:
19
+ from aci.types.app_configurations import AppConfiguration
20
+ from aci.types.apps import AppBasic, AppDetails
21
+ from aci.types.linked_accounts import LinkedAccount
22
+
23
+ from camel.logger import get_logger
24
+ from camel.toolkits import FunctionTool
25
+ from camel.toolkits.base import BaseToolkit
26
+ from camel.utils import api_keys_required, dependencies_required
27
+
28
+ logger = get_logger(__name__)
29
+
30
+
31
+ @api_keys_required(
32
+ [
33
+ (None, 'ACI_API_KEY'),
34
+ ]
35
+ )
36
+ class ACIToolkit(BaseToolkit):
37
+ r"""A toolkit for interacting with the ACI API."""
38
+
39
+ @dependencies_required('aci')
40
+ def __init__(
41
+ self,
42
+ api_key: Optional[str] = None,
43
+ base_url: Optional[str] = None,
44
+ linked_account_owner_id: Optional[str] = None,
45
+ timeout: Optional[float] = None,
46
+ ) -> None:
47
+ r"""Initialize the ACI toolkit.
48
+
49
+ Args:
50
+ api_key (Optional[str]): The API key for authentication.
51
+ (default: :obj: `None`)
52
+ base_url (Optional[str]): The base URL for the ACI API.
53
+ (default: :obj: `None`)
54
+ linked_account_owner_id (Optional[str]): ID of the owner of the
55
+ linked account, e.g., "johndoe"
56
+ (default: :obj: `None`)
57
+ timeout (Optional[float]): Request timeout.
58
+ (default: :obj: `None`)
59
+ """
60
+ from aci import ACI
61
+
62
+ super().__init__(timeout)
63
+
64
+ self._api_key = api_key or os.getenv("ACI_API_KEY")
65
+ self._base_url = base_url or os.getenv("ACI_BASE_URL")
66
+ self.client = ACI(api_key=self._api_key, base_url=self._base_url)
67
+ self.linked_account_owner_id = linked_account_owner_id
68
+
69
+ def search_tool(
70
+ self,
71
+ intent: Optional[str] = None,
72
+ allowed_app_only: bool = True,
73
+ include_functions: bool = False,
74
+ categories: Optional[List[str]] = None,
75
+ limit: Optional[int] = 10,
76
+ offset: Optional[int] = 0,
77
+ ) -> Union[List["AppBasic"], str]:
78
+ r"""Search for apps based on intent.
79
+
80
+ Args:
81
+ intent (Optional[str]): Search results will be sorted by relevance
82
+ to this intent.
83
+ (default: :obj: `None`)
84
+ allowed_app_only (bool): If true, only return apps that
85
+ are allowed by the agent/accessor, identified by the api key.
86
+ (default: :obj: `True`)
87
+ include_functions (bool): If true, include functions
88
+ (name and description) in the search results.
89
+ (default: :obj: `False`)
90
+ categories (Optional[List[str]]): List of categories to filter the
91
+ search results. Defaults to an empty list.
92
+ (default: :obj: `None`)
93
+ limit (Optional[int]): Maximum number of results to return.
94
+ (default: :obj: `10`)
95
+ offset (Optional[int]): Offset for pagination.
96
+ (default: :obj: `0`)
97
+
98
+ Returns:
99
+ Optional[List[AppBasic]]: List of matching apps if successful,
100
+ error message otherwise.
101
+ """
102
+ try:
103
+ apps = self.client.apps.search(
104
+ intent=intent,
105
+ allowed_apps_only=allowed_app_only,
106
+ include_functions=include_functions,
107
+ categories=categories,
108
+ limit=limit,
109
+ offset=offset,
110
+ )
111
+ return apps
112
+ except Exception as e:
113
+ logger.error(f"Error: {e}")
114
+ return str(e)
115
+
116
+ def list_configured_apps(
117
+ self,
118
+ app_names: Optional[List[str]] = None,
119
+ limit: Optional[int] = 10,
120
+ offset: Optional[int] = 0,
121
+ ) -> Union[List["AppConfiguration"], str]:
122
+ r"""List all configured apps.
123
+
124
+ Args:
125
+ app_names (Optional[List[str]]): List of app names to filter the
126
+ results. (default: :obj: `None`)
127
+ limit (Optional[int]): Maximum number of results to return.
128
+ (default: :obj: `10`)
129
+ offset (Optional[int]): Offset for pagination. (default: :obj: `0`)
130
+
131
+ Returns:
132
+ Union[List[AppConfiguration], str]: List of configured apps if
133
+ successful, error message otherwise.
134
+ """
135
+ try:
136
+ apps = self.client.app_configurations.list(
137
+ app_names=app_names, limit=limit, offset=offset
138
+ )
139
+ return apps
140
+ except Exception as e:
141
+ logger.error(f"Error: {e}")
142
+ return str(e)
143
+
144
+ def configure_app(self, app_name: str) -> Union[Dict, str]:
145
+ r"""Configure an app with specified authentication type.
146
+
147
+ Args:
148
+ app_name (str): Name of the app to configure.
149
+
150
+ Returns:
151
+ Union[Dict, str]: Configuration result or error message.
152
+ """
153
+ from aci.types.enums import SecurityScheme
154
+
155
+ try:
156
+ app_details = self.get_app_details(app_name)
157
+ if app_details and app_details.security_schemes[0] == "api_key":
158
+ security_scheme = SecurityScheme.API_KEY
159
+ elif app_details and app_details.security_schemes[0] == "oauth2":
160
+ security_scheme = SecurityScheme.OAUTH2
161
+ else:
162
+ security_scheme = SecurityScheme.NO_AUTH
163
+ configuration = self.client.app_configurations.create(
164
+ app_name=app_name, security_scheme=security_scheme
165
+ )
166
+ return configuration
167
+ except Exception as e:
168
+ logger.error(f"Error: {e}")
169
+ return str(e)
170
+
171
+ def get_app_configuration(
172
+ self, app_name: str
173
+ ) -> Union["AppConfiguration", str]:
174
+ r"""Get app configuration by app name.
175
+
176
+ Args:
177
+ app_name (str): Name of the app to get configuration for.
178
+
179
+ Returns:
180
+ Union[AppConfiguration, str]: App configuration if successful,
181
+ error message otherwise.
182
+ """
183
+ try:
184
+ app = self.client.app_configurations.get(app_name=app_name)
185
+ return app
186
+ except Exception as e:
187
+ logger.error(f"Error: {e}")
188
+ return str(e)
189
+
190
+ def delete_app(self, app_name: str) -> Optional[str]:
191
+ r"""Delete an app configuration.
192
+
193
+ Args:
194
+ app_name (str): Name of the app to delete.
195
+
196
+ Returns:
197
+ Optional[str]: None if successful, error message otherwise.
198
+ """
199
+ try:
200
+ self.client.app_configurations.delete(app_name=app_name)
201
+ return None
202
+ except Exception as e:
203
+ logger.error(f"Error: {e}")
204
+ return str(e)
205
+
206
+ def link_account(
207
+ self,
208
+ app_name: str,
209
+ ) -> Union["LinkedAccount", str]:
210
+ r"""Link an account to a configured app.
211
+
212
+ Args:
213
+ app_name (str): Name of the app to link the account to.
214
+
215
+ Returns:
216
+ Union[LinkedAccount, str]: LinkedAccount object if successful,
217
+ error message otherwise.
218
+ """
219
+ from aci.types.enums import SecurityScheme
220
+
221
+ try:
222
+ security_scheme = self.client.app_configurations.get(
223
+ app_name=app_name
224
+ ).security_scheme
225
+
226
+ if security_scheme == SecurityScheme.API_KEY:
227
+ return self.client.linked_accounts.link(
228
+ app_name=app_name,
229
+ linked_account_owner_id=self.linked_account_owner_id,
230
+ security_scheme=security_scheme,
231
+ api_key=self._api_key,
232
+ )
233
+ else:
234
+ return self.client.linked_accounts.link(
235
+ app_name=app_name,
236
+ linked_account_owner_id=self.linked_account_owner_id,
237
+ security_scheme=security_scheme,
238
+ )
239
+ except Exception as e:
240
+ logger.error(f"Error linking account: {e!s}")
241
+ return str(e)
242
+
243
+ def get_app_details(self, app_name: str) -> "AppDetails":
244
+ r"""Get details of an app.
245
+
246
+ Args:
247
+ app_name (str): Name of the app to get details for.
248
+
249
+ Returns:
250
+ AppDetails: App details.
251
+ """
252
+ app = self.client.apps.get(app_name=app_name)
253
+ return app
254
+
255
+ def get_linked_accounts(
256
+ self, app_name: str
257
+ ) -> Union[List["LinkedAccount"], str]:
258
+ r"""List all linked accounts for a specific app.
259
+
260
+ Args:
261
+ app_name (str): Name of the app to get linked accounts for.
262
+
263
+ Returns:
264
+ Union[List[LinkedAccount], str]: List of linked accounts if
265
+ successful, error message otherwise.
266
+ """
267
+ try:
268
+ accounts = self.client.linked_accounts.list(app_name=app_name)
269
+ return accounts
270
+ except Exception as e:
271
+ logger.error(f"Error: {e}")
272
+ return str(e)
273
+
274
+ def enable_linked_account(
275
+ self, linked_account_id: str
276
+ ) -> Union["LinkedAccount", str]:
277
+ r"""Enable a linked account.
278
+
279
+ Args:
280
+ linked_account_id (str): ID of the linked account to enable.
281
+
282
+ Returns:
283
+ Union[LinkedAccount, str]: Linked account if successful, error
284
+ message otherwise.
285
+ """
286
+ try:
287
+ linked_account = self.client.linked_accounts.enable(
288
+ linked_account_id=linked_account_id
289
+ )
290
+ return linked_account
291
+ except Exception as e:
292
+ logger.error(f"Error: {e}")
293
+ return str(e)
294
+
295
+ def disable_linked_account(
296
+ self, linked_account_id: str
297
+ ) -> Union["LinkedAccount", str]:
298
+ r"""Disable a linked account.
299
+
300
+ Args:
301
+ linked_account_id (str): ID of the linked account to disable.
302
+
303
+ Returns:
304
+ Union[LinkedAccount, str]: The updated linked account if
305
+ successful, error message otherwise.
306
+ """
307
+ try:
308
+ linked_account = self.client.linked_accounts.disable(
309
+ linked_account_id=linked_account_id
310
+ )
311
+ return linked_account
312
+ except Exception as e:
313
+ logger.error(f"Error: {e}")
314
+ return str(e)
315
+
316
+ def delete_linked_account(self, linked_account_id: str) -> str:
317
+ r"""Delete a linked account.
318
+
319
+ Args:
320
+ linked_account_id (str): ID of the linked account to delete.
321
+
322
+ Returns:
323
+ str: Success message if successful, error message otherwise.
324
+ """
325
+ try:
326
+ self.client.linked_accounts.delete(
327
+ linked_account_id=linked_account_id
328
+ )
329
+ return (
330
+ f"linked_account_id: {linked_account_id} deleted successfully"
331
+ )
332
+ except Exception as e:
333
+ logger.error(f"Error: {e}")
334
+ return str(e)
335
+
336
+ def function_definition(self, func_name: str) -> Dict:
337
+ r"""Get the function definition for an app.
338
+
339
+ Args:
340
+ app_name (str): Name of the app to get function definition for
341
+
342
+ Returns:
343
+ Dict: Function definition dictionary.
344
+ """
345
+ return self.client.functions.get_definition(func_name)
346
+
347
+ def search_function(
348
+ self,
349
+ app_names: Optional[List[str]] = None,
350
+ intent: Optional[str] = None,
351
+ allowed_apps_only: bool = True,
352
+ limit: Optional[int] = 10,
353
+ offset: Optional[int] = 0,
354
+ ) -> List[Dict]:
355
+ r"""Search for functions based on intent.
356
+
357
+ Args:
358
+ app_names (Optional[List[str]]): List of app names to filter the
359
+ search results. (default: :obj: `None`)
360
+ intent (Optional[str]): The search query/intent.
361
+ (default: :obj: `None`)
362
+ allowed_apps_only (bool): If true, only return
363
+ functions from allowed apps. (default: :obj: `True`)
364
+ limit (Optional[int]): Maximum number of results to return.
365
+ (default: :obj: `10`)
366
+ offset (Optional[int]): Offset for pagination.
367
+ (default: :obj: `0`)
368
+
369
+ Returns:
370
+ List[Dict]: List of matching functions
371
+ """
372
+ return self.client.functions.search(
373
+ app_names=app_names,
374
+ intent=intent,
375
+ allowed_apps_only=allowed_apps_only,
376
+ limit=limit,
377
+ offset=offset,
378
+ )
379
+
380
+ def execute_function(
381
+ self,
382
+ function_name: str,
383
+ function_arguments: Dict,
384
+ linked_account_owner_id: str,
385
+ allowed_apps_only: bool = False,
386
+ ) -> Dict:
387
+ r"""Execute a function call.
388
+
389
+ Args:
390
+ function_name (str): Name of the function to execute.
391
+ function_arguments (Dict): Arguments to pass to the function.
392
+ linked_account_owner_id (str): To specify the end-user (account
393
+ owner) on behalf of whom you want to execute functions
394
+ You need to first link corresponding account with the same
395
+ owner id in the ACI dashboard (https://platform.aci.dev).
396
+ allowed_apps_only (bool): If true, only returns functions/apps
397
+ that are allowed to be used by the agent/accessor, identified
398
+ by the api key. (default: :obj: `False`)
399
+
400
+ Returns:
401
+ Dict: Result of the function execution
402
+ """
403
+ result = self.client.handle_function_call(
404
+ function_name,
405
+ function_arguments,
406
+ linked_account_owner_id,
407
+ allowed_apps_only,
408
+ )
409
+ return result
410
+
411
+ def get_tools(self) -> List[FunctionTool]:
412
+ r"""Get a list of tools (functions) available in the configured apps.
413
+
414
+ Returns:
415
+ List[FunctionTool]: List of FunctionTool objects representing
416
+ available functions
417
+ """
418
+ _configure_app = [
419
+ app.app_name # type: ignore[union-attr]
420
+ for app in self.list_configured_apps() or []
421
+ ]
422
+ _all_function = self.search_function(app_names=_configure_app)
423
+ tools = [
424
+ FunctionTool(self.search_tool),
425
+ FunctionTool(self.list_configured_apps),
426
+ FunctionTool(self.configure_app),
427
+ FunctionTool(self.get_app_configuration),
428
+ FunctionTool(self.delete_app),
429
+ FunctionTool(self.link_account),
430
+ FunctionTool(self.get_app_details),
431
+ FunctionTool(self.get_linked_accounts),
432
+ FunctionTool(self.enable_linked_account),
433
+ FunctionTool(self.disable_linked_account),
434
+ FunctionTool(self.delete_linked_account),
435
+ FunctionTool(self.function_definition),
436
+ FunctionTool(self.search_function),
437
+ ]
438
+
439
+ for function in _all_function:
440
+ schema = self.client.functions.get_definition(
441
+ function['function']['name']
442
+ )
443
+
444
+ def dummy_func(*, schema=schema, **kwargs):
445
+ return self.execute_function(
446
+ function_name=schema['function']['name'],
447
+ function_arguments=kwargs,
448
+ linked_account_owner_id=self.linked_account_owner_id,
449
+ )
450
+
451
+ tool = FunctionTool(
452
+ func=dummy_func,
453
+ openai_tool_schema=schema,
454
+ )
455
+ tools.append(tool)
456
+ return tools
@@ -458,9 +458,11 @@ class FunctionTool:
458
458
  for param_name in properties.keys():
459
459
  param_dict = properties[param_name]
460
460
  if "description" not in param_dict:
461
- warnings.warn(f"""Parameter description is missing for
462
- {param_dict}. This may affect the quality of tool
463
- calling.""")
461
+ warnings.warn(
462
+ f"Parameter description is missing "
463
+ f"for {param_dict}. This may affect the "
464
+ f"quality of tool calling."
465
+ )
464
466
 
465
467
  def get_openai_tool_schema(self) -> Dict[str, Any]:
466
468
  r"""Gets the OpenAI tool schema for this function.
@@ -227,7 +227,7 @@ class MCPClient(BaseToolkit):
227
227
 
228
228
  func_params.append(param_name)
229
229
 
230
- async def dynamic_function(**kwargs):
230
+ async def dynamic_function(**kwargs) -> str:
231
231
  r"""Auto-generated function for MCP Tool interaction.
232
232
 
233
233
  Args:
@@ -351,6 +351,39 @@ class MCPClient(BaseToolkit):
351
351
  for mcp_tool in self._mcp_tools
352
352
  ]
353
353
 
354
+ def get_text_tools(self) -> str:
355
+ r"""Returns a string containing the descriptions of the tools
356
+ in the toolkit.
357
+
358
+ Returns:
359
+ str: A string containing the descriptions of the tools
360
+ in the toolkit.
361
+ """
362
+ return "\n".join(
363
+ f"tool_name: {tool.name}\n"
364
+ + f"description: {tool.description or 'No description'}\n"
365
+ + f"input Schema: {tool.inputSchema}\n"
366
+ for tool in self._mcp_tools
367
+ )
368
+
369
+ async def call_tool(
370
+ self, tool_name: str, tool_args: Dict[str, Any]
371
+ ) -> Any:
372
+ r"""Calls the specified tool with the provided arguments.
373
+
374
+ Args:
375
+ tool_name (str): Name of the tool to call.
376
+ tool_args (Dict[str, Any]): Arguments to pass to the tool
377
+ (default: :obj:`{}`).
378
+
379
+ Returns:
380
+ Any: The result of the tool call.
381
+ """
382
+ if self._session is None:
383
+ raise RuntimeError("Session is not initialized.")
384
+
385
+ return await self._session.call_tool(tool_name, tool_args)
386
+
354
387
  @property
355
388
  def session(self) -> Optional["ClientSession"]:
356
389
  return self._session
@@ -548,3 +581,13 @@ class MCPToolkit(BaseToolkit):
548
581
  for server in self.servers:
549
582
  all_tools.extend(server.get_tools())
550
583
  return all_tools
584
+
585
+ def get_text_tools(self) -> str:
586
+ r"""Returns a string containing the descriptions of the tools
587
+ in the toolkit.
588
+
589
+ Returns:
590
+ str: A string containing the descriptions of the tools
591
+ in the toolkit.
592
+ """
593
+ return "\n".join(server.get_text_tools() for server in self.servers)
camel/types/__init__.py CHANGED
@@ -14,6 +14,7 @@
14
14
  from .enums import (
15
15
  AudioModelType,
16
16
  EmbeddingModelType,
17
+ GeminiEmbeddingTaskType,
17
18
  HuggingFaceRepoType,
18
19
  ModelPlatformType,
19
20
  ModelType,
@@ -75,6 +76,7 @@ __all__ = [
75
76
  'UnifiedModelType',
76
77
  'ParsedChatCompletion',
77
78
  'HuggingFaceRepoType',
79
+ 'GeminiEmbeddingTaskType',
78
80
  'NOT_GIVEN',
79
81
  'NotGiven',
80
82
  ]
camel/types/enums.py CHANGED
@@ -1202,6 +1202,8 @@ class EmbeddingModelType(Enum):
1202
1202
 
1203
1203
  MISTRAL_EMBED = "mistral-embed"
1204
1204
 
1205
+ GEMINI_EMBEDDING_EXP = "gemini-embedding-exp-03-07"
1206
+
1205
1207
  @property
1206
1208
  def is_openai(self) -> bool:
1207
1209
  r"""Returns whether this type of models is an OpenAI-released model."""
@@ -1230,6 +1232,13 @@ class EmbeddingModelType(Enum):
1230
1232
  EmbeddingModelType.MISTRAL_EMBED,
1231
1233
  }
1232
1234
 
1235
+ @property
1236
+ def is_gemini(self) -> bool:
1237
+ r"""Returns whether this type of models is an Gemini-released model."""
1238
+ return self in {
1239
+ EmbeddingModelType.GEMINI_EMBEDDING_EXP,
1240
+ }
1241
+
1233
1242
  @property
1234
1243
  def output_dim(self) -> int:
1235
1244
  if self in {
@@ -1253,10 +1262,29 @@ class EmbeddingModelType(Enum):
1253
1262
  return 3072
1254
1263
  elif self is EmbeddingModelType.MISTRAL_EMBED:
1255
1264
  return 1024
1265
+ elif self is EmbeddingModelType.GEMINI_EMBEDDING_EXP:
1266
+ return 3072
1256
1267
  else:
1257
1268
  raise ValueError(f"Unknown model type {self}.")
1258
1269
 
1259
1270
 
1271
+ class GeminiEmbeddingTaskType(str, Enum):
1272
+ r"""Task types for Gemini embedding models.
1273
+
1274
+ For more information, please refer to:
1275
+ https://ai.google.dev/gemini-api/docs/embeddings#task-types
1276
+ """
1277
+
1278
+ SEMANTIC_SIMILARITY = "SEMANTIC_SIMILARITY"
1279
+ CLASSIFICATION = "CLASSIFICATION"
1280
+ CLUSTERING = "CLUSTERING"
1281
+ RETRIEVAL_DOCUMENT = "RETRIEVAL_DOCUMENT"
1282
+ RETRIEVAL_QUERY = "RETRIEVAL_QUERY"
1283
+ QUESTION_ANSWERING = "QUESTION_ANSWERING"
1284
+ FACT_VERIFICATION = "FACT_VERIFICATION"
1285
+ CODE_RETRIEVAL_QUERY = "CODE_RETRIEVAL_QUERY"
1286
+
1287
+
1260
1288
  class TaskType(Enum):
1261
1289
  AI_SOCIETY = "ai_society"
1262
1290
  CODE = "code"
camel/utils/__init__.py CHANGED
@@ -17,6 +17,7 @@ from .commons import (
17
17
  BatchProcessor,
18
18
  agentops_decorator,
19
19
  api_keys_required,
20
+ browser_toolkit_save_auth_cookie,
20
21
  check_server_running,
21
22
  create_chunks,
22
23
  dependencies_required,
@@ -92,4 +93,5 @@ __all__ = [
92
93
  "with_timeout",
93
94
  "MCPServer",
94
95
  "sanitize_filename",
96
+ "browser_toolkit_save_auth_cookie",
95
97
  ]
camel/utils/commons.py CHANGED
@@ -1040,3 +1040,66 @@ def with_timeout(timeout=None):
1040
1040
  return decorator(func)
1041
1041
 
1042
1042
  return decorator
1043
+
1044
+
1045
+ def browser_toolkit_save_auth_cookie(
1046
+ cookie_json_path: str, url: str, wait_time: int = 60
1047
+ ):
1048
+ r"""Saves authentication cookies and browser storage state to a JSON file.
1049
+
1050
+ This function launches a browser window and navigates to the specified URL,
1051
+ allowing the user to manually authenticate (log in) during a 60-second
1052
+ wait period.After authentication, it saves all cookies, localStorage, and
1053
+ sessionStorage data to the specified JSON file path, which can be used
1054
+ later to maintain authenticated sessions without requiring manual login.
1055
+
1056
+ Args:
1057
+ cookie_json_path (str): Path where the authentication cookies and
1058
+ storage state will be saved as a JSON file. If the file already
1059
+ exists, it will be loaded first and then overwritten with updated
1060
+ state. The function checks if this file exists before attempting
1061
+ to use it.
1062
+ url (str): The URL to navigate to for authentication (e.g., a login
1063
+ page).
1064
+ wait_time (int): The time in seconds to wait for the user to manually
1065
+ authenticate.
1066
+
1067
+ Usage:
1068
+ 1. The function opens a browser window and navigates to the specified
1069
+ URL
1070
+ 2. User manually logs in during the wait_time wait period
1071
+ 3. Browser storage state (including auth cookies) is saved to the
1072
+ specified file
1073
+ 4. The saved state can be used in subsequent browser sessions to
1074
+ maintain authentication
1075
+
1076
+ Note:
1077
+ The wait_time sleep is intentional to give the user enough time to
1078
+ complete the manual authentication process before the storage state
1079
+ is captured.
1080
+ """
1081
+ from playwright.sync_api import sync_playwright
1082
+
1083
+ playwright = sync_playwright().start()
1084
+
1085
+ # Launch visible browser window using Chromium
1086
+ browser = playwright.chromium.launch(headless=False, channel="chromium")
1087
+
1088
+ # Check if cookie file exists before using it
1089
+ storage_state = (
1090
+ cookie_json_path if os.path.exists(cookie_json_path) else None
1091
+ )
1092
+
1093
+ # Create browser context with proper typing
1094
+ context = browser.new_context(
1095
+ accept_downloads=True, storage_state=storage_state
1096
+ )
1097
+ page = context.new_page()
1098
+ page.goto(url) # Navigate to the authentication URL
1099
+ # Wait for page to fully load
1100
+ page.wait_for_load_state("load", timeout=1000)
1101
+ time.sleep(wait_time) # Wait 60 seconds for user to manually authenticate
1102
+ # Save browser storage state (cookies, localStorage, etc.) to JSON file
1103
+ context.storage_state(path=cookie_json_path)
1104
+
1105
+ browser.close() # Close the browser when finished