camel-ai 0.2.72a10__py3-none-any.whl → 0.2.73a1__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 (37) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +113 -338
  3. camel/memories/agent_memories.py +18 -17
  4. camel/societies/workforce/prompts.py +10 -4
  5. camel/societies/workforce/single_agent_worker.py +7 -5
  6. camel/toolkits/__init__.py +6 -1
  7. camel/toolkits/base.py +57 -1
  8. camel/toolkits/hybrid_browser_toolkit/config_loader.py +136 -413
  9. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +796 -1631
  10. camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4356 -0
  11. camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
  12. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
  13. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +945 -0
  14. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +226 -0
  15. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +522 -0
  16. camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
  17. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +110 -0
  18. camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +26 -0
  19. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +210 -0
  20. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +533 -0
  21. camel/toolkits/message_integration.py +592 -0
  22. camel/toolkits/notion_mcp_toolkit.py +234 -0
  23. camel/toolkits/screenshot_toolkit.py +116 -31
  24. camel/toolkits/search_toolkit.py +20 -2
  25. camel/toolkits/terminal_toolkit.py +16 -2
  26. camel/toolkits/video_analysis_toolkit.py +13 -13
  27. camel/toolkits/video_download_toolkit.py +11 -11
  28. {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73a1.dist-info}/METADATA +12 -6
  29. {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73a1.dist-info}/RECORD +31 -24
  30. camel/toolkits/hybrid_browser_toolkit/actions.py +0 -417
  31. camel/toolkits/hybrid_browser_toolkit/agent.py +0 -311
  32. camel/toolkits/hybrid_browser_toolkit/browser_session.py +0 -740
  33. camel/toolkits/hybrid_browser_toolkit/snapshot.py +0 -227
  34. camel/toolkits/hybrid_browser_toolkit/stealth_script.js +0 -0
  35. camel/toolkits/hybrid_browser_toolkit/unified_analyzer.js +0 -1002
  36. {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73a1.dist-info}/WHEEL +0 -0
  37. {camel_ai-0.2.72a10.dist-info → camel_ai-0.2.73a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,234 @@
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
+ from typing import Any, ClassVar, Dict, List, Optional, Set
16
+
17
+ from camel.toolkits import BaseToolkit, FunctionTool
18
+
19
+ from .mcp_toolkit import MCPToolkit
20
+
21
+
22
+ class NotionMCPToolkit(BaseToolkit):
23
+ r"""NotionMCPToolkit provides an interface for interacting with Notion
24
+ through the Model Context Protocol (MCP).
25
+
26
+ Attributes:
27
+ timeout (Optional[float]): Connection timeout in seconds.
28
+ (default: :obj:`None`)
29
+
30
+ Note:
31
+ Currently only supports asynchronous operation mode.
32
+ """
33
+
34
+ # TODO: Create unified method to validate and fix the schema
35
+ SCHEMA_KEYWORDS: ClassVar[Set[str]] = {
36
+ "type",
37
+ "properties",
38
+ "items",
39
+ "required",
40
+ "additionalProperties",
41
+ "description",
42
+ "title",
43
+ "default",
44
+ "enum",
45
+ "const",
46
+ "examples",
47
+ "$ref",
48
+ "$defs",
49
+ "definitions",
50
+ "allOf",
51
+ "oneOf",
52
+ "anyOf",
53
+ "not",
54
+ "if",
55
+ "then",
56
+ "else",
57
+ "format",
58
+ "pattern",
59
+ "minimum",
60
+ "maximum",
61
+ "minLength",
62
+ "maxLength",
63
+ "minItems",
64
+ "maxItems",
65
+ "uniqueItems",
66
+ }
67
+
68
+ def __init__(
69
+ self,
70
+ timeout: Optional[float] = None,
71
+ ) -> None:
72
+ r"""Initializes the NotionMCPToolkit.
73
+
74
+ Args:
75
+ timeout (Optional[float]): Connection timeout in seconds.
76
+ (default: :obj:`None`)
77
+ """
78
+ super().__init__(timeout=timeout)
79
+
80
+ self._mcp_toolkit = MCPToolkit(
81
+ config_dict={
82
+ "mcpServers": {
83
+ "notionMCP": {
84
+ "command": "npx",
85
+ "args": [
86
+ "-y",
87
+ "mcp-remote",
88
+ "https://mcp.notion.com/mcp",
89
+ ],
90
+ }
91
+ }
92
+ },
93
+ timeout=timeout,
94
+ )
95
+
96
+ async def connect(self):
97
+ r"""Explicitly connect to the Notion MCP server."""
98
+ await self._mcp_toolkit.connect()
99
+
100
+ async def disconnect(self):
101
+ r"""Explicitly disconnect from the Notion MCP server."""
102
+ await self._mcp_toolkit.disconnect()
103
+
104
+ def get_tools(self) -> List[FunctionTool]:
105
+ r"""Returns a list of tools provided by the NotionMCPToolkit.
106
+
107
+ Returns:
108
+ List[FunctionTool]: List of available tools.
109
+ """
110
+ all_tools = []
111
+ for client in self._mcp_toolkit.clients:
112
+ try:
113
+ original_build_schema = client._build_tool_schema
114
+
115
+ def create_wrapper(orig_func):
116
+ def wrapper(mcp_tool):
117
+ return self._build_notion_tool_schema(
118
+ mcp_tool, orig_func
119
+ )
120
+
121
+ return wrapper
122
+
123
+ client._build_tool_schema = create_wrapper( # type: ignore[method-assign]
124
+ original_build_schema
125
+ )
126
+
127
+ client_tools = client.get_tools()
128
+ all_tools.extend(client_tools)
129
+
130
+ client._build_tool_schema = original_build_schema # type: ignore[method-assign]
131
+
132
+ except Exception as e:
133
+ from camel.logger import get_logger
134
+
135
+ logger = get_logger(__name__)
136
+ logger.error(f"Failed to get tools from client: {e}")
137
+
138
+ return all_tools
139
+
140
+ def _build_notion_tool_schema(self, mcp_tool, original_build_schema):
141
+ r"""Build tool schema with Notion-specific fixes."""
142
+ schema = original_build_schema(mcp_tool)
143
+ self._fix_notion_schema_recursively(schema)
144
+ return schema
145
+
146
+ def _fix_notion_schema_recursively(self, obj: Any) -> None:
147
+ r"""Recursively fix Notion MCP schema issues."""
148
+ if isinstance(obj, dict):
149
+ self._fix_dict_schema(obj)
150
+ self._process_nested_structures(obj)
151
+ elif isinstance(obj, list):
152
+ for item in obj:
153
+ self._fix_notion_schema_recursively(item)
154
+
155
+ def _fix_dict_schema(self, obj: Dict[str, Any]) -> None:
156
+ r"""Fix dictionary schema issues."""
157
+ if "properties" in obj and "type" not in obj:
158
+ self._fix_missing_type_with_properties(obj)
159
+ elif obj.get("type") == "object" and "properties" in obj:
160
+ self._fix_object_with_properties(obj)
161
+
162
+ def _fix_missing_type_with_properties(self, obj: Dict[str, Any]) -> None:
163
+ r"""Fix objects with properties but missing type field."""
164
+ properties = obj.get("properties", {})
165
+ if properties and isinstance(properties, dict):
166
+ obj["type"] = "object"
167
+ obj["additionalProperties"] = False
168
+
169
+ required_properties = self._get_required_properties(
170
+ properties, conservative=True
171
+ )
172
+ if required_properties:
173
+ obj["required"] = required_properties
174
+
175
+ def _fix_object_with_properties(self, obj: Dict[str, Any]) -> None:
176
+ r"""Fix objects with type="object" and properties."""
177
+ properties = obj.get("properties", {})
178
+ if properties and isinstance(properties, dict):
179
+ existing_required = obj.get("required", [])
180
+
181
+ for prop_name, prop_schema in properties.items():
182
+ if (
183
+ prop_name not in existing_required
184
+ and prop_name not in self.SCHEMA_KEYWORDS
185
+ and self._is_property_required(prop_schema)
186
+ ):
187
+ existing_required.append(prop_name)
188
+
189
+ if existing_required:
190
+ obj["required"] = existing_required
191
+
192
+ if "additionalProperties" not in obj:
193
+ obj["additionalProperties"] = False
194
+
195
+ def _get_required_properties(
196
+ self, properties: Dict[str, Any], conservative: bool = False
197
+ ) -> List[str]:
198
+ r"""Get list of required properties from a properties dict."""
199
+ required = []
200
+ for prop_name, prop_schema in properties.items():
201
+ if (
202
+ prop_name not in self.SCHEMA_KEYWORDS
203
+ and isinstance(prop_schema, dict)
204
+ and self._is_property_required(prop_schema)
205
+ ):
206
+ required.append(prop_name)
207
+ return required
208
+
209
+ def _is_property_required(self, prop_schema: Dict[str, Any]) -> bool:
210
+ r"""Check if a property should be marked as required."""
211
+ prop_type = prop_schema.get("type")
212
+ return (
213
+ prop_type is not None
214
+ and prop_type != "null"
215
+ and "default" not in prop_schema
216
+ and not (isinstance(prop_type, list) and "null" in prop_type)
217
+ )
218
+
219
+ def _process_nested_structures(self, obj: Dict[str, Any]) -> None:
220
+ r"""Process all nested structures in a schema object."""
221
+ for key, value in obj.items():
222
+ if key in ["anyOf", "oneOf", "allOf"] and isinstance(value, list):
223
+ for item in value:
224
+ self._fix_notion_schema_recursively(item)
225
+ elif key == "items" and isinstance(value, dict):
226
+ self._fix_notion_schema_recursively(value)
227
+ elif key == "properties" and isinstance(value, dict):
228
+ for prop_value in value.values():
229
+ self._fix_notion_schema_recursively(prop_value)
230
+ elif key == "$defs" and isinstance(value, dict):
231
+ for def_value in value.values():
232
+ self._fix_notion_schema_recursively(def_value)
233
+ elif isinstance(value, (dict, list)):
234
+ self._fix_notion_schema_recursively(value)
@@ -12,22 +12,22 @@
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
14
 
15
- import base64
16
- import io
17
15
  import os
18
- import time
19
16
  from pathlib import Path
20
17
  from typing import List, Optional
21
18
 
19
+ from PIL import Image
20
+
22
21
  from camel.logger import get_logger
22
+ from camel.messages import BaseMessage
23
23
  from camel.toolkits import BaseToolkit, FunctionTool
24
+ from camel.toolkits.base import RegisteredAgentToolkit
24
25
  from camel.utils import dependencies_required
25
- from camel.utils.tool_result import ToolResult
26
26
 
27
27
  logger = get_logger(__name__)
28
28
 
29
29
 
30
- class ScreenshotToolkit(BaseToolkit):
30
+ class ScreenshotToolkit(BaseToolkit, RegisteredAgentToolkit):
31
31
  r"""A toolkit for taking screenshots."""
32
32
 
33
33
  @dependencies_required('PIL')
@@ -50,6 +50,7 @@ class ScreenshotToolkit(BaseToolkit):
50
50
  from PIL import ImageGrab
51
51
 
52
52
  super().__init__(timeout=timeout)
53
+ RegisteredAgentToolkit.__init__(self)
53
54
 
54
55
  camel_workdir = os.environ.get("CAMEL_WORKDIR")
55
56
  if working_directory:
@@ -60,25 +61,101 @@ class ScreenshotToolkit(BaseToolkit):
60
61
  path = Path("camel_working_dir")
61
62
 
62
63
  self.ImageGrab = ImageGrab
63
- self.screenshots_dir = path / "screenshots"
64
+ self.screenshots_dir = path
64
65
  self.screenshots_dir.mkdir(parents=True, exist_ok=True)
65
66
 
66
- def take_screenshot(
67
+ def read_image(
68
+ self,
69
+ image_path: str,
70
+ instruction: str = "",
71
+ ) -> str:
72
+ r"""Analyzes an image from a local file path.
73
+
74
+ This function enables you to "see" and interpret an image from a
75
+ file. It's useful for tasks where you need to understand visual
76
+ information, such as reading a screenshot of a webpage or a diagram.
77
+
78
+ Args:
79
+ image_path (str): The local file path to the image.
80
+ For example: 'screenshots/login_page.png'.
81
+ instruction (str, optional): Specific instructions for what to look
82
+ for or what to do with the image. For example: "What is the
83
+ main headline on this page?" or "Find the 'Submit' button.".
84
+
85
+ Returns:
86
+ str: The response after analyzing the image, which could be a
87
+ description, an answer, or a confirmation of an action.
88
+ """
89
+ if self.agent is None:
90
+ logger.error(
91
+ "Cannot record screenshot in memory: No agent registered. "
92
+ "Please pass this toolkit to ChatAgent via "
93
+ "toolkits_to_register_agent parameter."
94
+ )
95
+ return (
96
+ "Error: No agent registered. Please pass this toolkit to "
97
+ "ChatAgent via toolkits_to_register_agent parameter."
98
+ )
99
+
100
+ try:
101
+ image_path = str(Path(image_path).absolute())
102
+
103
+ # Check if file exists before trying to open
104
+ if not os.path.exists(image_path):
105
+ error_msg = f"Screenshot file not found: {image_path}"
106
+ logger.error(error_msg)
107
+ return f"Error: {error_msg}"
108
+
109
+ # Load the image from the path
110
+ img = Image.open(image_path)
111
+
112
+ # Create a message with the screenshot image
113
+ message = BaseMessage.make_user_message(
114
+ role_name="User",
115
+ content=instruction,
116
+ image_list=[img],
117
+ )
118
+
119
+ # Record the message in agent's memory
120
+ response = self.agent.step(message)
121
+ return response.msgs[0].content
122
+
123
+ except Exception as e:
124
+ logger.error(f"Error reading screenshot: {e}")
125
+ return f"Error reading screenshot: {e}"
126
+
127
+ def take_screenshot_and_read_image(
67
128
  self,
129
+ filename: str,
68
130
  save_to_file: bool = True,
69
- ) -> ToolResult:
70
- r"""Take a screenshot of the entire screen and return it as a
71
- base64-encoded image.
131
+ read_image: bool = True,
132
+ instruction: Optional[str] = None,
133
+ ) -> str:
134
+ r"""Captures a screenshot of the entire screen.
135
+
136
+ This function can save the screenshot to a file and optionally analyze
137
+ it. It's useful for capturing the current state of the UI for
138
+ documentation, analysis, or to guide subsequent actions.
72
139
 
73
140
  Args:
74
- save_to_file (bool): Whether to save the screenshot to a file.
141
+ filename (str): The name for the screenshot file (e.g.,
142
+ "homepage.png"). The file is saved in a `screenshots`
143
+ subdirectory within the working directory. Must end with
144
+ `.png`. (default: :obj:`None`)
145
+ save_to_file (bool, optional): If `True`, saves the screenshot to
146
+ a file. (default: :obj:`True`)
147
+ read_image (bool, optional): If `True`, the agent will analyze
148
+ the screenshot. `save_to_file` must also be `True`.
75
149
  (default: :obj:`True`)
150
+ instruction (Optional[str], optional): A specific question or
151
+ command for the agent regarding the screenshot, used only if
152
+ `read_image` is `True`. For example: "Confirm that the
153
+ user is logged in.".
76
154
 
77
155
  Returns:
78
- ToolResult: An object containing:
79
- - text (str): A description of the screenshot.
80
- - images (List[str]): A list containing one base64-encoded
81
- PNG image data URL.
156
+ str: A confirmation message indicating success or failure,
157
+ including the file path if saved, and the agent's response
158
+ if `read_image` is `True`.
82
159
  """
83
160
  try:
84
161
  # Take screenshot of entire screen
@@ -90,32 +167,39 @@ class ScreenshotToolkit(BaseToolkit):
90
167
  # Create directory if it doesn't exist
91
168
  os.makedirs(self.screenshots_dir, exist_ok=True)
92
169
 
93
- # Generate filename with timestamp
94
- timestamp = int(time.time())
95
- filename = f"screenshot_{timestamp}.png"
96
- file_path = os.path.join(self.screenshots_dir, filename)
170
+ # Create unique filename if file already exists
171
+ base_path = os.path.join(self.screenshots_dir, filename)
172
+ file_path = base_path
173
+ counter = 1
174
+ while os.path.exists(file_path):
175
+ name, ext = os.path.splitext(filename)
176
+ unique_filename = f"{name}_{counter}{ext}"
177
+ file_path = os.path.join(
178
+ self.screenshots_dir, unique_filename
179
+ )
180
+ counter += 1
181
+
97
182
  screenshot.save(file_path)
98
183
  logger.info(f"Screenshot saved to {file_path}")
99
184
 
100
- # Convert to base64
101
- img_buffer = io.BytesIO()
102
- screenshot.save(img_buffer, format="PNG")
103
- img_buffer.seek(0)
104
- img_base64 = base64.b64encode(img_buffer.getvalue()).decode(
105
- 'utf-8'
106
- )
107
- img_data_url = f"data:image/png;base64,{img_base64}"
108
-
109
185
  # Create result text
110
186
  result_text = "Screenshot captured successfully"
111
187
  if file_path:
112
188
  result_text += f" and saved to {file_path}"
113
189
 
114
- return ToolResult(text=result_text, images=[img_data_url])
190
+ # Record in agent memory if requested
191
+ if read_image and file_path is not None:
192
+ inst = instruction if instruction is not None else ""
193
+ response = self.read_image(
194
+ str(Path(file_path).absolute()), inst
195
+ )
196
+ result_text += f". Agent response: {response}"
197
+
198
+ return result_text
115
199
 
116
200
  except Exception as e:
117
201
  logger.error(f"Error taking screenshot: {e}")
118
- return ToolResult(text=f"Error taking screenshot: {e}", images=[])
202
+ return f"Error taking screenshot: {e}"
119
203
 
120
204
  def get_tools(self) -> List[FunctionTool]:
121
205
  r"""Returns a list of FunctionTool objects for screenshot operations.
@@ -124,5 +208,6 @@ class ScreenshotToolkit(BaseToolkit):
124
208
  List[FunctionTool]: List of screenshot functions.
125
209
  """
126
210
  return [
127
- FunctionTool(self.take_screenshot),
211
+ FunctionTool(self.take_screenshot_and_read_image),
212
+ FunctionTool(self.read_image),
128
213
  ]
@@ -36,6 +36,7 @@ class SearchToolkit(BaseToolkit):
36
36
  self,
37
37
  timeout: Optional[float] = None,
38
38
  number_of_result_pages: int = 10,
39
+ exclude_domains: Optional[List[str]] = None,
39
40
  ):
40
41
  r"""Initializes the RedditToolkit with the specified number of retries
41
42
  and delay.
@@ -45,9 +46,14 @@ class SearchToolkit(BaseToolkit):
45
46
  (default: :obj:`None`)
46
47
  number_of_result_pages (int): The number of result pages to
47
48
  retrieve. (default: :obj:`10`)
49
+ exclude_domains (Optional[List[str]]): List of domains to
50
+ exclude from search results. Currently only supported
51
+ by the `search_google` function.
52
+ (default: :obj:`None`)
48
53
  """
49
54
  super().__init__(timeout=timeout)
50
55
  self.number_of_result_pages = number_of_result_pages
56
+ self.exclude_domains = exclude_domains
51
57
 
52
58
  @dependencies_required("wikipedia")
53
59
  def search_wiki(self, entity: str) -> str:
@@ -435,7 +441,9 @@ class SearchToolkit(BaseToolkit):
435
441
  ]
436
442
  )
437
443
  def search_google(
438
- self, query: str, search_type: str = "web"
444
+ self,
445
+ query: str,
446
+ search_type: str = "web",
439
447
  ) -> List[Dict[str, Any]]:
440
448
  r"""Use Google search engine to search information for the given query.
441
449
 
@@ -499,11 +507,21 @@ class SearchToolkit(BaseToolkit):
499
507
  start_page_idx = 1
500
508
  # Different language may get different result
501
509
  search_language = "en"
510
+
511
+ modified_query = query
512
+ if self.exclude_domains:
513
+ # Use Google's -site: operator to exclude domains
514
+ exclusion_terms = " ".join(
515
+ [f"-site:{domain}" for domain in self.exclude_domains]
516
+ )
517
+ modified_query = f"{query} {exclusion_terms}"
518
+ logger.debug(f"Excluded domains, modified query: {modified_query}")
519
+
502
520
  # Constructing the URL
503
521
  # Doc: https://developers.google.com/custom-search/v1/using_rest
504
522
  base_url = (
505
523
  f"https://www.googleapis.com/customsearch/v1?"
506
- f"key={GOOGLE_API_KEY}&cx={SEARCH_ENGINE_ID}&q={query}&start="
524
+ f"key={GOOGLE_API_KEY}&cx={SEARCH_ENGINE_ID}&q={modified_query}&start="
507
525
  f"{start_page_idx}&lr={search_language}&num={self.number_of_result_pages}"
508
526
  )
509
527
 
@@ -319,7 +319,14 @@ class TerminalToolkit(BaseToolkit):
319
319
  )
320
320
 
321
321
  # Install essential packages using uv
322
- essential_packages = ["pip", "setuptools", "wheel"]
322
+ essential_packages = [
323
+ "pip",
324
+ "setuptools",
325
+ "wheel",
326
+ "pyautogui",
327
+ "plotly",
328
+ "ffmpeg",
329
+ ]
323
330
  subprocess.run(
324
331
  [
325
332
  "uv",
@@ -371,7 +378,14 @@ class TerminalToolkit(BaseToolkit):
371
378
  pip_path = os.path.join(self.initial_env_path, "bin", "pip")
372
379
 
373
380
  # Upgrade pip and install essential packages
374
- essential_packages = ["pip", "setuptools", "wheel"]
381
+ essential_packages = [
382
+ "pip",
383
+ "setuptools",
384
+ "wheel",
385
+ "pyautogui",
386
+ "plotly",
387
+ "ffmpeg",
388
+ ]
375
389
  subprocess.run(
376
390
  [pip_path, "install", "--upgrade", *essential_packages],
377
391
  check=True,
@@ -97,7 +97,7 @@ class VideoAnalysisToolkit(BaseToolkit):
97
97
  r"""A class for analysing videos with vision-language model.
98
98
 
99
99
  Args:
100
- download_directory (Optional[str], optional): The directory where the
100
+ working_directory (Optional[str], optional): The directory where the
101
101
  video will be downloaded to. If not provided, video will be stored
102
102
  in a temporary directory and will be cleaned up after use.
103
103
  (default: :obj:`None`)
@@ -123,7 +123,7 @@ class VideoAnalysisToolkit(BaseToolkit):
123
123
  @dependencies_required("ffmpeg", "scenedetect")
124
124
  def __init__(
125
125
  self,
126
- download_directory: Optional[str] = None,
126
+ working_directory: Optional[str] = None,
127
127
  model: Optional[BaseModelBackend] = None,
128
128
  use_audio_transcription: bool = False,
129
129
  use_ocr: bool = False,
@@ -133,30 +133,30 @@ class VideoAnalysisToolkit(BaseToolkit):
133
133
  timeout: Optional[float] = None,
134
134
  ) -> None:
135
135
  super().__init__(timeout=timeout)
136
- self._cleanup = download_directory is None
136
+ self._cleanup = working_directory is None
137
137
  self._temp_files: list[str] = [] # Track temporary files for cleanup
138
138
  self._use_audio_transcription = use_audio_transcription
139
139
  self._use_ocr = use_ocr
140
140
  self.output_language = output_language
141
141
  self.frame_interval = frame_interval
142
142
 
143
- self._download_directory = Path(
144
- download_directory or tempfile.mkdtemp()
143
+ self._working_directory = Path(
144
+ working_directory or tempfile.mkdtemp()
145
145
  ).resolve()
146
146
 
147
147
  self.video_downloader_toolkit = VideoDownloaderToolkit(
148
- download_directory=str(self._download_directory),
148
+ working_directory=str(self._working_directory),
149
149
  cookies_path=cookies_path,
150
150
  )
151
151
 
152
152
  try:
153
- self._download_directory.mkdir(parents=True, exist_ok=True)
153
+ self._working_directory.mkdir(parents=True, exist_ok=True)
154
154
  except OSError as e:
155
155
  raise ValueError(
156
- f"Error creating directory {self._download_directory}: {e}"
156
+ f"Error creating directory {self._working_directory}: {e}"
157
157
  )
158
158
 
159
- logger.info(f"Video will be downloaded to {self._download_directory}")
159
+ logger.info(f"Video will be downloaded to {self._working_directory}")
160
160
 
161
161
  self.vl_model = model
162
162
  # Ensure ChatAgent is initialized with a model if provided
@@ -206,16 +206,16 @@ class VideoAnalysisToolkit(BaseToolkit):
206
206
  )
207
207
 
208
208
  # Clean up temporary directory if needed
209
- if self._cleanup and os.path.exists(self._download_directory):
209
+ if self._cleanup and os.path.exists(self._working_directory):
210
210
  try:
211
211
  import sys
212
212
 
213
213
  if getattr(sys, 'modules', None) is not None:
214
214
  import shutil
215
215
 
216
- shutil.rmtree(self._download_directory)
216
+ shutil.rmtree(self._working_directory)
217
217
  logger.debug(
218
- f"Removed temp directory: {self._download_directory}"
218
+ f"Removed temp directory: {self._working_directory}"
219
219
  )
220
220
  except (ImportError, AttributeError):
221
221
  # Skip cleanup if interpreter is shutting down
@@ -223,7 +223,7 @@ class VideoAnalysisToolkit(BaseToolkit):
223
223
  except OSError as e:
224
224
  logger.warning(
225
225
  f"Failed to remove temporary directory "
226
- f"{self._download_directory}: {e}"
226
+ f"{self._working_directory}: {e}"
227
227
  )
228
228
 
229
229
  @dependencies_required("pytesseract", "cv2", "numpy")
@@ -62,7 +62,7 @@ class VideoDownloaderToolkit(BaseToolkit):
62
62
  chunks.
63
63
 
64
64
  Args:
65
- download_directory (Optional[str], optional): The directory where the
65
+ working_directory (Optional[str], optional): The directory where the
66
66
  video will be downloaded to. If not provided, video will be stored
67
67
  in a temporary directory and will be cleaned up after use.
68
68
  (default: :obj:`None`)
@@ -73,30 +73,30 @@ class VideoDownloaderToolkit(BaseToolkit):
73
73
  @dependencies_required("yt_dlp", "ffmpeg")
74
74
  def __init__(
75
75
  self,
76
- download_directory: Optional[str] = None,
76
+ working_directory: Optional[str] = None,
77
77
  cookies_path: Optional[str] = None,
78
78
  timeout: Optional[float] = None,
79
79
  ) -> None:
80
80
  super().__init__(timeout=timeout)
81
- self._cleanup = download_directory is None
81
+ self._cleanup = working_directory is None
82
82
  self._cookies_path = cookies_path
83
83
 
84
- self._download_directory = Path(
85
- download_directory or tempfile.mkdtemp()
84
+ self._working_directory = Path(
85
+ working_directory or tempfile.mkdtemp()
86
86
  ).resolve()
87
87
 
88
88
  try:
89
- self._download_directory.mkdir(parents=True, exist_ok=True)
89
+ self._working_directory.mkdir(parents=True, exist_ok=True)
90
90
  except FileExistsError:
91
91
  raise ValueError(
92
- f"{self._download_directory} is not a valid directory."
92
+ f"{self._working_directory} is not a valid directory."
93
93
  )
94
94
  except OSError as e:
95
95
  raise ValueError(
96
- f"Error creating directory {self._download_directory}: {e}"
96
+ f"Error creating directory {self._working_directory}: {e}"
97
97
  )
98
98
 
99
- logger.info(f"Video will be downloaded to {self._download_directory}")
99
+ logger.info(f"Video will be downloaded to {self._working_directory}")
100
100
 
101
101
  def __del__(self) -> None:
102
102
  r"""Deconstructor for the VideoDownloaderToolkit class.
@@ -111,7 +111,7 @@ class VideoDownloaderToolkit(BaseToolkit):
111
111
  if getattr(sys, 'modules', None) is not None:
112
112
  import shutil
113
113
 
114
- shutil.rmtree(self._download_directory, ignore_errors=True)
114
+ shutil.rmtree(self._working_directory, ignore_errors=True)
115
115
  except (ImportError, AttributeError):
116
116
  # Skip cleanup if interpreter is shutting down
117
117
  pass
@@ -130,7 +130,7 @@ class VideoDownloaderToolkit(BaseToolkit):
130
130
  """
131
131
  import yt_dlp
132
132
 
133
- video_template = self._download_directory / "%(title)s.%(ext)s"
133
+ video_template = self._working_directory / "%(title)s.%(ext)s"
134
134
  ydl_opts = {
135
135
  'format': 'bestvideo+bestaudio/best',
136
136
  'outtmpl': str(video_template),