from agentpress.tool import ToolResult, openapi_schema, usage_example
from sandbox.tool_base import SandboxToolsBase
from agentpress.thread_manager import ThreadManager
from typing import List, Dict, Optional
import json
import os
from datetime import datetime
import re
from .presentation_styles_config import get_style_config, get_all_styles
class SandboxPresentationTool(SandboxToolsBase):
"""
Per-slide HTML presentation tool for creating professional presentations.
Each slide is managed individually with 1920x1080 dimensions.
Supports iterative slide creation, editing, and presentation assembly.
"""
def __init__(self, project_id: str, thread_manager: ThreadManager):
super().__init__(project_id, thread_manager)
self.workspace_path = "/workspace"
self.presentations_dir = "presentations"
async def _ensure_presentations_dir(self):
"""Ensure the presentations directory exists"""
full_path = f"{self.workspace_path}/{self.presentations_dir}"
try:
await self.sandbox.fs.create_folder(full_path, "755")
except:
pass
async def _ensure_presentation_dir(self, presentation_name: str):
"""Ensure a specific presentation directory exists"""
safe_name = self._sanitize_filename(presentation_name)
presentation_path = f"{self.workspace_path}/{self.presentations_dir}/{safe_name}"
try:
await self.sandbox.fs.create_folder(presentation_path, "755")
except:
pass
return safe_name, presentation_path
def _sanitize_filename(self, name: str) -> str:
"""Convert presentation name to safe filename"""
return "".join(c for c in name if c.isalnum() or c in "-_").lower()
def _get_style_config(self, style_name: str) -> Dict:
"""Get style configuration for a given style name"""
return get_style_config(style_name)
def _create_slide_html(self, slide_content: str, slide_number: int, total_slides: int, presentation_title: str, style: str = "default") -> str:
"""Create a complete HTML document for a single slide with proper 1920x1080 dimensions"""
# Get style configuration
style_config = self._get_style_config(style)
html_template = f"""
{presentation_title} - Slide {slide_number}
{slide_content}
{slide_number}{f" / {total_slides}" if total_slides > 0 else ""}
"""
return html_template
async def _load_presentation_metadata(self, presentation_path: str):
"""Load presentation metadata, create if doesn't exist"""
metadata_path = f"{presentation_path}/metadata.json"
try:
metadata_content = await self.sandbox.fs.download_file(metadata_path)
return json.loads(metadata_content.decode())
except:
# Create default metadata
return {
"presentation_name": "",
"title": "Presentation",
"description": "",
"slides": {},
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}
async def _save_presentation_metadata(self, presentation_path: str, metadata: Dict):
"""Save presentation metadata"""
metadata["updated_at"] = datetime.now().isoformat()
metadata_path = f"{presentation_path}/metadata.json"
await self.sandbox.fs.upload_file(json.dumps(metadata, indent=2).encode(), metadata_path)
@openapi_schema({
"type": "function",
"function": {
"name": "create_slide",
"description": "Create or update a single slide in a presentation. Each slide is saved as a standalone HTML file with 1920x1080 dimensions (16:9 aspect ratio). Perfect for iterative slide creation and editing. Use 'presentation_styles' tool first to see available styles.",
"parameters": {
"type": "object",
"properties": {
"presentation_name": {
"type": "string",
"description": "Name of the presentation (creates folder if doesn't exist)"
},
"slide_number": {
"type": "integer",
"description": "Slide number (1-based). If slide exists, it will be updated."
},
"slide_title": {
"type": "string",
"description": "Title of this specific slide (for reference and navigation)"
},
"content": {
"type": "string",
"description": "HTML content for the slide body. Should include all styling within the content. The content will be placed inside a 1920x1080 slide container with CSS frameworks (Tailwind, FontAwesome, D3, Chart.js) available. Use professional styling with good typography, spacing, and visual hierarchy. You can use style-aware CSS classes: .primary-color, .primary-bg, .accent-color, .accent-bg, .text-color, .card, .highlight"
},
"presentation_title": {
"type": "string",
"description": "Main title of the presentation (used in HTML title and navigation)",
"default": "Presentation"
},
"style": {
"type": "string",
"description": "Visual style theme for the slide. Use 'presentation_styles' tool to see all available options. Examples: 'velvet', 'glacier', 'ember', 'sage', 'obsidian', 'coral', 'platinum', 'aurora', 'midnight', 'citrus', or 'default'",
"default": "default"
}
},
"required": ["presentation_name", "slide_number", "slide_title", "content"]
}
}
})
@usage_example('''
Create individual slides for a presentation about "Modern Web Development":
modern_web_development1Title SlideModern Web Development Trends 2024
Modern Web Development
Trends & Technologies 2024
Building Tomorrow's Web Today
Then create the next slide:
modern_web_development2Frontend FrameworksModern Web Development Trends 2024
Frontend Frameworks
React - Component-based UI library
Vue.js - Progressive framework
📱
Modern Tools
This approach allows you to:
- Create slides one at a time
- Edit existing slides by using the same slide number
- Build presentations iteratively
- Mix and match different slide designs
- Each slide is a standalone HTML file with full styling
''')
async def create_slide(
self,
presentation_name: str,
slide_number: int,
slide_title: str,
content: str,
presentation_title: str = "Presentation",
style: str = "default"
) -> ToolResult:
"""Create or update a single slide in a presentation"""
try:
await self._ensure_sandbox()
await self._ensure_presentations_dir()
# Validation
if not presentation_name:
return self.fail_response("Presentation name is required.")
if slide_number < 1:
return self.fail_response("Slide number must be 1 or greater.")
if not slide_title:
return self.fail_response("Slide title is required.")
if not content:
return self.fail_response("Slide content is required.")
# Ensure presentation directory exists
safe_name, presentation_path = await self._ensure_presentation_dir(presentation_name)
# Load or create metadata
metadata = await self._load_presentation_metadata(presentation_path)
metadata["presentation_name"] = presentation_name
if presentation_title != "Presentation": # Only update if explicitly provided
metadata["title"] = presentation_title
# Create slide HTML
slide_html = self._create_slide_html(
slide_content=content,
slide_number=slide_number,
total_slides=0, # Will be updated when regenerating navigation
presentation_title=presentation_title,
style=style
)
# Save slide file
slide_filename = f"slide_{slide_number:02d}.html"
slide_path = f"{presentation_path}/{slide_filename}"
await self.sandbox.fs.upload_file(slide_html.encode(), slide_path)
# Update metadata
if "slides" not in metadata:
metadata["slides"] = {}
metadata["slides"][str(slide_number)] = {
"title": slide_title,
"filename": slide_filename,
"file_path": f"{self.presentations_dir}/{safe_name}/{slide_filename}",
"preview_url": f"/workspace/{self.presentations_dir}/{safe_name}/{slide_filename}",
"style": style,
"created_at": datetime.now().isoformat()
}
# Save updated metadata
await self._save_presentation_metadata(presentation_path, metadata)
return self.success_response({
"message": f"Slide {slide_number} '{slide_title}' created/updated successfully with '{style}' style",
"presentation_name": presentation_name,
"presentation_path": f"{self.presentations_dir}/{safe_name}",
"slide_number": slide_number,
"slide_title": slide_title,
"slide_file": f"{self.presentations_dir}/{safe_name}/{slide_filename}",
"preview_url": f"/workspace/{self.presentations_dir}/{safe_name}/{slide_filename}",
"style": style,
"total_slides": len(metadata["slides"]),
"note": "Slide saved as standalone HTML file with 1920x1080 dimensions"
})
except Exception as e:
return self.fail_response(f"Failed to create slide: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "list_slides",
"description": "List all slides in a presentation, showing their titles and order",
"parameters": {
"type": "object",
"properties": {
"presentation_name": {
"type": "string",
"description": "Name of the presentation to list slides for"
}
},
"required": ["presentation_name"]
}
}
})
async def list_slides(self, presentation_name: str) -> ToolResult:
"""List all slides in a presentation"""
try:
await self._ensure_sandbox()
if not presentation_name:
return self.fail_response("Presentation name is required.")
safe_name = self._sanitize_filename(presentation_name)
presentation_path = f"{self.workspace_path}/{self.presentations_dir}/{safe_name}"
# Load metadata
metadata = await self._load_presentation_metadata(presentation_path)
if not metadata.get("slides"):
return self.success_response({
"message": f"No slides found in presentation '{presentation_name}'",
"presentation_name": presentation_name,
"slides": [],
"total_slides": 0
})
# Sort slides by number
slides_info = []
for slide_num_str, slide_data in metadata["slides"].items():
slides_info.append({
"slide_number": int(slide_num_str),
"title": slide_data["title"],
"filename": slide_data["filename"],
"preview_url": slide_data["preview_url"],
"created_at": slide_data.get("created_at", "Unknown")
})
slides_info.sort(key=lambda x: x["slide_number"])
return self.success_response({
"message": f"Found {len(slides_info)} slides in presentation '{presentation_name}'",
"presentation_name": presentation_name,
"presentation_title": metadata.get("title", "Presentation"),
"slides": slides_info,
"total_slides": len(slides_info),
"presentation_path": f"{self.presentations_dir}/{safe_name}"
})
except Exception as e:
return self.fail_response(f"Failed to list slides: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "delete_slide",
"description": "Delete a specific slide from a presentation",
"parameters": {
"type": "object",
"properties": {
"presentation_name": {
"type": "string",
"description": "Name of the presentation"
},
"slide_number": {
"type": "integer",
"description": "Slide number to delete (1-based)"
}
},
"required": ["presentation_name", "slide_number"]
}
}
})
async def delete_slide(self, presentation_name: str, slide_number: int) -> ToolResult:
"""Delete a specific slide from a presentation"""
try:
await self._ensure_sandbox()
if not presentation_name:
return self.fail_response("Presentation name is required.")
if slide_number < 1:
return self.fail_response("Slide number must be 1 or greater.")
safe_name = self._sanitize_filename(presentation_name)
presentation_path = f"{self.workspace_path}/{self.presentations_dir}/{safe_name}"
# Load metadata
metadata = await self._load_presentation_metadata(presentation_path)
if not metadata.get("slides") or str(slide_number) not in metadata["slides"]:
return self.fail_response(f"Slide {slide_number} not found in presentation '{presentation_name}'")
# Get slide info before deletion
slide_info = metadata["slides"][str(slide_number)]
slide_filename = slide_info["filename"]
# Delete slide file
slide_path = f"{presentation_path}/{slide_filename}"
try:
await self.sandbox.fs.delete_file(slide_path)
except:
pass # File might not exist
# Remove from metadata
del metadata["slides"][str(slide_number)]
# Save updated metadata
await self._save_presentation_metadata(presentation_path, metadata)
return self.success_response({
"message": f"Slide {slide_number} '{slide_info['title']}' deleted successfully",
"presentation_name": presentation_name,
"deleted_slide": slide_number,
"deleted_title": slide_info['title'],
"remaining_slides": len(metadata["slides"])
})
except Exception as e:
return self.fail_response(f"Failed to delete slide: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "presentation_styles",
"description": "Get available presentation styles with their descriptions and visual characteristics. Use this to show users different style options before creating slides.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
})
async def presentation_styles(self) -> ToolResult:
"""Get available presentation styles with descriptions and examples"""
try:
styles = get_all_styles()
return self.success_response({
"message": f"Found {len(styles)} presentation styles available",
"styles": styles,
"usage_tip": "Choose a style and use it with the 'style' parameter in create_slide"
})
except Exception as e:
return self.fail_response(f"Failed to get presentation styles: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "list_presentations",
"description": "List all available presentations in the workspace",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
})
async def list_presentations(self) -> ToolResult:
"""List all presentations in the workspace"""
try:
await self._ensure_sandbox()
presentations_path = f"{self.workspace_path}/{self.presentations_dir}"
try:
files = await self.sandbox.fs.list_files(presentations_path)
presentations = []
for file_info in files:
if file_info.is_directory:
metadata = await self._load_presentation_metadata(f"{presentations_path}/{file_info.name}")
presentations.append({
"folder": file_info.name,
"title": metadata.get("title", "Unknown Title"),
"description": metadata.get("description", ""),
"total_slides": len(metadata.get("slides", {})),
"created_at": metadata.get("created_at", "Unknown"),
"updated_at": metadata.get("updated_at", "Unknown")
})
return self.success_response({
"message": f"Found {len(presentations)} presentations",
"presentations": presentations,
"presentations_directory": f"/workspace/{self.presentations_dir}"
})
except Exception as e:
return self.success_response({
"message": "No presentations found",
"presentations": [],
"presentations_directory": f"/workspace/{self.presentations_dir}",
"note": "Create your first slide using create_slide"
})
except Exception as e:
return self.fail_response(f"Failed to list presentations: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "delete_presentation",
"description": "Delete an entire presentation and all its files",
"parameters": {
"type": "object",
"properties": {
"presentation_name": {
"type": "string",
"description": "Name of the presentation to delete"
}
},
"required": ["presentation_name"]
}
}
})
async def delete_presentation(self, presentation_name: str) -> ToolResult:
"""Delete a presentation and all its files"""
try:
await self._ensure_sandbox()
if not presentation_name:
return self.fail_response("Presentation name is required.")
safe_name = self._sanitize_filename(presentation_name)
presentation_path = f"{self.workspace_path}/{self.presentations_dir}/{safe_name}"
try:
await self.sandbox.fs.delete_folder(presentation_path)
return self.success_response({
"message": f"Presentation '{presentation_name}' deleted successfully",
"deleted_path": f"{self.presentations_dir}/{safe_name}"
})
except Exception as e:
return self.fail_response(f"Presentation '{presentation_name}' not found or could not be deleted: {str(e)}")
except Exception as e:
return self.fail_response(f"Failed to delete presentation: {str(e)}")