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_development 1 Title Slide Modern Web Development Trends 2024

Modern Web Development

Trends & Technologies 2024

Building Tomorrow's Web Today

Then create the next slide: modern_web_development 2 Frontend Frameworks Modern 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)}")