-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat: add PptxNodeToolkit with PptxGenJS support #3712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
aviralgarg05
wants to merge
14
commits into
camel-ai:master
Choose a base branch
from
aviralgarg05:feat/pptx-node-toolkit
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+40,730
−39,680
Open
Changes from 6 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
3d0228d
feat: add PptxNodeToolkit with PptxGenJS support
aviralgarg05 37de5d2
fix: address PR feedback (JSON output, docstrings, extensions)
aviralgarg05 16326b0
fix: ci failures (add license headers, rename variable, restore file …
aviralgarg05 9d753e8
fix: resolve ruff line length violations (E501)
aviralgarg05 c923f67
fix(toolkits): robust PptxNodeToolkit with checks and package.json
aviralgarg05 922388b
fix(toolkits): improve PptxNodeToolkit docstrings and type hints
aviralgarg05 c26c8f6
Address PR feedback: refactor PptxNodeToolkit, add validation, and fi…
aviralgarg05 02d8336
Merge branch 'master' into feat/pptx-node-toolkit
aviralgarg05 2b0664a
feat: address PR review feedback for PptxNodeToolkit
aviralgarg05 e715f75
Merge remote-tracking branch 'origin/master' into feat/pptx-node-toolkit
aviralgarg05 4d29ba4
fix: resolve merge conflicts and workflow failures
aviralgarg05 b2786e2
feat: resolve PR comments and fix CI workflows
aviralgarg05 4be13c6
Merge remote-tracking branch 'origin/master' into feat/pptx-node-toolkit
aviralgarg05 2a00cc7
style: fix end-of-file issues
aviralgarg05 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| # ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. ========= | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. ========= | ||
| import json | ||
| import os | ||
| import shutil | ||
| import subprocess | ||
| from pathlib import Path | ||
| from typing import Any, Dict, List, Optional, Union | ||
|
|
||
| from camel.logger import get_logger | ||
| from camel.toolkits.base import BaseToolkit | ||
| from camel.toolkits.function_tool import FunctionTool | ||
| from camel.utils import MCPServer | ||
|
|
||
| logger = get_logger(__name__) | ||
|
|
||
|
|
||
| @MCPServer() | ||
| class PptxNodeToolkit(BaseToolkit): | ||
aviralgarg05 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| r"""A toolkit for creating PowerPoint presentations using PptxGenJS | ||
| (Node.js). | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| working_directory: Optional[str] = None, | ||
| timeout: Optional[float] = None, | ||
| node_executable: str = "node", | ||
| ) -> None: | ||
| r"""Initialize the PptxNodeToolkit. | ||
|
|
||
| Args: | ||
| working_directory (str, optional): The default directory for | ||
| output files. | ||
| timeout (Optional[float]): The timeout for the toolkit. | ||
| (default: :obj:`None`) | ||
| node_executable (str): The path to the Node.js executable. | ||
| (default: "node") | ||
| """ | ||
| super().__init__(timeout=timeout) | ||
| self.node_executable = node_executable | ||
|
|
||
| if working_directory: | ||
| self.working_directory = Path(working_directory).resolve() | ||
| else: | ||
| camel_workdir = os.environ.get("CAMEL_WORKDIR") | ||
| if camel_workdir: | ||
| self.working_directory = Path(camel_workdir).resolve() | ||
| else: | ||
| self.working_directory = Path("./camel_working_dir").resolve() | ||
|
|
||
| self.working_directory.mkdir(parents=True, exist_ok=True) | ||
| self._check_node_environment() | ||
|
|
||
| def _check_node_environment(self) -> None: | ||
| """Checks if Node.js and the required scripts are available.""" | ||
| # 1. Check if node is available | ||
| if not shutil.which(self.node_executable): | ||
aviralgarg05 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| logger.warning( | ||
| f"Node.js executable '{self.node_executable}' not found. " | ||
| "PptxNodeToolkit will not function correctly." | ||
| ) | ||
|
|
||
| # 2. Check if the script's package.json dependencies are installed | ||
| script_dir = Path(__file__).parent / "scripts" | ||
| node_modules_path = script_dir / "node_modules" | ||
|
|
||
| if not node_modules_path.exists(): | ||
| logger.warning( | ||
| f"Node dependencies not found in {script_dir}. " | ||
| "Please run `npm install` in that directory." | ||
aviralgarg05 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
|
|
||
| def _resolve_filepath(self, file_path: str) -> Path: | ||
| path_obj = Path(file_path) | ||
| if not path_obj.is_absolute(): | ||
| path_obj = self.working_directory / path_obj | ||
| return path_obj.resolve() | ||
|
|
||
| def create_presentation( | ||
aviralgarg05 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self, | ||
| content: Union[str, List[Dict[str, Any]]], | ||
| filename: str, | ||
| ) -> str: | ||
| r"""Create a PowerPoint presentation (PPTX) file using PptxGenJS. | ||
|
|
||
| The filename MUST end with ".pptx". If it does not, the toolkit will | ||
| automatically append it. | ||
|
|
||
| Args: | ||
| content (Union[str, List[Dict[str, Any]]]): The content to write | ||
| to the PPTX file. It can be a JSON string or a list of | ||
| dictionaries/slides. | ||
|
|
||
| Supported keys for each slide dictionary: | ||
| - `title` (str): Title text for the slide. | ||
| - `subtitle` (str): Subtitle text. | ||
| - `heading` (str): Main heading for the slide. | ||
| - `text` (str): Body text. | ||
| - `bullet_points` (List[str]): A list of bullet point strings. | ||
| - `table` (Dict): A dictionary with `headers` (List[str]) and | ||
| `rows` (List[List[str]]). | ||
|
|
||
| Example Structure: | ||
| [ | ||
| { | ||
| "title": "Main Title", | ||
| "subtitle": "Subtitle text" | ||
| }, | ||
| { | ||
| "heading": "Slide Heading", | ||
| "bullet_points": [ | ||
| "Point 1", | ||
| "Point 2" | ||
| ] | ||
| }, | ||
| { | ||
| "heading": "Table Slide", | ||
| "table": { | ||
| "headers": ["Col 1", "Col 2"], | ||
| "rows": [["A", "B"], ["C", "D"]] | ||
| } | ||
| } | ||
| ] | ||
| filename (str): The name of the file to save. MUST end in .pptx. | ||
|
|
||
| Returns: | ||
| str: A JSON string containing the result status, file path, and | ||
| number of slides generated. | ||
| """ | ||
| if not filename.lower().endswith('.pptx'): | ||
| filename += '.pptx' | ||
aviralgarg05 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| file_path = self._resolve_filepath(filename) | ||
| script_path = Path(__file__).parent / "scripts" / "generate_pptx.js" | ||
|
|
||
| try: | ||
| # Ensure content is a valid JSON string | ||
| try: | ||
| if not isinstance(content, str): | ||
| content_str = json.dumps(content) | ||
| else: | ||
| # Validate JSON | ||
| json_obj = json.loads(content) | ||
aviralgarg05 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| content_str = json.dumps(json_obj) | ||
| except json.JSONDecodeError: | ||
| return ( | ||
| "Error: Content must be valid JSON string or structure " | ||
| "representing slides." | ||
| ) | ||
|
|
||
| # Run node script | ||
| result = subprocess.run( | ||
| [self.node_executable, str(script_path), str(file_path), content_str], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True, | ||
| ) | ||
|
|
||
| # Parse JSON output from script | ||
| try: | ||
| script_output = json.loads(result.stdout.strip()) | ||
| if script_output.get("success"): | ||
| return ( | ||
| f"Presentation created successfully. " | ||
| f"Path: {script_output.get('path')}, " | ||
| f"Slides: {script_output.get('slides')}" | ||
| ) | ||
| else: | ||
| return ( | ||
| f"Error creating presentation: " | ||
| f"{script_output.get('error')}" | ||
| ) | ||
| except json.JSONDecodeError: | ||
| return f"Error parsing script output: {result.stdout.strip()}" | ||
|
|
||
| except FileNotFoundError: | ||
| return ( | ||
| f"Error: Node.js executable '{self.node_executable}' not found." | ||
| ) | ||
| except subprocess.CalledProcessError as e: | ||
| logger.error(f"Error creating presentation: {e.stderr}") | ||
| return f"Error creating presentation subprocess: {e.stderr}" | ||
| except Exception as e: | ||
| logger.error(f"Error creating presentation: {e!s}") | ||
| return f"Error creating presentation: {e!s}" | ||
|
|
||
| def get_tools(self) -> list[FunctionTool]: | ||
| r"""Returns a list of FunctionTool objects representing the | ||
| functions in the toolkit. | ||
|
|
||
| Returns: | ||
| List[FunctionTool]: A list of FunctionTool objects | ||
| representing the functions in the toolkit. | ||
| """ | ||
| return [FunctionTool(self.create_presentation)] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
|
|
||
| import PptxGenJS from "pptxgenjs"; | ||
| import fs from "fs"; | ||
|
|
||
| // Get arguments | ||
| const args = process.argv.slice(2); | ||
| if (args.length < 2) { | ||
| console.error("Usage: node generate_pptx.js <filename> <content_json>"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const filename = args[0]; | ||
| const contentJson = args[1]; | ||
|
|
||
| let slidesData; | ||
| try { | ||
| slidesData = JSON.parse(contentJson); | ||
| } catch (e) { | ||
| console.error("Error parsing JSON content:", e); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| // Create Presentation | ||
| const ppt = new PptxGenJS(); | ||
|
|
||
| // Process slides | ||
| if (Array.isArray(slidesData)) { | ||
| slidesData.forEach((slideData) => { | ||
| const slide = ppt.addSlide(); | ||
|
|
||
| // simple layout heuristics based on keys | ||
| if (slideData.title) { | ||
| slide.addText(slideData.title, { x: 1, y: 1, w: "80%", h: 1, fontSize: 24, bold: true, color: "363636" }); | ||
aviralgarg05 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| if (slideData.subtitle) { | ||
| slide.addText(slideData.subtitle, { x: 1, y: 2.5, w: "80%", h: 1, fontSize: 18, color: "737373" }); | ||
| } | ||
|
|
||
| if (slideData.heading) { | ||
| slide.addText(slideData.heading, { x: 0.5, y: 0.5, w: "90%", h: 0.5, fontSize: 20, bold: true, color: "000000" }); | ||
| } | ||
|
|
||
| if (slideData.bullet_points && Array.isArray(slideData.bullet_points)) { | ||
| const bullets = slideData.bullet_points.map(bp => ({ text: bp, options: { fontSize: 14, bullet: true, color: "000000", breakLine: true } })); | ||
| slide.addText(bullets, { x: 1, y: 1.5, w: "80%", h: 4 }); | ||
| } | ||
|
|
||
| if (slideData.text) { | ||
| slide.addText(slideData.text, { x: 1, y: 1.5, w: "80%", h: 4, fontSize: 14, color: "000000" }); | ||
| } | ||
|
|
||
| // Table support | ||
| if (slideData.table) { | ||
| const tableData = []; | ||
| // headers | ||
| if (slideData.table.headers) { | ||
| tableData.push(slideData.table.headers.map(h => ({ text: h, options: { bold: true, fill: "F7F7F7" } }))); | ||
| } | ||
| // rows | ||
| if (slideData.table.rows) { | ||
| slideData.table.rows.forEach(row => { | ||
| tableData.push(row); | ||
| }); | ||
| } | ||
| slide.addTable(tableData, { x: 1, y: 2, w: "80%" }); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // Save File | ||
| ppt.writeFile({ fileName: filename }) | ||
| .then((fileName) => { | ||
| const result = { | ||
| success: true, | ||
| path: fileName, | ||
| slides: Array.isArray(slidesData) ? slidesData.length : 0 | ||
| }; | ||
| console.log(JSON.stringify(result)); | ||
| }) | ||
| .catch((err) => { | ||
| const result = { | ||
| success: false, | ||
| error: err.toString() | ||
| }; | ||
| console.log(JSON.stringify(result)); | ||
| process.exit(1); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "name": "camel-pptx-toolkit-scripts", | ||
| "version": "1.0.0", | ||
| "description": "Node.js scripts for CAMEL PptxNodeToolkit", | ||
| "main": "generate_pptx.js", | ||
| "type": "module", | ||
| "dependencies": { | ||
| "pptxgenjs": "^3.12.0" | ||
| }, | ||
| "private": true | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.