camel-ai 0.2.23a0__py3-none-any.whl → 0.2.24__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,371 @@
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
+
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+ from typing import List, Optional, Union
19
+
20
+ from camel.logger import get_logger
21
+ from camel.toolkits.base import BaseToolkit
22
+ from camel.toolkits.function_tool import FunctionTool
23
+
24
+ logger = get_logger(__name__)
25
+
26
+ # Default format when no extension is provided
27
+ DEFAULT_FORMAT = '.md'
28
+
29
+
30
+ class FileWriteToolkit(BaseToolkit):
31
+ r"""A toolkit for creating, writing, and modifying text in files.
32
+
33
+ This class provides cross-platform (macOS, Linux, Windows) support for
34
+ writing to various file formats (Markdown, DOCX, PDF, and plaintext),
35
+ replacing text in existing files, automatic backups, custom encoding,
36
+ and enhanced formatting options for specialized formats.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ output_dir: str = "./",
42
+ timeout: Optional[float] = None,
43
+ default_encoding: str = "utf-8",
44
+ backup_enabled: bool = True,
45
+ ) -> None:
46
+ r"""Initialize the FileWriteToolkit.
47
+
48
+ Args:
49
+ output_dir (str): The default directory for output files.
50
+ Defaults to the current working directory.
51
+ timeout (Optional[float]): The timeout for the toolkit.
52
+ (default: :obj: `None`)
53
+ default_encoding (str): Default character encoding for text
54
+ operations. (default: :obj: `utf-8`)
55
+ backup_enabled (bool): Whether to create backups of existing files
56
+ before overwriting. (default: :obj: `True`)
57
+ """
58
+ super().__init__(timeout=timeout)
59
+ self.output_dir = Path(output_dir).resolve()
60
+ self.output_dir.mkdir(parents=True, exist_ok=True)
61
+ self.default_encoding = default_encoding
62
+ self.backup_enabled = backup_enabled
63
+ logger.info(
64
+ f"FileWriteToolkit initialized with output directory"
65
+ f": {self.output_dir}, encoding: {default_encoding}"
66
+ )
67
+
68
+ def _resolve_filepath(self, file_path: str) -> Path:
69
+ r"""Convert the given string path to a Path object.
70
+
71
+ If the provided path is not absolute, it is made relative to the
72
+ default output directory.
73
+
74
+ Args:
75
+ file_path (str): The file path to resolve.
76
+
77
+ Returns:
78
+ Path: A fully resolved (absolute) Path object.
79
+ """
80
+ path_obj = Path(file_path)
81
+ if not path_obj.is_absolute():
82
+ path_obj = self.output_dir / path_obj
83
+ return path_obj.resolve()
84
+
85
+ def _write_text_file(
86
+ self, file_path: Path, content: str, encoding: str = "utf-8"
87
+ ) -> None:
88
+ r"""Write text content to a plaintext file.
89
+
90
+ Args:
91
+ file_path (Path): The target file path.
92
+ content (str): The text content to write.
93
+ encoding (str): Character encoding to use. (default: :obj: `utf-8`)
94
+ """
95
+ with file_path.open("w", encoding=encoding) as f:
96
+ f.write(content)
97
+ logger.debug(f"Wrote text to {file_path} with {encoding} encoding")
98
+
99
+ def _create_backup(self, file_path: Path) -> None:
100
+ r"""Create a backup of the file if it exists and backup is enabled.
101
+
102
+ Args:
103
+ file_path (Path): Path to the file to backup.
104
+ """
105
+ import shutil
106
+
107
+ if not self.backup_enabled or not file_path.exists():
108
+ return
109
+
110
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
111
+ backup_path = file_path.parent / f"{file_path.name}.{timestamp}.bak"
112
+ shutil.copy2(file_path, backup_path)
113
+ logger.info(f"Created backup at {backup_path}")
114
+
115
+ def _write_docx_file(self, file_path: Path, content: str) -> None:
116
+ r"""Write text content to a DOCX file with default formatting.
117
+
118
+ Args:
119
+ file_path (Path): The target file path.
120
+ content (str): The text content to write.
121
+ """
122
+ import docx
123
+
124
+ # Use default formatting values
125
+ font_name = 'Calibri'
126
+ font_size = 11
127
+ line_spacing = 1.0
128
+
129
+ document = docx.Document()
130
+ style = document.styles['Normal']
131
+ style.font.name = font_name
132
+ style.font.size = docx.shared.Pt(font_size)
133
+ style.paragraph_format.line_spacing = line_spacing
134
+
135
+ # Split content into paragraphs and add them
136
+ for para_text in content.split('\n'):
137
+ para = document.add_paragraph(para_text)
138
+ para.style = style
139
+
140
+ document.save(str(file_path))
141
+ logger.debug(f"Wrote DOCX to {file_path} with default formatting")
142
+
143
+ def _write_pdf_file(self, file_path: Path, content: str, **kwargs) -> None:
144
+ r"""Write text content to a PDF file with default formatting.
145
+
146
+ Args:
147
+ file_path (Path): The target file path.
148
+ content (str): The text content to write.
149
+
150
+ Raises:
151
+ RuntimeError: If the 'fpdf' library is not installed.
152
+ """
153
+ from fpdf import FPDF
154
+
155
+ # Use default formatting values
156
+ font_family = 'Arial'
157
+ font_size = 12
158
+ font_style = ''
159
+ line_height = 10
160
+ margin = 10
161
+
162
+ pdf = FPDF()
163
+ pdf.set_margins(margin, margin, margin)
164
+
165
+ pdf.add_page()
166
+ pdf.set_font(font_family, style=font_style, size=font_size)
167
+
168
+ # Split content into paragraphs and add them
169
+ for para in content.split('\n'):
170
+ if para.strip(): # Skip empty paragraphs
171
+ pdf.multi_cell(0, line_height, para)
172
+ else:
173
+ pdf.ln(line_height) # Add empty line
174
+
175
+ pdf.output(str(file_path))
176
+ logger.debug(f"Wrote PDF to {file_path} with custom formatting")
177
+
178
+ def _write_csv_file(
179
+ self,
180
+ file_path: Path,
181
+ content: Union[str, List[List]],
182
+ encoding: str = "utf-8",
183
+ ) -> None:
184
+ r"""Write CSV content to a file.
185
+
186
+ Args:
187
+ file_path (Path): The target file path.
188
+ content (Union[str, List[List]]): The CSV content as a string or
189
+ list of lists.
190
+ encoding (str): Character encoding to use. (default: :obj: `utf-8`)
191
+ """
192
+ import csv
193
+
194
+ with file_path.open("w", encoding=encoding, newline='') as f:
195
+ if isinstance(content, str):
196
+ f.write(content)
197
+ else:
198
+ writer = csv.writer(f)
199
+ writer.writerows(content)
200
+ logger.debug(f"Wrote CSV to {file_path} with {encoding} encoding")
201
+
202
+ def _write_json_file(
203
+ self,
204
+ file_path: Path,
205
+ content: str,
206
+ encoding: str = "utf-8",
207
+ ) -> None:
208
+ r"""Write JSON content to a file.
209
+
210
+ Args:
211
+ file_path (Path): The target file path.
212
+ content (str): The JSON content as a string.
213
+ encoding (str): Character encoding to use. (default: :obj: `utf-8`)
214
+ """
215
+ import json
216
+
217
+ with file_path.open("w", encoding=encoding) as f:
218
+ if isinstance(content, str):
219
+ try:
220
+ # Try parsing as JSON string first
221
+ data = json.loads(content)
222
+ json.dump(data, f)
223
+ except json.JSONDecodeError:
224
+ # If not valid JSON string, write as is
225
+ f.write(content)
226
+ else:
227
+ # If not string, dump as JSON
228
+ json.dump(content, f)
229
+ logger.debug(f"Wrote JSON to {file_path} with {encoding} encoding")
230
+
231
+ def _write_yaml_file(
232
+ self,
233
+ file_path: Path,
234
+ content: str,
235
+ encoding: str = "utf-8",
236
+ ) -> None:
237
+ r"""Write YAML content to a file.
238
+
239
+ Args:
240
+ file_path (Path): The target file path.
241
+ content (str): The YAML content as a string.
242
+ encoding (str): Character encoding to use. (default: :obj: `utf-8`)
243
+ """
244
+ with file_path.open("w", encoding=encoding) as f:
245
+ f.write(content)
246
+ logger.debug(f"Wrote YAML to {file_path} with {encoding} encoding")
247
+
248
+ def _write_html_file(
249
+ self, file_path: Path, content: str, encoding: str = "utf-8"
250
+ ) -> None:
251
+ r"""Write text content to an HTML file.
252
+
253
+ Args:
254
+ file_path (Path): The target file path.
255
+ content (str): The HTML content to write.
256
+ encoding (str): Character encoding to use. (default: :obj: `utf-8`)
257
+ """
258
+ with file_path.open("w", encoding=encoding) as f:
259
+ f.write(content)
260
+ logger.debug(f"Wrote HTML to {file_path} with {encoding} encoding")
261
+
262
+ def _write_markdown_file(
263
+ self, file_path: Path, content: str, encoding: str = "utf-8"
264
+ ) -> None:
265
+ r"""Write text content to a Markdown file.
266
+
267
+ Args:
268
+ file_path (Path): The target file path.
269
+ content (str): The Markdown content to write.
270
+ encoding (str): Character encoding to use. (default: :obj: `utf-8`)
271
+ """
272
+ with file_path.open("w", encoding=encoding) as f:
273
+ f.write(content)
274
+ logger.debug(f"Wrote Markdown to {file_path} with {encoding} encoding")
275
+
276
+ def write_to_file(
277
+ self,
278
+ content: Union[str, List[List[str]]],
279
+ filename: str,
280
+ encoding: Optional[str] = None,
281
+ ) -> str:
282
+ r"""Write the given content to a file.
283
+
284
+ If the file exists, it will be overwritten. Supports multiple formats:
285
+ Markdown (.md, .markdown, default), Plaintext (.txt), CSV (.csv),
286
+ DOC/DOCX (.doc, .docx), PDF (.pdf), JSON (.json), YAML (.yml, .yaml),
287
+ and HTML (.html, .htm).
288
+
289
+ Args:
290
+ content (Union[str, List[List[str]]]): The content to write to the
291
+ file. For all formats, content must be a string or list in the
292
+ appropriate format.
293
+ filename (str): The name or path of the file. If a relative path is
294
+ supplied, it is resolved to self.output_dir.
295
+ encoding (Optional[str]): The character encoding to use. (default:
296
+ :obj: `None`)
297
+
298
+ Returns:
299
+ str: A message indicating success or error details.
300
+ """
301
+ file_path = self._resolve_filepath(filename)
302
+ file_path.parent.mkdir(parents=True, exist_ok=True)
303
+
304
+ # Create backup if file exists
305
+ self._create_backup(file_path)
306
+
307
+ extension = file_path.suffix.lower()
308
+
309
+ # If no extension is provided, use the default format
310
+ if extension == "":
311
+ file_path = file_path.with_suffix(DEFAULT_FORMAT)
312
+ extension = DEFAULT_FORMAT
313
+
314
+ try:
315
+ # Get encoding or use default
316
+ file_encoding = encoding or self.default_encoding
317
+
318
+ if extension in [".doc", ".docx"]:
319
+ self._write_docx_file(file_path, str(content))
320
+ elif extension == ".pdf":
321
+ self._write_pdf_file(file_path, str(content))
322
+ elif extension == ".csv":
323
+ self._write_csv_file(
324
+ file_path, content, encoding=file_encoding
325
+ )
326
+ elif extension == ".json":
327
+ self._write_json_file(
328
+ file_path,
329
+ content, # type: ignore[arg-type]
330
+ encoding=file_encoding,
331
+ )
332
+ elif extension in [".yml", ".yaml"]:
333
+ self._write_yaml_file(
334
+ file_path, str(content), encoding=file_encoding
335
+ )
336
+ elif extension in [".html", ".htm"]:
337
+ self._write_html_file(
338
+ file_path, str(content), encoding=file_encoding
339
+ )
340
+ elif extension in [".md", ".markdown"]:
341
+ self._write_markdown_file(
342
+ file_path, str(content), encoding=file_encoding
343
+ )
344
+ else:
345
+ # Fallback to simple text writing for unknown or .txt
346
+ # extensions
347
+ self._write_text_file(
348
+ file_path, str(content), encoding=file_encoding
349
+ )
350
+
351
+ msg = f"Content successfully written to file: {file_path}"
352
+ logger.info(msg)
353
+ return msg
354
+ except Exception as e:
355
+ error_msg = (
356
+ f"Error occurred while writing to file {file_path}: {e}"
357
+ )
358
+ logger.error(error_msg)
359
+ return error_msg
360
+
361
+ def get_tools(self) -> List[FunctionTool]:
362
+ r"""Return a list of FunctionTool objects representing the functions
363
+ in the toolkit.
364
+
365
+ Returns:
366
+ List[FunctionTool]: A list of FunctionTool objects representing
367
+ the available functions in this toolkit.
368
+ """
369
+ return [
370
+ FunctionTool(self.write_to_file),
371
+ ]
@@ -0,0 +1,202 @@
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 io import BytesIO
16
+ from typing import List, Optional
17
+ from urllib.parse import urlparse
18
+
19
+ import requests
20
+ from PIL import Image
21
+
22
+ from camel.logger import get_logger
23
+ from camel.messages import BaseMessage
24
+ from camel.models import BaseModelBackend, ModelFactory
25
+ from camel.toolkits import FunctionTool
26
+ from camel.toolkits.base import BaseToolkit
27
+ from camel.types import ModelPlatformType, ModelType
28
+
29
+ logger = get_logger(__name__)
30
+
31
+
32
+ class ImageAnalysisToolkit(BaseToolkit):
33
+ r"""A toolkit for comprehensive image analysis and understanding.
34
+ The toolkit uses vision-capable language models to perform these tasks.
35
+ """
36
+
37
+ def __init__(self, model: Optional[BaseModelBackend] = None):
38
+ r"""Initialize the ImageAnalysisToolkit.
39
+
40
+ Args:
41
+ model (Optional[BaseModelBackend]): The model backend to use for
42
+ image analysis tasks. This model should support processing
43
+ images for tasks like image description and visual question
44
+ answering. If None, a default model will be created using
45
+ ModelFactory. (default: :obj:`None`)
46
+ """
47
+ if model:
48
+ self.model = model
49
+ else:
50
+ self.model = ModelFactory.create(
51
+ model_platform=ModelPlatformType.DEFAULT,
52
+ model_type=ModelType.DEFAULT,
53
+ )
54
+
55
+ def image_to_text(
56
+ self, image_path: str, sys_prompt: Optional[str] = None
57
+ ) -> str:
58
+ r"""Generates textual description of an image with optional custom
59
+ prompt.
60
+
61
+ Args:
62
+ image_path (str): Local path or URL to an image file.
63
+ sys_prompt (Optional[str]): Custom system prompt for the analysis.
64
+ (default: :obj:`None`)
65
+
66
+ Returns:
67
+ str: Natural language description of the image.
68
+ """
69
+ default_content = '''You are an image analysis expert. Provide a
70
+ detailed description including text if present.'''
71
+
72
+ system_msg = BaseMessage.make_assistant_message(
73
+ role_name="Senior Computer Vision Analyst",
74
+ content=sys_prompt if sys_prompt else default_content,
75
+ )
76
+
77
+ return self._analyze_image(
78
+ image_path=image_path,
79
+ prompt="Please describe the contents of this image.",
80
+ system_message=system_msg,
81
+ )
82
+
83
+ def ask_question_about_image(
84
+ self, image_path: str, question: str, sys_prompt: Optional[str] = None
85
+ ) -> str:
86
+ r"""Answers image questions with optional custom instructions.
87
+
88
+ Args:
89
+ image_path (str): Local path or URL to an image file.
90
+ question (str): Query about the image content.
91
+ sys_prompt (Optional[str]): Custom system prompt for the analysis.
92
+ (default: :obj:`None`)
93
+
94
+ Returns:
95
+ str: Detailed answer based on visual understanding
96
+ """
97
+ default_content = """Answer questions about images by:
98
+ 1. Careful visual inspection
99
+ 2. Contextual reasoning
100
+ 3. Text transcription where relevant
101
+ 4. Logical deduction from visual evidence"""
102
+
103
+ system_msg = BaseMessage.make_assistant_message(
104
+ role_name="Visual QA Specialist",
105
+ content=sys_prompt if sys_prompt else default_content,
106
+ )
107
+
108
+ return self._analyze_image(
109
+ image_path=image_path,
110
+ prompt=question,
111
+ system_message=system_msg,
112
+ )
113
+
114
+ def _load_image(self, image_path: str) -> Image.Image:
115
+ r"""Loads an image from either local path or URL.
116
+
117
+ Args:
118
+ image_path (str): Local path or URL to image.
119
+
120
+ Returns:
121
+ Image.Image: Loaded PIL Image object.
122
+
123
+ Raises:
124
+ ValueError: For invalid paths/URLs or unreadable images.
125
+ requests.exceptions.RequestException: For URL fetch failures.
126
+ """
127
+ parsed = urlparse(image_path)
128
+
129
+ if parsed.scheme in ("http", "https"):
130
+ logger.debug(f"Fetching image from URL: {image_path}")
131
+ try:
132
+ response = requests.get(image_path, timeout=15)
133
+ response.raise_for_status()
134
+ return Image.open(BytesIO(response.content))
135
+ except requests.exceptions.RequestException as e:
136
+ logger.error(f"URL fetch failed: {e}")
137
+ raise
138
+ else:
139
+ logger.debug(f"Loading local image: {image_path}")
140
+ try:
141
+ return Image.open(image_path)
142
+ except Exception as e:
143
+ logger.error(f"Image loading failed: {e}")
144
+ raise ValueError(f"Invalid image file: {e}")
145
+
146
+ def _analyze_image(
147
+ self,
148
+ image_path: str,
149
+ prompt: str,
150
+ system_message: BaseMessage,
151
+ ) -> str:
152
+ r"""Core analysis method handling image loading and processing.
153
+
154
+ Args:
155
+ image_path (str): Image location.
156
+ prompt (str): Analysis query/instructions.
157
+ system_message (BaseMessage): Custom system prompt for the
158
+ analysis.
159
+
160
+ Returns:
161
+ str: Analysis result or error message.
162
+ """
163
+ try:
164
+ image = self._load_image(image_path)
165
+ logger.info(f"Analyzing image: {image_path}")
166
+
167
+ from camel.agents.chat_agent import ChatAgent
168
+
169
+ agent = ChatAgent(
170
+ system_message=system_message,
171
+ model=self.model,
172
+ )
173
+
174
+ user_msg = BaseMessage.make_user_message(
175
+ role_name="User",
176
+ content=prompt,
177
+ image_list=[image],
178
+ )
179
+
180
+ response = agent.step(user_msg)
181
+ agent.reset()
182
+ return response.msgs[0].content
183
+
184
+ except (ValueError, requests.exceptions.RequestException) as e:
185
+ logger.error(f"Image handling error: {e}")
186
+ return f"Image error: {e!s}"
187
+ except Exception as e:
188
+ logger.error(f"Unexpected error: {e}")
189
+ return f"Analysis failed: {e!s}"
190
+
191
+ def get_tools(self) -> List[FunctionTool]:
192
+ r"""Returns a list of FunctionTool objects representing the functions
193
+ in the toolkit.
194
+
195
+ Returns:
196
+ List[FunctionTool]: A list of FunctionTool objects representing the
197
+ functions in the toolkit.
198
+ """
199
+ return [
200
+ FunctionTool(self.image_to_text),
201
+ FunctionTool(self.ask_question_about_image),
202
+ ]