Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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',
]
111 changes: 111 additions & 0 deletions camel/toolkits/pptx_node_toolkit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@

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.

Args:
content (str): The content to write to the PPTX file as a JSON
string.
filename (str): The name or path of the file.

Returns:
str: A success message indicating the file was created.
"""
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
)
res = result.stdout.strip()
return res

except subprocess.CalledProcessError as e:
logger.error(f"Error creating presentation: {e.stderr}")
return f"Error creating presentation: {e.stderr}"
except Exception as e:
logger.error(f"Error creating presentation: {str(e)}")
return f"Error creating presentation: {str(e)}"

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)]
79 changes: 79 additions & 0 deletions camel/toolkits/scripts/generate_pptx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@

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 pres = new PptxGenJS();

// Process slides
if (Array.isArray(slidesData)) {
slidesData.forEach((slideData) => {
const slide = pres.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
pres.writeFile({ fileName: filename })
.then((fileName) => {
console.log(`PowerPoint presentation successfully created: ${fileName}`);
})
.catch((err) => {
console.error("Error saving file:", err);
process.exit(1);
});
Loading