Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions camel/toolkits/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
from .imap_mail_toolkit import IMAPMailToolkit
from .microsoft_outlook_mail_toolkit import OutlookMailToolkit
from .earth_science_toolkit import EarthScienceToolkit
from .pptx_node_toolkit import PptxNodeToolkit

__all__ = [
'BaseToolkit',
Expand Down Expand Up @@ -191,4 +192,5 @@
'IMAPMailToolkit',
"OutlookMailToolkit",
'EarthScienceToolkit',
'PptxNodeToolkit',
]
199 changes: 199 additions & 0 deletions camel/toolkits/pptx_node_toolkit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# ========= 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 subprocess
from pathlib import Path
from typing import Optional

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):
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
import shutil

if not shutil.which(self.node_executable):
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."
)

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(
self,
content: str,
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, but the agent should strive to provide the
correct extension.

Args:
content (str): The content to write to the PPTX file as a JSON
string. It must be a list of dictionaries representing slides.

JSON Schema Example:
[
{
"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'

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)
content_str = json.dumps(json_obj)
except json.JSONDecodeError:
return (
"Error: Content must be valid JSON string 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)]
88 changes: 88 additions & 0 deletions camel/toolkits/scripts/generate_pptx.js
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" });
}

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);
});
11 changes: 11 additions & 0 deletions camel/toolkits/scripts/package.json
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
}
Loading
Loading