"""
Google Slides MCP Tools for FastMCP2.
This module provides MCP tools for interacting with Google Slides API.
Migrated from decorator-based pattern to FastMCP2 architecture.
"""
import asyncio
import os
import re
from datetime import datetime
import aiofiles
import aiohttp
from fastmcp import FastMCP
from googleapiclient.errors import HttpError
from typing_extensions import Any, Dict, List, Optional
from auth.service_helpers import get_service
from config.enhanced_logging import setup_logger
# Import our custom type for consistent parameter definition
from tools.common_types import UserGoogleEmailSlides
from .slides_types import (
AddSlideResponse,
BatchUpdateReply,
CreatePresentationResponse,
ExportAndDownloadPresentationResponse,
FileDownloadInfo,
PageSize,
PresentationInfoResponse,
SlideInfo,
UpdateSlideContentResponse,
)
logger = setup_logger()
# ============================================================================
# CONSTANTS AND CONFIGURATION
# ============================================================================
# Valid thumbnail sizes
VALID_THUMBNAIL_SIZES = {"LARGE", "MEDIUM", "SMALL"}
# Valid page types
VALID_PAGE_TYPES = {"SLIDE", "MASTER", "LAYOUT"}
# Valid shape types for validation
VALID_SHAPE_TYPES = {
"TEXT_BOX",
"RECTANGLE",
"ROUND_RECTANGLE",
"ELLIPSE",
"ARC",
"BENT_ARROW",
"BENT_UP_ARROW",
"BEVEL",
"BLOCK_ARC",
"BRACE_PAIR",
"BRACKET_PAIR",
"CAN",
"CHEVRON",
"CHORD",
"CLOUD",
"CORNER",
"CUBE",
"CURVED_DOWN_ARROW",
"CURVED_LEFT_ARROW",
"CURVED_RIGHT_ARROW",
"CURVED_UP_ARROW",
"DECAGON",
"DIAGONAL_STRIPE",
"DIAMOND",
"DODECAGON",
"DONUT",
"DOUBLE_WAVE",
"DOWN_ARROW",
"DOWN_ARROW_CALLOUT",
"FOLDED_CORNER",
"FRAME",
"HALF_FRAME",
"HEART",
"HEPTAGON",
"HEXAGON",
"HOME_PLATE",
"HORIZONTAL_SCROLL",
"IRREGULAR_SEAL_1",
"IRREGULAR_SEAL_2",
"LEFT_ARROW",
"LEFT_ARROW_CALLOUT",
"LEFT_BRACE",
"LEFT_BRACKET",
"LEFT_RIGHT_ARROW",
"LEFT_RIGHT_ARROW_CALLOUT",
"LEFT_RIGHT_UP_ARROW",
"LEFT_UP_ARROW",
"LIGHTNING_BOLT",
"MATH_DIVIDE",
"MATH_EQUAL",
"MATH_MINUS",
"MATH_MULTIPLY",
"MATH_NOT_EQUAL",
"MATH_PLUS",
"MOON",
"NO_SMOKING",
"NOTCHED_RIGHT_ARROW",
"OCTAGON",
"PARALLELOGRAM",
"PENTAGON",
"PIE",
"PLAQUE",
"PLUS",
"QUAD_ARROW",
"QUAD_ARROW_CALLOUT",
"RIBBON",
"RIBBON_2",
"RIGHT_ARROW",
"RIGHT_ARROW_CALLOUT",
"RIGHT_BRACE",
"RIGHT_BRACKET",
"ROUND_1_RECTANGLE",
"ROUND_2_DIAGONAL_RECTANGLE",
"ROUND_2_SAME_RECTANGLE",
"RIGHT_TRIANGLE",
"SMILEY_FACE",
"SNIP_1_RECTANGLE",
"SNIP_2_DIAGONAL_RECTANGLE",
"SNIP_2_SAME_RECTANGLE",
"SNIP_ROUND_RECTANGLE",
"STAR_10",
"STAR_12",
"STAR_16",
"STAR_24",
"STAR_32",
"STAR_4",
"STAR_5",
"STAR_6",
"STAR_7",
"STAR_8",
"STRIPED_RIGHT_ARROW",
"SUN",
"TRAPEZOID",
"TRIANGLE",
"UP_ARROW",
"UP_ARROW_CALLOUT",
"UP_DOWN_ARROW",
"UTURN_ARROW",
"VERTICAL_SCROLL",
"WAVE",
"WEDGE_ELLIPSE_CALLOUT",
"WEDGE_RECTANGLE_CALLOUT",
"WEDGE_ROUND_RECTANGLE_CALLOUT",
}
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
def format_slide_details(slide: Dict[str, Any], index: int) -> str:
"""
Format a single slide's details for display.
Args:
slide: Slide dict from Slides API
index: 1-based index of the slide
Returns:
Formatted string describing the slide
"""
slide_id = slide.get("objectId", "Unknown")
page_elements = slide.get("pageElements", [])
# Count different types of elements
element_counts = {}
for element in page_elements:
if "shape" in element:
element_counts["shapes"] = element_counts.get("shapes", 0) + 1
elif "table" in element:
element_counts["tables"] = element_counts.get("tables", 0) + 1
elif "line" in element:
element_counts["lines"] = element_counts.get("lines", 0) + 1
elif "image" in element:
element_counts["images"] = element_counts.get("images", 0) + 1
else:
element_counts["other"] = element_counts.get("other", 0) + 1
# Build element summary
element_summary = []
for elem_type, count in element_counts.items():
element_summary.append(f"{count} {elem_type}")
element_text = ", ".join(element_summary) if element_summary else "no elements"
return f" Slide {index}: ID {slide_id}, {len(page_elements)} total elements ({element_text})"
def format_page_element_details(element: Dict[str, Any]) -> str:
"""
Format a page element's details for display.
Args:
element: Page element dict from Slides API
Returns:
Formatted string describing the element
"""
element_id = element.get("objectId", "Unknown")
if "shape" in element:
shape = element["shape"]
shape_type = shape.get("shapeType", "Unknown")
# Check if shape has text
text_content = shape.get("text", {})
has_text = bool(text_content.get("textElements", []))
text_info = " (with text)" if has_text else ""
return f" Shape: ID {element_id}, Type: {shape_type}{text_info}"
elif "table" in element:
table = element["table"]
rows = table.get("rows", 0)
cols = table.get("columns", 0)
return f" Table: ID {element_id}, Size: {rows}x{cols}"
elif "line" in element:
line_type = element["line"].get("lineType", "Unknown")
return f" Line: ID {element_id}, Type: {line_type}"
elif "image" in element:
return f" Image: ID {element_id}"
elif "video" in element:
return f" Video: ID {element_id}"
elif "wordArt" in element:
return f" WordArt: ID {element_id}"
elif "sheetsChart" in element:
return f" Sheets Chart: ID {element_id}"
else:
return f" Element: ID {element_id}, Type: Unknown"
def validate_thumbnail_size(size: str) -> bool:
"""
Validate thumbnail size parameter.
Args:
size: Thumbnail size to validate
Returns:
bool: True if valid, False otherwise
"""
return size in VALID_THUMBNAIL_SIZES
def build_create_slide_request(
title: Optional[str] = None,
layout_id: Optional[str] = None,
index: Optional[int] = None,
) -> Dict[str, Any]:
"""
Build a create slide request for batch update.
Args:
title: Optional title for the slide
layout_id: Optional layout ID to use
index: Optional index to insert the slide at
Returns:
Create slide request dict
"""
request = {"createSlide": {}}
if index is not None:
request["createSlide"]["insertionIndex"] = index
if layout_id:
request["createSlide"]["slideLayoutReference"] = {"layoutId": layout_id}
# Note: Title is typically set after creation
return request
def build_create_shape_request(
slide_id: str,
shape_type: str = "TEXT_BOX",
element_properties: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Build a create shape request for batch update.
Args:
slide_id: ID of the slide to add shape to
shape_type: Type of shape to create
element_properties: Optional element properties
Returns:
Create shape request dict
"""
request = {
"createShape": {
"shapeType": shape_type,
"elementProperties": element_properties
or {
"pageObjectId": slide_id,
"size": {
"width": {"magnitude": 300, "unit": "PT"},
"height": {"magnitude": 100, "unit": "PT"},
},
"transform": {
"scaleX": 1,
"scaleY": 1,
"translateX": 100,
"translateY": 100,
"unit": "PT",
},
},
}
}
return request
# ============================================================================
# SERVICE HELPER FUNCTIONS
# ============================================================================
async def _get_slides_service_with_fallback(user_google_email: str):
"""Get Slides service with fallback to direct creation."""
try:
return await get_service("slides", user_google_email)
except Exception as e:
logger.warning(f"Failed to get Slides service via middleware: {e}")
logger.info("Falling back to direct service creation")
return await get_service("slides", user_google_email)
# ============================================================================
# MAIN TOOL FUNCTIONS
# ============================================================================
def setup_slides_tools(mcp: FastMCP) -> None:
"""
Setup and register all Google Slides tools with the MCP server.
Args:
mcp: The FastMCP server instance to register tools with
"""
logger.info("Setting up Google Slides tools")
@mcp.tool(
name="create_presentation",
description="Create a new Google Slides presentation",
tags={"slides", "create", "google", "presentation"},
annotations={
"title": "Create Google Slides Presentation",
"readOnlyHint": False,
"destructiveHint": False,
"idempotentHint": False,
"openWorldHint": True,
},
)
async def create_presentation(
user_google_email: UserGoogleEmailSlides = None,
title: str = "Untitled Presentation",
) -> CreatePresentationResponse:
"""
Create a new Google Slides presentation.
Args:
user_google_email (str): The user's Google email address. Required.
title (str): The title for the new presentation. Defaults to "Untitled Presentation".
Returns:
CreatePresentationResponse: Structured response with details about the created presentation.
"""
logger.info(
f"[create_presentation] Invoked. Email: '{user_google_email}', Title: '{title}'"
)
try:
# Get the Slides service via middleware injection
slides_service = await _get_slides_service_with_fallback(user_google_email)
# Build the presentation data
body = {"title": title}
# Create the presentation via the API
result = await asyncio.to_thread(
slides_service.presentations().create(body=body).execute
)
presentation_id = result.get("presentationId")
presentation_url = (
f"https://docs.google.com/presentation/d/{presentation_id}/edit"
)
logger.info(f"Presentation created successfully for {user_google_email}")
return CreatePresentationResponse(
presentationId=presentation_id,
presentationUrl=presentation_url,
title=title,
slideCount=len(result.get("slides", [])),
success=True,
message=f"Presentation '{title}' created successfully for {user_google_email}",
)
except HttpError as e:
error_msg = f"Failed to create presentation: {e}"
logger.error(f"[create_presentation] HTTP error: {e}")
return CreatePresentationResponse(
presentationId="",
presentationUrl="",
title=title,
slideCount=0,
success=False,
message="",
error=error_msg,
)
except Exception as e:
error_msg = f"Unexpected error creating presentation: {str(e)}"
logger.error(f"[create_presentation] Unexpected error: {e}")
return CreatePresentationResponse(
presentationId="",
presentationUrl="",
title=title,
slideCount=0,
success=False,
message="",
error=error_msg,
)
@mcp.tool(
name="get_presentation_info",
description="Get details about a Google Slides presentation including title, slides count, and metadata",
tags={"slides", "read", "google", "get", "info"},
annotations={
"title": "Get Presentation Information",
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": True,
"openWorldHint": True,
},
)
async def get_presentation_info(
presentation_id: str, user_google_email: UserGoogleEmailSlides = None
) -> PresentationInfoResponse:
"""
Get details about a Google Slides presentation.
Args:
user_google_email (str): The user's Google email address. Required.
presentation_id (str): The ID of the presentation to retrieve.
Returns:
PresentationInfoResponse: Structured response with presentation details.
"""
logger.info(
f"[get_presentation_info] Invoked. Email: '{user_google_email}', ID: '{presentation_id}'"
)
try:
slides_service = await _get_slides_service_with_fallback(user_google_email)
# Get the presentation
result = await asyncio.to_thread(
slides_service.presentations()
.get(presentationId=presentation_id)
.execute
)
title = result.get("title", "Untitled")
slides = result.get("slides", [])
page_size_data = result.get("pageSize", {})
# Build page size info
page_size: PageSize = {
"width": page_size_data.get("width", {}).get("magnitude", 0),
"height": page_size_data.get("height", {}).get("magnitude", 0),
"unit": page_size_data.get("width", {}).get("unit", "PT"),
}
# Build slide info list
slides_info: List[SlideInfo] = []
for i, slide in enumerate(slides):
slide_id = slide.get("objectId", "Unknown")
page_elements = slide.get("pageElements", [])
# Count different types of elements
element_counts = {}
for element in page_elements:
if "shape" in element:
element_counts["shapes"] = element_counts.get("shapes", 0) + 1
elif "table" in element:
element_counts["tables"] = element_counts.get("tables", 0) + 1
elif "line" in element:
element_counts["lines"] = element_counts.get("lines", 0) + 1
elif "image" in element:
element_counts["images"] = element_counts.get("images", 0) + 1
else:
element_counts["other"] = element_counts.get("other", 0) + 1
slide_info: SlideInfo = {
"objectId": slide_id,
"index": i,
"elementCount": len(page_elements),
"elementTypes": element_counts,
}
slides_info.append(slide_info)
return PresentationInfoResponse(
presentationId=presentation_id,
title=title,
presentationUrl=f"https://docs.google.com/presentation/d/{presentation_id}/edit",
slideCount=len(slides),
pageSize=page_size,
slides=slides_info,
)
except HttpError as e:
error_msg = f"Failed to get presentation: {e}"
logger.error(f"[get_presentation_info] HTTP error: {e}")
return PresentationInfoResponse(
presentationId=presentation_id,
title="",
presentationUrl="",
slideCount=0,
pageSize={"width": 0, "height": 0, "unit": ""},
slides=[],
error=error_msg,
)
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
logger.error(f"[get_presentation_info] {error_msg}")
return PresentationInfoResponse(
presentationId=presentation_id,
title="",
presentationUrl="",
slideCount=0,
pageSize={"width": 0, "height": 0, "unit": ""},
slides=[],
error=error_msg,
)
@mcp.tool(
name="add_slide",
description="Add a new slide to an existing Google Slides presentation",
tags={"slides", "update", "google", "add"},
annotations={
"title": "Add Slide to Presentation",
"readOnlyHint": False,
"destructiveHint": False,
"idempotentHint": False,
"openWorldHint": True,
},
)
async def add_slide(
presentation_id: str,
layout_id: Optional[str] = None,
index: Optional[int] = None,
user_google_email: UserGoogleEmailSlides = None,
) -> AddSlideResponse:
"""
Add a new slide to an existing presentation using batch update.
Args:
user_google_email (str): The user's Google email address. Required.
presentation_id (str): The ID of the presentation.
layout_id (Optional[str]): ID of the layout to use for the new slide.
index (Optional[int]): Position to insert the slide (0-based). If not specified, appends to end.
Returns:
AddSlideResponse: Structured response with details about the added slide.
"""
logger.info(
f"[add_slide] Invoked. Email: '{user_google_email}', Presentation: '{presentation_id}'"
)
try:
slides_service = await _get_slides_service_with_fallback(user_google_email)
# Build the create slide request
create_slide_request = build_create_slide_request(
layout_id=layout_id, index=index
)
# Execute batch update
body = {"requests": [create_slide_request]}
result = await asyncio.to_thread(
slides_service.presentations()
.batchUpdate(presentationId=presentation_id, body=body)
.execute
)
# Extract the created slide ID
replies = result.get("replies", [])
slide_id = None
if replies and "createSlide" in replies[0]:
slide_id = replies[0]["createSlide"].get("objectId", "Unknown")
logger.info(f"Slide added successfully for {user_google_email}")
return AddSlideResponse(
presentationId=presentation_id,
slideId=slide_id or "",
insertionIndex=index,
layoutId=layout_id,
presentationUrl=f"https://docs.google.com/presentation/d/{presentation_id}/edit",
success=True,
message=f"Successfully added slide to presentation {presentation_id}",
)
except HttpError as e:
error_msg = f"Failed to add slide: {e}"
logger.error(f"[add_slide] HTTP error: {e}")
return AddSlideResponse(
presentationId=presentation_id,
slideId="",
insertionIndex=index,
layoutId=layout_id,
presentationUrl="",
success=False,
message="",
error=error_msg,
)
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
logger.error(f"[add_slide] {error_msg}")
return AddSlideResponse(
presentationId=presentation_id,
slideId="",
insertionIndex=index,
layoutId=layout_id,
presentationUrl="",
success=False,
message="",
error=error_msg,
)
@mcp.tool(
name="update_slide_content",
description="Update content on a specific slide using batch update operations",
tags={"slides", "update", "google", "content"},
annotations={
"title": "Update Slide Content",
"readOnlyHint": False,
"destructiveHint": False,
"idempotentHint": False,
"openWorldHint": True,
},
)
async def update_slide_content(
presentation_id: str,
requests: List[Dict[str, Any]],
user_google_email: UserGoogleEmailSlides = None,
) -> UpdateSlideContentResponse:
"""
Apply batch updates to a Google Slides presentation.
This is a flexible tool that can handle various update operations including:
- Creating shapes, text boxes, tables, etc.
- Updating text content
- Modifying element properties
- Deleting elements
Args:
user_google_email (str): The user's Google email address. Required.
presentation_id (str): The ID of the presentation to update.
requests (List[Dict[str, Any]]): List of update requests to apply.
Returns:
UpdateSlideContentResponse: Structured response with batch update operation results.
"""
logger.info(
f"[update_slide_content] Invoked. Email: '{user_google_email}', ID: '{presentation_id}', Requests: {len(requests)}"
)
try:
slides_service = await _get_slides_service_with_fallback(user_google_email)
body = {"requests": requests}
result = await asyncio.to_thread(
slides_service.presentations()
.batchUpdate(presentationId=presentation_id, body=body)
.execute
)
replies = result.get("replies", [])
# Build structured replies
batch_replies: List[BatchUpdateReply] = []
for i, reply in enumerate(replies):
object_id = None
operation_type = "unknown"
details = "Operation completed"
if "createSlide" in reply:
object_id = reply["createSlide"].get("objectId")
operation_type = "createSlide"
details = f"Created slide with ID {object_id}"
elif "createShape" in reply:
object_id = reply["createShape"].get("objectId")
operation_type = "createShape"
details = f"Created shape with ID {object_id}"
elif "createTable" in reply:
object_id = reply["createTable"].get("objectId")
operation_type = "createTable"
details = f"Created table with ID {object_id}"
elif "createImage" in reply:
object_id = reply["createImage"].get("objectId")
operation_type = "createImage"
details = f"Created image with ID {object_id}"
else:
# Try to determine operation type from keys
if reply:
operation_type = list(reply.keys())[0] if reply else "unknown"
batch_reply: BatchUpdateReply = {
"requestIndex": i,
"operationType": operation_type,
"objectId": object_id,
"details": details,
}
batch_replies.append(batch_reply)
return UpdateSlideContentResponse(
presentationId=presentation_id,
presentationUrl=f"https://docs.google.com/presentation/d/{presentation_id}/edit",
requestCount=len(requests),
replyCount=len(replies),
replies=batch_replies,
success=True,
message=f"Batch update completed successfully for {user_google_email}",
)
except HttpError as e:
error_msg = f"Failed to update slide content: {e}"
logger.error(f"[update_slide_content] HTTP error: {e}")
return UpdateSlideContentResponse(
presentationId=presentation_id,
presentationUrl="",
requestCount=len(requests),
replyCount=0,
replies=[],
success=False,
message="",
error=error_msg,
)
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
logger.error(f"[update_slide_content] {error_msg}")
return UpdateSlideContentResponse(
presentationId=presentation_id,
presentationUrl="",
requestCount=len(requests),
replyCount=0,
replies=[],
success=False,
message="",
error=error_msg,
)
@mcp.tool(
name="export_and_download_presentation",
description="Export a Google Slides presentation to various formats with optional local download",
tags={"slides", "export", "download", "google", "file"},
annotations={
"title": "Export and Download Presentation",
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": False,
"openWorldHint": True,
},
)
async def export_and_download_presentation(
presentation_id: str,
export_format: str = "PDF",
download_to_local: bool = False,
download_directory: str = "./downloads/presentations",
user_google_email: UserGoogleEmailSlides = None,
) -> ExportAndDownloadPresentationResponse:
"""
Export a Google Slides presentation to various formats with optional local download.
This unified tool combines the functionality of both export_presentation and
get_presentation_file tools. It always generates export URLs and optionally
downloads the file to local storage based on the download_to_local parameter.
Args:
presentation_id (str): The ID of the presentation to export.
export_format (str): Export format - "PDF", "PPTX", "ODP", "TXT", "PNG", "JPEG", "SVG".
download_to_local (bool): Whether to download the file locally. If False, only generates URLs.
download_directory (str): Local directory to save the file. Only used if download_to_local=True.
user_google_email (str): The user's Google email address. Required.
Returns:
ExportAndDownloadPresentationResponse: Structured response with export URL and optional file details.
"""
logger.info(
f"[export_and_download_presentation] Email: '{user_google_email}', ID: '{presentation_id}', Format: '{export_format}', Download: {download_to_local}"
)
try:
# Map format strings to export MIME types
format_mapping = {
"PDF": "application/pdf",
"PPTX": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"ODP": "application/vnd.oasis.opendocument.presentation",
"TXT": "text/plain",
"PNG": "image/png",
"JPEG": "image/jpeg",
"SVG": "image/svg+xml",
}
export_format_upper = export_format.upper()
if export_format_upper not in format_mapping:
return ExportAndDownloadPresentationResponse(
presentationId=presentation_id,
presentationTitle="",
exportFormat=export_format_upper,
exportUrl="",
editUrl="",
downloaded=False,
fileInfo=None,
success=False,
message="",
error=f"Invalid export format. Supported formats: {', '.join(format_mapping.keys())}",
)
# Get presentation title (needed for both modes)
slides_service = await _get_slides_service_with_fallback(user_google_email)
presentation_info = await asyncio.to_thread(
slides_service.presentations()
.get(presentationId=presentation_id)
.execute
)
presentation_title = presentation_info.get("title", "Untitled_Presentation")
# Generate export URL
if export_format_upper in ["PNG", "JPEG", "SVG"]:
export_url = f"https://docs.google.com/presentation/d/{presentation_id}/export/{export_format_upper.lower()}"
else:
export_url = f"https://docs.google.com/presentation/d/{presentation_id}/export/{export_format_upper.lower()}"
edit_url = f"https://docs.google.com/presentation/d/{presentation_id}/edit"
# Set up warning for image formats
warning = None
if export_format_upper in ["PNG", "JPEG", "SVG"]:
warning = "For image formats, only the first slide is exported. To export all slides as images, use the get_page_thumbnail tool for each slide."
# If download not requested, return just the URLs
if not download_to_local:
logger.info(
f"Export URL generated successfully for {user_google_email}"
)
return ExportAndDownloadPresentationResponse(
presentationId=presentation_id,
presentationTitle=presentation_title,
exportFormat=export_format_upper,
exportUrl=export_url,
editUrl=edit_url,
downloaded=False,
fileInfo=None,
success=True,
message=f"Export URL generated for presentation '{presentation_title}' in {export_format_upper} format",
warning=warning,
)
# Download functionality
try:
# Get Drive service for file export/download
drive_service = await get_service("drive", user_google_email)
# Create download directory if it doesn't exist
os.makedirs(download_directory, exist_ok=True)
# Sanitize filename for filesystem safety
safe_title = re.sub(r'[<>:"/\\|?*]', "_", presentation_title)
safe_title = re.sub(r"[^\w\s\-_.]", "_", safe_title)
safe_title = re.sub(r"\s+", "_", safe_title.strip())
# Generate timestamp for unique filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# Build filename with proper extension
file_extension = export_format_upper.lower()
if file_extension == "jpeg":
file_extension = "jpg"
filename = f"{safe_title}_{timestamp}.{file_extension}"
local_file_path = os.path.join(download_directory, filename)
# Download the file using Drive API export
download_start_time = datetime.now()
# Use Drive API to export and get file content
if export_format_upper in ["PDF", "PPTX", "ODP", "TXT"]:
# Use Drive API export for document formats
mime_type = format_mapping[export_format_upper]
file_content = await asyncio.to_thread(
drive_service.files()
.export(fileId=presentation_id, mimeType=mime_type)
.execute
)
else:
# For image formats, use direct download approach
async with aiohttp.ClientSession() as session:
# Get credentials for authorization
credentials = drive_service._http.credentials
credentials.refresh(drive_service._http.http.request)
headers = {"Authorization": f"Bearer {credentials.token}"}
async with session.get(export_url, headers=headers) as response:
if response.status == 200:
file_content = await response.read()
else:
raise Exception(
f"Failed to download file: HTTP {response.status}"
)
# Save file to local storage
async with aiofiles.open(local_file_path, "wb") as f:
if isinstance(file_content, str):
await f.write(file_content.encode("utf-8"))
else:
await f.write(file_content)
download_end_time = datetime.now()
download_duration = (
download_end_time - download_start_time
).total_seconds()
# Get file size
file_size = os.path.getsize(local_file_path)
file_size_mb = file_size / (1024 * 1024)
# Build file info
file_info: FileDownloadInfo = {
"localPath": local_file_path,
"absolutePath": os.path.abspath(local_file_path),
"fileSize": file_size,
"fileSizeMB": file_size_mb,
"downloadDuration": download_duration,
"timestamp": download_end_time.strftime("%Y-%m-%d %H:%M:%S"),
}
logger.info(
f"[export_and_download_presentation] File downloaded successfully: {local_file_path}"
)
return ExportAndDownloadPresentationResponse(
presentationId=presentation_id,
presentationTitle=presentation_title,
exportFormat=export_format_upper,
exportUrl=export_url,
editUrl=edit_url,
downloaded=True,
fileInfo=file_info,
success=True,
message=f"Presentation '{presentation_title}' exported and downloaded successfully in {export_format_upper} format",
warning=warning,
)
except Exception as download_error:
# Clean up partial file if it exists
if "local_file_path" in locals() and os.path.exists(local_file_path):
try:
os.remove(local_file_path)
except:
pass
# Return with URL generation success but download failure
error_msg = f"Export URL generated successfully, but download failed: {str(download_error)}"
logger.error(
f"[export_and_download_presentation] Download error: {download_error}"
)
return ExportAndDownloadPresentationResponse(
presentationId=presentation_id,
presentationTitle=presentation_title,
exportFormat=export_format_upper,
exportUrl=export_url,
editUrl=edit_url,
downloaded=False,
fileInfo=None,
success=True, # URL generation succeeded
message=error_msg,
warning=warning,
error=f"Download failed: {str(download_error)}",
)
except HttpError as e:
error_msg = f"HTTP error accessing presentation: {e}"
logger.error(f"[export_and_download_presentation] HTTP error: {e}")
return ExportAndDownloadPresentationResponse(
presentationId=presentation_id,
presentationTitle="",
exportFormat=export_format.upper(),
exportUrl="",
editUrl="",
downloaded=False,
fileInfo=None,
success=False,
message="",
error=error_msg,
)
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
logger.error(f"[export_and_download_presentation] {error_msg}")
return ExportAndDownloadPresentationResponse(
presentationId=presentation_id,
presentationTitle="",
exportFormat=export_format.upper(),
exportUrl="",
editUrl="",
downloaded=False,
fileInfo=None,
success=False,
message="",
error=error_msg,
)
# Log successful setup
tool_count = 5 # Updated total number of Slides tools (combined export tools)
logger.info(f"Successfully registered {tool_count} Google Slides tools")