Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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',
]
169 changes: 169 additions & 0 deletions camel/toolkits/pptx_node_toolkit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# ========= 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,
) -> 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`)
"""
super().__init__(timeout=timeout)

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)

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(
["node", 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 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);
});
71 changes: 71 additions & 0 deletions test/toolkits/test_pptx_node_toolkit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# ========= 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 unittest
from unittest.mock import MagicMock, patch

from camel.toolkits.pptx_node_toolkit import PptxNodeToolkit


class TestPptxNodeToolkit(unittest.TestCase):
def setUp(self):
self.toolkit = PptxNodeToolkit()

@patch("subprocess.run")
def test_create_presentation_success(self, mock_subprocess_run):
# Mock successful execution
mock_result = MagicMock()
mock_result.stdout = json.dumps(
{"success": True, "path": "/path/to/test.pptx", "slides": 5}
)
mock_subprocess_run.return_value = mock_result

content = [{"title": "Test Slide"}]
filename = "test_presentation"

result = self.toolkit.create_presentation(content, filename)

expected_call_args = mock_subprocess_run.call_args[0][0]
self.assertEqual(expected_call_args[0], "node")
self.assertTrue(
expected_call_args[2].endswith("test_presentation.pptx")
)

self.assertIn("Presentation created successfully", result)
self.assertIn("/path/to/test.pptx", result)
self.assertIn("Slides: 5", result)

@patch("subprocess.run")
def test_create_presentation_failure(self, mock_subprocess_run):
# Mock failed execution
mock_result = MagicMock()
mock_result.stdout = json.dumps(
{"success": False, "error": "Some error occurred"}
)
mock_subprocess_run.return_value = mock_result

content = [{"title": "Test Slide"}]
filename = "test.pptx"

result = self.toolkit.create_presentation(content, filename)

self.assertIn(
"Error creating presentation: Some error occurred", result
)

def test_create_presentation_invalid_json(self):
content = "Invalid JSON"
filename = "test.pptx"
result = self.toolkit.create_presentation(content, filename)
self.assertIn("Error: Content must be valid JSON", result)
Loading