camel-ai 0.2.61__py3-none-any.whl → 0.2.62__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 (32) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +1 -1
  3. camel/agents/mcp_agent.py +5 -5
  4. camel/{data_collector → data_collectors}/alpaca_collector.py +1 -1
  5. camel/{data_collector → data_collectors}/sharegpt_collector.py +1 -1
  6. camel/retrievers/auto_retriever.py +20 -1
  7. camel/{runtime → runtimes}/daytona_runtime.py +1 -1
  8. camel/{runtime → runtimes}/docker_runtime.py +1 -1
  9. camel/{runtime → runtimes}/llm_guard_runtime.py +2 -2
  10. camel/{runtime → runtimes}/remote_http_runtime.py +1 -1
  11. camel/{runtime → runtimes}/ubuntu_docker_runtime.py +1 -1
  12. camel/societies/workforce/base.py +7 -3
  13. camel/societies/workforce/single_agent_worker.py +2 -1
  14. camel/societies/workforce/worker.py +5 -3
  15. camel/toolkits/__init__.py +2 -0
  16. camel/toolkits/file_write_toolkit.py +4 -2
  17. camel/toolkits/mcp_toolkit.py +469 -733
  18. camel/toolkits/pptx_toolkit.py +777 -0
  19. camel/utils/mcp_client.py +979 -0
  20. {camel_ai-0.2.61.dist-info → camel_ai-0.2.62.dist-info}/METADATA +4 -1
  21. {camel_ai-0.2.61.dist-info → camel_ai-0.2.62.dist-info}/RECORD +32 -30
  22. /camel/{data_collector → data_collectors}/__init__.py +0 -0
  23. /camel/{data_collector → data_collectors}/base.py +0 -0
  24. /camel/{runtime → runtimes}/__init__.py +0 -0
  25. /camel/{runtime → runtimes}/api.py +0 -0
  26. /camel/{runtime → runtimes}/base.py +0 -0
  27. /camel/{runtime → runtimes}/configs.py +0 -0
  28. /camel/{runtime → runtimes}/utils/__init__.py +0 -0
  29. /camel/{runtime → runtimes}/utils/function_risk_toolkit.py +0 -0
  30. /camel/{runtime → runtimes}/utils/ignore_risk_toolkit.py +0 -0
  31. {camel_ai-0.2.61.dist-info → camel_ai-0.2.62.dist-info}/WHEEL +0 -0
  32. {camel_ai-0.2.61.dist-info → camel_ai-0.2.62.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,777 @@
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
+ import os
17
+ import random
18
+ import re
19
+ from pathlib import Path
20
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
21
+
22
+ if TYPE_CHECKING:
23
+ from pptx import presentation
24
+ from pptx.slide import Slide
25
+ from pptx.text.text import TextFrame
26
+
27
+ from camel.logger import get_logger
28
+ from camel.toolkits.base import BaseToolkit
29
+ from camel.toolkits.function_tool import FunctionTool
30
+ from camel.utils import MCPServer, api_keys_required
31
+
32
+ logger = get_logger(__name__)
33
+
34
+ # Constants
35
+ EMU_TO_INCH_SCALING_FACTOR = 1.0 / 914400
36
+
37
+ STEP_BY_STEP_PROCESS_MARKER = '>> '
38
+
39
+ IMAGE_DISPLAY_PROBABILITY = 1 / 3.0
40
+
41
+ SLIDE_NUMBER_REGEX = re.compile(r"^slide[ ]+\d+:", re.IGNORECASE)
42
+ BOLD_ITALICS_PATTERN = re.compile(r'(\*\*(.*?)\*\*|\*(.*?)\*)')
43
+
44
+
45
+ @MCPServer()
46
+ class PPTXToolkit(BaseToolkit):
47
+ r"""A toolkit for creating and writing PowerPoint presentations (PPTX
48
+ files).
49
+
50
+ This class provides cross-platform support for creating PPTX files with
51
+ title slides, content slides, text formatting, and image embedding.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ output_dir: str = "./",
57
+ timeout: Optional[float] = None,
58
+ ) -> None:
59
+ r"""Initialize the PPTXToolkit.
60
+
61
+ Args:
62
+ output_dir (str): The default directory for output files.
63
+ Defaults to the current working directory.
64
+ timeout (Optional[float]): The timeout for the toolkit.
65
+ (default: :obj: `None`)
66
+ """
67
+ super().__init__(timeout=timeout)
68
+ self.output_dir = Path(output_dir).resolve()
69
+ self.output_dir.mkdir(parents=True, exist_ok=True)
70
+ logger.info(
71
+ f"PPTXToolkit initialized with output directory: {self.output_dir}"
72
+ )
73
+
74
+ def _resolve_filepath(self, file_path: str) -> Path:
75
+ r"""Convert the given string path to a Path object.
76
+
77
+ If the provided path is not absolute, it is made relative to the
78
+ default output directory. The filename part is sanitized to replace
79
+ spaces and special characters with underscores, ensuring safe usage
80
+ in downstream processing.
81
+
82
+ Args:
83
+ file_path (str): The file path to resolve.
84
+
85
+ Returns:
86
+ Path: A fully resolved (absolute) and sanitized Path object.
87
+ """
88
+ path_obj = Path(file_path)
89
+ if not path_obj.is_absolute():
90
+ path_obj = self.output_dir / path_obj
91
+
92
+ sanitized_filename = self._sanitize_filename(path_obj.name)
93
+ path_obj = path_obj.parent / sanitized_filename
94
+ return path_obj.resolve()
95
+
96
+ def _sanitize_filename(self, filename: str) -> str:
97
+ r"""Sanitize a filename by replacing special characters and spaces.
98
+
99
+ Args:
100
+ filename (str): The filename to sanitize.
101
+
102
+ Returns:
103
+ str: The sanitized filename.
104
+ """
105
+ import re
106
+
107
+ # Replace spaces and special characters with underscores
108
+ sanitized = re.sub(r'[^\w\-_\.]', '_', filename)
109
+ # Remove multiple consecutive underscores
110
+ sanitized = re.sub(r'_+', '_', sanitized)
111
+ return sanitized
112
+
113
+ def _format_text(
114
+ self, frame_paragraph, text: str, set_color_to_white=False
115
+ ) -> None:
116
+ r"""Apply bold and italic formatting while preserving the original
117
+ word order.
118
+
119
+ Args:
120
+ frame_paragraph: The paragraph to format.
121
+ text (str): The text to format.
122
+ set_color_to_white (bool): Whether to set the color to white.
123
+ (default: :obj: `False`)
124
+ """
125
+ from pptx.dml.color import RGBColor
126
+
127
+ matches = list(BOLD_ITALICS_PATTERN.finditer(text))
128
+ last_index = 0
129
+
130
+ for match in matches:
131
+ start, end = match.span()
132
+ if start > last_index:
133
+ run = frame_paragraph.add_run()
134
+ run.text = text[last_index:start]
135
+ if set_color_to_white:
136
+ run.font.color.rgb = RGBColor(255, 255, 255)
137
+
138
+ if match.group(2): # Bold
139
+ run = frame_paragraph.add_run()
140
+ run.text = match.group(2)
141
+ run.font.bold = True
142
+ if set_color_to_white:
143
+ run.font.color.rgb = RGBColor(255, 255, 255)
144
+ elif match.group(3): # Italics
145
+ run = frame_paragraph.add_run()
146
+ run.text = match.group(3)
147
+ run.font.italic = True
148
+ if set_color_to_white:
149
+ run.font.color.rgb = RGBColor(255, 255, 255)
150
+
151
+ last_index = end
152
+
153
+ if last_index < len(text):
154
+ run = frame_paragraph.add_run()
155
+ run.text = text[last_index:]
156
+ if set_color_to_white:
157
+ run.font.color.rgb = RGBColor(255, 255, 255)
158
+
159
+ def _add_bulleted_items(
160
+ self,
161
+ text_frame: "TextFrame",
162
+ flat_items_list: List[Tuple[str, int]],
163
+ set_color_to_white: bool = False,
164
+ ) -> None:
165
+ r"""Add a list of texts as bullet points and apply formatting.
166
+
167
+ Args:
168
+ text_frame (TextFrame): The text frame where text is to be
169
+ displayed.
170
+ flat_items_list (List[Tuple[str, int]]): The list of items to be
171
+ displayed.
172
+ set_color_to_white (bool): Whether to set the font color to white.
173
+ (default: :obj: `False`)
174
+ """
175
+ if not flat_items_list:
176
+ logger.warning("Empty bullet point list provided")
177
+ return
178
+ for idx, item_content in enumerate(flat_items_list):
179
+ item_text, item_level = item_content
180
+
181
+ if idx == 0:
182
+ if not text_frame.paragraphs:
183
+ # Ensure a paragraph exists if the frame is empty or
184
+ # cleared
185
+ paragraph = text_frame.add_paragraph()
186
+ else:
187
+ # Use the first existing paragraph
188
+ paragraph = text_frame.paragraphs[0]
189
+ else:
190
+ paragraph = text_frame.add_paragraph()
191
+
192
+ paragraph.level = item_level
193
+
194
+ self._format_text(
195
+ paragraph,
196
+ item_text.removeprefix(STEP_BY_STEP_PROCESS_MARKER),
197
+ set_color_to_white=set_color_to_white,
198
+ )
199
+
200
+ def _get_flat_list_of_contents(
201
+ self, items: List[Union[str, List[Any]]], level: int
202
+ ) -> List[Tuple[str, int]]:
203
+ r"""Flatten a hierarchical list of bullet points to a single list.
204
+
205
+ Args:
206
+ items (List[Union[str, List[Any]]]): A bullet point (string or
207
+ list).
208
+ level (int): The current level of hierarchy.
209
+
210
+ Returns:
211
+ List[Tuple[str, int]]: A list of (bullet item text, hierarchical
212
+ level) tuples.
213
+ """
214
+ flat_list = []
215
+
216
+ for item in items:
217
+ if isinstance(item, str):
218
+ flat_list.append((item, level))
219
+ elif isinstance(item, list):
220
+ flat_list.extend(
221
+ self._get_flat_list_of_contents(item, level + 1)
222
+ )
223
+
224
+ return flat_list
225
+
226
+ def _get_slide_width_height_inches(
227
+ self, presentation: "presentation.Presentation"
228
+ ) -> Tuple[float, float]:
229
+ r"""Get the dimensions of a slide in inches.
230
+
231
+ Args:
232
+ presentation (presentation.Presentation): The presentation object.
233
+
234
+ Returns:
235
+ Tuple[float, float]: The width and height in inches.
236
+ """
237
+ slide_width_inch = EMU_TO_INCH_SCALING_FACTOR * (
238
+ presentation.slide_width or 0
239
+ )
240
+ slide_height_inch = EMU_TO_INCH_SCALING_FACTOR * (
241
+ presentation.slide_height or 0
242
+ )
243
+ return slide_width_inch, slide_height_inch
244
+
245
+ def _write_pptx_file(
246
+ self,
247
+ file_path: Path,
248
+ content: List[Dict[str, Any]],
249
+ template: Optional[str] = None,
250
+ ) -> None:
251
+ r"""Write text content to a PPTX file with enhanced formatting.
252
+
253
+ Args:
254
+ file_path (Path): The target file path.
255
+ content (List[Dict[str, Any]]): The content to write to the PPTX
256
+ file. Must be a list of dictionaries where:
257
+ - First element: Title slide with keys 'title' and 'subtitle'
258
+ - Subsequent elements: Content slides with keys 'title', 'text'
259
+ template (Optional[str]): The name of the template to use. If not
260
+ provided, the default template will be used. (default: :obj:
261
+ `None`)
262
+ """
263
+ import pptx
264
+
265
+ # Use template if provided, otherwise create new presentation
266
+ if template is not None:
267
+ template_path = Path(template).resolve()
268
+ if not template_path.exists():
269
+ logger.warning(
270
+ f"Template file not found: {template_path}, using "
271
+ "default template"
272
+ )
273
+ presentation = pptx.Presentation()
274
+ else:
275
+ presentation = pptx.Presentation(str(template_path))
276
+ # Clear all existing slides by removing them from the slide
277
+ # list
278
+ while len(presentation.slides) > 0:
279
+ rId = presentation.slides._sldIdLst[-1].rId
280
+ presentation.part.drop_rel(rId)
281
+ del presentation.slides._sldIdLst[-1]
282
+ else:
283
+ presentation = pptx.Presentation()
284
+
285
+ slide_width_inch, slide_height_inch = (
286
+ self._get_slide_width_height_inches(presentation)
287
+ )
288
+
289
+ # Process slides
290
+ if content:
291
+ # Title slide (first element)
292
+ title_slide_data = content.pop(0) if content else {}
293
+ title_layout = presentation.slide_layouts[0]
294
+ title_slide = presentation.slides.add_slide(title_layout)
295
+
296
+ # Set title and subtitle
297
+ if title_slide.shapes.title:
298
+ title_slide.shapes.title.text_frame.clear()
299
+ self._format_text(
300
+ title_slide.shapes.title.text_frame.paragraphs[0],
301
+ title_slide_data.get("title", ""),
302
+ )
303
+
304
+ if len(title_slide.placeholders) > 1:
305
+ subtitle = title_slide.placeholders[1]
306
+ subtitle.text_frame.clear()
307
+ self._format_text(
308
+ subtitle.text_frame.paragraphs[0],
309
+ title_slide_data.get("subtitle", ""),
310
+ )
311
+
312
+ # Content slides
313
+ for slide_data in content:
314
+ if not isinstance(slide_data, dict):
315
+ continue
316
+
317
+ # Handle different slide types
318
+ if 'table' in slide_data:
319
+ self._handle_table(
320
+ presentation,
321
+ slide_data,
322
+ )
323
+ elif 'bullet_points' in slide_data:
324
+ if any(
325
+ step.startswith(STEP_BY_STEP_PROCESS_MARKER)
326
+ for step in slide_data['bullet_points']
327
+ ):
328
+ self._handle_step_by_step_process(
329
+ presentation,
330
+ slide_data,
331
+ slide_width_inch,
332
+ slide_height_inch,
333
+ )
334
+ else:
335
+ self._handle_default_display(
336
+ presentation,
337
+ slide_data,
338
+ )
339
+
340
+ # Save the presentation
341
+ presentation.save(str(file_path))
342
+ logger.debug(f"Wrote PPTX to {file_path} with enhanced formatting")
343
+
344
+ def create_presentation(
345
+ self,
346
+ content: str,
347
+ filename: str,
348
+ template: Optional[str] = None,
349
+ ) -> str:
350
+ r"""Create a PowerPoint presentation (PPTX) file.
351
+
352
+ Args:
353
+ content (str): The content to write to the PPTX file as a JSON
354
+ string. Must represent a list of dictionaries with the
355
+ following structure:
356
+ - First dict: title slide {"title": str, "subtitle": str}
357
+ - Other dicts: content slides, which can be one of:
358
+ * Bullet/step slides: {"heading": str, "bullet_points":
359
+ list of str or nested lists, "img_keywords": str
360
+ (optional)}
361
+ - If any bullet point starts with '>> ', it will be
362
+ rendered as a step-by-step process.
363
+ - "img_keywords" can be a URL or search keywords for
364
+ an image (optional).
365
+ * Table slides: {"heading": str, "table": {"headers": list
366
+ of str, "rows": list of list of str}}
367
+ filename (str): The name or path of the file. If a relative path is
368
+ supplied, it is resolved to self.output_dir.
369
+ template (Optional[str]): The path to the template PPTX file.
370
+ Initializes a presentation from a given template file Or PPTX
371
+ file. (default: :obj: `None`)
372
+
373
+ Returns:
374
+ str: A success message indicating the file was created.
375
+
376
+ Example:
377
+ [
378
+ {
379
+ "title": "Presentation Title",
380
+ "subtitle": "Presentation Subtitle"
381
+ },
382
+ {
383
+ "heading": "Slide Title",
384
+ "bullet_points": [
385
+ "**Bold text** for emphasis",
386
+ "*Italic text* for additional emphasis",
387
+ "Regular text for normal content"
388
+ ],
389
+ "img_keywords": "relevant search terms for images"
390
+ },
391
+ {
392
+ "heading": "Step-by-Step Process",
393
+ "bullet_points": [
394
+ ">> **Step 1:** First step description",
395
+ ">> **Step 2:** Second step description",
396
+ ">> **Step 3:** Third step description"
397
+ ],
398
+ "img_keywords": "process workflow steps"
399
+ },
400
+ {
401
+ "heading": "Comparison Table",
402
+ "table": {
403
+ "headers": ["Column 1", "Column 2", "Column 3"],
404
+ "rows": [
405
+ ["Row 1, Col 1", "Row 1, Col 2", "Row 1, Col 3"],
406
+ ["Row 2, Col 1", "Row 2, Col 2", "Row 2, Col 3"]
407
+ ]
408
+ },
409
+ "img_keywords": "comparison visualization"
410
+ }
411
+ ]
412
+ """
413
+ # Ensure filename has .pptx extension
414
+ if not filename.lower().endswith('.pptx'):
415
+ filename += '.pptx'
416
+
417
+ # Resolve file path
418
+ file_path = self._resolve_filepath(filename)
419
+
420
+ # Parse and validate content format
421
+ try:
422
+ import json
423
+
424
+ parsed_content = json.loads(content)
425
+ except json.JSONDecodeError as e:
426
+ logger.error(f"Content must be valid JSON: {e}")
427
+ return "Failed to parse content as JSON"
428
+
429
+ if not isinstance(parsed_content, list):
430
+ logger.error(
431
+ f"PPTX content must be a list of dictionaries, "
432
+ f"got {type(parsed_content).__name__}"
433
+ )
434
+ return "PPTX content must be a list of dictionaries"
435
+
436
+ try:
437
+ # Create the PPTX file
438
+ self._write_pptx_file(file_path, parsed_content.copy(), template)
439
+
440
+ success_msg = (
441
+ f"PowerPoint presentation successfully created: {file_path}"
442
+ )
443
+ logger.info(success_msg)
444
+ return success_msg
445
+
446
+ except Exception as e:
447
+ error_msg = f"Failed to create PPTX file {file_path}: {e!s}"
448
+ logger.error(error_msg)
449
+ return error_msg
450
+
451
+ def _handle_default_display(
452
+ self,
453
+ presentation: "presentation.Presentation",
454
+ slide_json: Dict[str, Any],
455
+ ) -> None:
456
+ r"""Display a list of text in a slide.
457
+
458
+ Args:
459
+ presentation (presentation.Presentation): The presentation object.
460
+ slide_json (Dict[str, Any]): The content of the slide as JSON data.
461
+ """
462
+ status = False
463
+
464
+ if 'img_keywords' in slide_json:
465
+ if random.random() < IMAGE_DISPLAY_PROBABILITY:
466
+ status = self._handle_display_image__in_foreground(
467
+ presentation,
468
+ slide_json,
469
+ )
470
+
471
+ if status:
472
+ return
473
+
474
+ # Image display failed, so display only text
475
+ bullet_slide_layout = presentation.slide_layouts[1]
476
+ slide = presentation.slides.add_slide(bullet_slide_layout)
477
+
478
+ shapes = slide.shapes
479
+ title_shape = shapes.title
480
+
481
+ try:
482
+ body_shape = shapes.placeholders[1]
483
+ except KeyError:
484
+ # Get placeholders from the slide without layout_number
485
+ placeholders = self._get_slide_placeholders(slide)
486
+ body_shape = shapes.placeholders[placeholders[0][0]]
487
+
488
+ title_shape.text = self._remove_slide_number_from_heading(
489
+ slide_json['heading']
490
+ )
491
+ text_frame = body_shape.text_frame
492
+
493
+ flat_items_list = self._get_flat_list_of_contents(
494
+ slide_json['bullet_points'], level=0
495
+ )
496
+ self._add_bulleted_items(text_frame, flat_items_list)
497
+
498
+ @api_keys_required(
499
+ [
500
+ ("api_key", 'PEXELS_API_KEY'),
501
+ ]
502
+ )
503
+ def _handle_display_image__in_foreground(
504
+ self,
505
+ presentation: "presentation.Presentation",
506
+ slide_json: Dict[str, Any],
507
+ ) -> bool:
508
+ r"""Create a slide with text and image using a picture placeholder
509
+ layout.
510
+
511
+ Args:
512
+ presentation (presentation.Presentation): The presentation object.
513
+ slide_json (Dict[str, Any]): The content of the slide as JSON data.
514
+
515
+ Returns:
516
+ bool: True if the slide has been processed.
517
+ """
518
+ from io import BytesIO
519
+
520
+ import requests
521
+
522
+ img_keywords = slide_json.get('img_keywords', '').strip()
523
+ slide = presentation.slide_layouts[8] # Picture with Caption
524
+ slide = presentation.slides.add_slide(slide)
525
+ placeholders = None
526
+
527
+ title_placeholder = slide.shapes.title # type: ignore[attr-defined]
528
+ title_placeholder.text = self._remove_slide_number_from_heading(
529
+ slide_json['heading']
530
+ )
531
+
532
+ try:
533
+ pic_col = slide.shapes.placeholders[1] # type: ignore[attr-defined]
534
+ except KeyError:
535
+ # Get placeholders from the slide without layout_number
536
+ placeholders = self._get_slide_placeholders(slide) # type: ignore[arg-type]
537
+ pic_col = None
538
+ for idx, name in placeholders:
539
+ if 'picture' in name:
540
+ pic_col = slide.shapes.placeholders[idx] # type: ignore[attr-defined]
541
+
542
+ try:
543
+ text_col = slide.shapes.placeholders[2] # type: ignore[attr-defined]
544
+ except KeyError:
545
+ text_col = None
546
+ if not placeholders:
547
+ placeholders = self._get_slide_placeholders(slide) # type: ignore[arg-type]
548
+
549
+ for idx, name in placeholders:
550
+ if 'content' in name:
551
+ text_col = slide.shapes.placeholders[idx] # type: ignore[attr-defined]
552
+
553
+ flat_items_list = self._get_flat_list_of_contents(
554
+ slide_json['bullet_points'], level=0
555
+ )
556
+ self._add_bulleted_items(text_col.text_frame, flat_items_list)
557
+
558
+ if not img_keywords:
559
+ return True
560
+
561
+ if isinstance(img_keywords, str) and img_keywords.startswith(
562
+ ('http://', 'https://')
563
+ ):
564
+ try:
565
+ img_response = requests.get(img_keywords, timeout=30)
566
+ img_response.raise_for_status()
567
+ image_data = BytesIO(img_response.content)
568
+ pic_col.insert_picture(image_data)
569
+ return True
570
+ except Exception as ex:
571
+ logger.error(
572
+ 'Error while downloading image from URL: %s', str(ex)
573
+ )
574
+
575
+ try:
576
+ url = 'https://api.pexels.com/v1/search'
577
+ api_key = os.getenv('PEXELS_API_KEY')
578
+
579
+ headers = {
580
+ 'Authorization': api_key,
581
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) '
582
+ 'Gecko/20100101 Firefox/10.0',
583
+ }
584
+ params = {
585
+ 'query': img_keywords,
586
+ 'size': 'medium',
587
+ 'page': 1,
588
+ 'per_page': 3,
589
+ }
590
+ response = requests.get(
591
+ url, headers=headers, params=params, timeout=12
592
+ )
593
+ response.raise_for_status()
594
+ json_response = response.json()
595
+
596
+ if json_response.get('photos'):
597
+ photo = random.choice(json_response['photos'])
598
+ photo_url = photo.get('src', {}).get('large') or photo.get(
599
+ 'src', {}
600
+ ).get('original')
601
+
602
+ if photo_url:
603
+ # Download and insert the image
604
+ img_response = requests.get(
605
+ photo_url, headers=headers, stream=True, timeout=12
606
+ )
607
+ img_response.raise_for_status()
608
+ image_data = BytesIO(img_response.content)
609
+
610
+ pic_col.insert_picture(image_data)
611
+ except Exception as ex:
612
+ logger.error(
613
+ 'Error occurred while adding image to slide: %s', str(ex)
614
+ )
615
+
616
+ return True
617
+
618
+ def _handle_table(
619
+ self,
620
+ presentation: "presentation.Presentation",
621
+ slide_json: Dict[str, Any],
622
+ ) -> None:
623
+ r"""Add a table to a slide.
624
+
625
+ Args:
626
+ presentation (presentation.Presentation): The presentation object.
627
+ slide_json (Dict[str, Any]): The content of the slide as JSON data.
628
+ """
629
+ headers = slide_json['table'].get('headers', [])
630
+ rows = slide_json['table'].get('rows', [])
631
+ bullet_slide_layout = presentation.slide_layouts[1]
632
+ slide = presentation.slides.add_slide(bullet_slide_layout)
633
+ shapes = slide.shapes
634
+ shapes.title.text = self._remove_slide_number_from_heading(
635
+ slide_json['heading']
636
+ )
637
+ left = slide.placeholders[1].left
638
+ top = slide.placeholders[1].top
639
+ width = slide.placeholders[1].width
640
+ height = slide.placeholders[1].height
641
+ table = slide.shapes.add_table(
642
+ len(rows) + 1, len(headers), left, top, width, height
643
+ ).table
644
+
645
+ # Set headers
646
+ for col_idx, header_text in enumerate(headers):
647
+ table.cell(0, col_idx).text = header_text
648
+ table.cell(0, col_idx).text_frame.paragraphs[0].font.bold = True
649
+
650
+ # Fill in rows
651
+ for row_idx, row_data in enumerate(rows, start=1):
652
+ for col_idx, cell_text in enumerate(row_data):
653
+ table.cell(row_idx, col_idx).text = cell_text
654
+
655
+ def _handle_step_by_step_process(
656
+ self,
657
+ presentation: "presentation.Presentation",
658
+ slide_json: Dict[str, Any],
659
+ slide_width_inch: float,
660
+ slide_height_inch: float,
661
+ ) -> None:
662
+ r"""Add shapes to display a step-by-step process in the slide.
663
+
664
+ Args:
665
+ presentation (presentation.Presentation): The presentation object.
666
+ slide_json (Dict[str, Any]): The content of the slide as JSON data.
667
+ slide_width_inch (float): The width of the slide in inches.
668
+ slide_height_inch (float): The height of the slide in inches.
669
+ """
670
+ import pptx
671
+ from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
672
+ from pptx.util import Inches, Pt
673
+
674
+ steps = slide_json['bullet_points']
675
+ n_steps = len(steps)
676
+
677
+ bullet_slide_layout = presentation.slide_layouts[1]
678
+ slide = presentation.slides.add_slide(bullet_slide_layout)
679
+ shapes = slide.shapes
680
+ shapes.title.text = self._remove_slide_number_from_heading(
681
+ slide_json['heading']
682
+ )
683
+
684
+ if 3 <= n_steps <= 4:
685
+ # Horizontal display
686
+ height = Inches(1.5)
687
+ width = Inches(slide_width_inch / n_steps - 0.01)
688
+ top = Inches(slide_height_inch / 2)
689
+ left = Inches(
690
+ (slide_width_inch - width.inches * n_steps) / 2 + 0.05
691
+ )
692
+
693
+ for step in steps:
694
+ shape = shapes.add_shape(
695
+ MSO_AUTO_SHAPE_TYPE.CHEVRON, left, top, width, height
696
+ )
697
+ text_frame = shape.text_frame
698
+ text_frame.clear()
699
+ paragraph = text_frame.paragraphs[0]
700
+ paragraph.alignment = pptx.enum.text.PP_ALIGN.CENTER
701
+ text_frame.vertical_anchor = pptx.enum.text.MSO_ANCHOR.MIDDLE
702
+ self._format_text(
703
+ paragraph, step.removeprefix(STEP_BY_STEP_PROCESS_MARKER)
704
+ )
705
+ for run in paragraph.runs:
706
+ run.font.size = Pt(14)
707
+ left = Inches(left.inches + width.inches - Inches(0.4).inches)
708
+ elif 4 < n_steps <= 6:
709
+ # Vertical display
710
+ height = Inches(0.65)
711
+ top = Inches(slide_height_inch / 4)
712
+ left = Inches(1)
713
+ width = Inches(slide_width_inch * 2 / 3)
714
+
715
+ for step in steps:
716
+ shape = shapes.add_shape(
717
+ MSO_AUTO_SHAPE_TYPE.PENTAGON, left, top, width, height
718
+ )
719
+ text_frame = shape.text_frame
720
+ text_frame.clear()
721
+ paragraph = text_frame.paragraphs[0]
722
+ paragraph.alignment = pptx.enum.text.PP_ALIGN.CENTER
723
+ text_frame.vertical_anchor = pptx.enum.text.MSO_ANCHOR.MIDDLE
724
+ self._format_text(
725
+ paragraph, step.removeprefix(STEP_BY_STEP_PROCESS_MARKER)
726
+ )
727
+ for run in paragraph.runs:
728
+ run.font.size = Pt(14)
729
+ top = Inches(top.inches + height.inches + Inches(0.3).inches)
730
+ left = Inches(left.inches + Inches(0.5).inches)
731
+
732
+ def _remove_slide_number_from_heading(self, header: str) -> str:
733
+ r"""Remove the slide number from a given slide header.
734
+
735
+ Args:
736
+ header (str): The header of a slide.
737
+
738
+ Returns:
739
+ str: The header without slide number.
740
+ """
741
+ if SLIDE_NUMBER_REGEX.match(header):
742
+ idx = header.find(':')
743
+ header = header[idx + 1 :]
744
+ return header
745
+
746
+ def _get_slide_placeholders(
747
+ self,
748
+ slide: "Slide",
749
+ ) -> List[Tuple[int, str]]:
750
+ r"""Return the index and name of all placeholders present in a slide.
751
+
752
+ Args:
753
+ slide (Slide): The slide.
754
+
755
+ Returns:
756
+ List[Tuple[int, str]]: A list containing placeholders (idx, name)
757
+ tuples.
758
+ """
759
+ if hasattr(slide.shapes, 'placeholders'):
760
+ placeholders = [
761
+ (shape.placeholder_format.idx, shape.name.lower())
762
+ for shape in slide.shapes.placeholders
763
+ ]
764
+ if placeholders and len(placeholders) > 0:
765
+ placeholders.pop(0) # Remove the title placeholder
766
+ return placeholders
767
+ return []
768
+
769
+ def get_tools(self) -> List[FunctionTool]:
770
+ r"""Returns a list of FunctionTool objects representing the
771
+ functions in the toolkit.
772
+
773
+ Returns:
774
+ List[FunctionTool]: A list of FunctionTool objects
775
+ representing the functions in the toolkit.
776
+ """
777
+ return [FunctionTool(self.create_presentation)]