""" Batch Operation Manager This module provides high-level batch operation management for Google Docs, extracting complex validation and request building logic. """ import logging import asyncio from typing import Any, Union, Dict, List, Tuple from gdocs.docs_helpers import ( create_insert_text_request, create_delete_range_request, create_format_text_request, create_update_paragraph_style_request, create_update_table_cell_style_request, create_find_replace_request, create_insert_table_request, create_insert_page_break_request, create_insert_section_break_request, create_insert_image_request, create_bullet_list_request, create_delete_bullet_list_request, create_named_range_request, create_delete_named_range_request, create_replace_named_range_content_request, create_update_document_style_request, create_update_section_style_request, create_create_header_footer_request, create_insert_doc_tab_request, create_delete_doc_tab_request, create_update_doc_tab_request, create_insert_table_row_request, create_delete_table_row_request, create_insert_table_column_request, create_delete_table_column_request, create_merge_table_cells_request, create_unmerge_table_cells_request, create_update_table_column_properties_request, validate_operation, ) from gdocs.managers.validation_manager import ValidationManager logger = logging.getLogger(__name__) class BatchOperationManager: """ High-level manager for Google Docs batch operations. Handles complex multi-operation requests including: - Operation validation and request building - Batch execution with proper error handling - Operation result processing and reporting """ def __init__(self, service): """ Initialize the batch operation manager. Args: service: Google Docs API service instance """ self.service = service self.validation_manager = ValidationManager() async def execute_batch_operations( self, document_id: str, operations: list[dict[str, Any]] ) -> tuple[bool, str, dict[str, Any]]: """ Execute multiple document operations in a single atomic batch. This method extracts the complex logic from batch_update_doc tool function. Args: document_id: ID of the document to update operations: List of operation dictionaries Returns: Tuple of (success, message, metadata) """ logger.info(f"Executing batch operations on document {document_id}") logger.info(f"Operations count: {len(operations)}") if not operations: return ( False, "No operations provided. Please provide at least one operation.", {}, ) try: preflight_error = await self._preflight_create_header_footer_operations( document_id, operations ) if preflight_error: return False, preflight_error, {} # Validate and build requests requests, operation_descriptions = await self._validate_and_build_requests( operations ) if not requests: return False, "No valid requests could be built from operations", {} # Execute the batch result = await self._execute_batch_requests(document_id, requests) # Process results metadata = { "operations_count": len(operations), "requests_count": len(requests), "replies_count": len(result.get("replies", [])), "operation_summary": operation_descriptions[:5], # First 5 operations } # Fetch document length after batch for downstream chaining try: doc = await asyncio.to_thread( self.service.documents() .get(documentId=document_id, fields="body/content(endIndex)") .execute ) body_content = doc.get("body", {}).get("content", []) metadata["document_length"] = ( body_content[-1].get("endIndex", 0) if body_content else 1 ) except Exception: metadata["document_length"] = None # Extract new tab IDs from insert_doc_tab replies created_tabs = self._extract_created_tabs(result) if created_tabs: metadata["created_tabs"] = created_tabs summary = self._build_operation_summary(operation_descriptions) msg = f"Successfully executed {len(operations)} operations ({summary})" if created_tabs: tab_info = ", ".join( f"'{t['title']}' (tab_id: {t['tab_id']})" for t in created_tabs ) msg += f". Created tabs: {tab_info}" return True, msg, metadata except Exception as e: error_msg = self._rewrite_execution_error(str(e), operations) logger.error(f"Failed to execute batch operations: {error_msg}") return False, error_msg, {} async def _preflight_create_header_footer_operations( self, document_id: str, operations: list[dict[str, Any]] ) -> str | None: """ Validate create_header_footer operations against the live document before sending low-level Docs API requests that often fail opaquely. """ create_ops = [ op for op in operations if op.get("type") == "create_header_footer" ] if not create_ops: return None doc = await asyncio.to_thread( self.service.documents().get(documentId=document_id).execute ) style_field_map = { "header": { "DEFAULT": "defaultHeaderId", "FIRST_PAGE_ONLY": "firstPageHeaderId", "EVEN_PAGE": "evenPageHeaderId", }, "footer": { "DEFAULT": "defaultFooterId", "FIRST_PAGE_ONLY": "firstPageFooterId", "EVEN_PAGE": "evenPageFooterId", }, } section_breaks: dict[int, dict[str, Any]] = {} for element in doc.get("body", {}).get("content", []): if "sectionBreak" in element: section_breaks[element.get("startIndex", 0)] = element["sectionBreak"] for op in create_ops: section_type = op["section_type"] header_footer_type = op.get("header_footer_type", "DEFAULT") style_field = style_field_map[section_type][header_footer_type] section_break_index = op.get("section_break_index") if section_break_index is None: if doc.get("documentStyle", {}).get(style_field): return ( "Batch operation failed: the requested header/footer already exists. " "For normal header or footer text, use update_doc_headers_footers " "instead of batch_update_doc with create_header_footer. " "Reserve create_header_footer for advanced section-break layouts." ) continue if section_break_index not in section_breaks: available = sorted(section_breaks.keys()) return ( "Batch operation failed: section_break_index must match an existing " "section break start index from inspect_doc_structure.section_breaks[]. " f"Got {section_break_index}. Available section break indices: {available or 'none'}. " "For normal header/footer text, use update_doc_headers_footers instead." ) if ( section_breaks[section_break_index] .get("sectionStyle", {}) .get(style_field) ): return ( "Batch operation failed: the requested section-scoped header/footer " "already exists at that section break. Use inspect_doc_structure to " "find real section breaks, or use update_doc_headers_footers for " "ordinary header/footer text." ) return None async def _validate_and_build_requests( self, operations: list[dict[str, Any]] ) -> tuple[list[dict[str, Any]], list[str]]: """ Validate operations and build API requests. Args: operations: List of operation dictionaries Returns: Tuple of (requests, operation_descriptions) """ requests = [] operation_descriptions = [] for i, op in enumerate(operations): # Validate operation structure is_valid, error_msg = validate_operation(op) if not is_valid: raise ValueError(f"Operation {i + 1}: {error_msg}") op_type = op.get("type") try: # Build request based on operation type result = self._build_operation_request(op, op_type) # Handle both single request and list of requests if isinstance(result[0], list): # Multiple requests (e.g., replace_text) for req in result[0]: requests.append(req) operation_descriptions.append(result[1]) elif result[0]: # Single request requests.append(result[0]) operation_descriptions.append(result[1]) except KeyError as e: raise ValueError( f"Operation {i + 1} ({op_type}) missing required field: {e}" ) except Exception as e: raise ValueError( f"Operation {i + 1} ({op_type}) failed validation: {str(e)}" ) return requests, operation_descriptions def _build_operation_request( self, op: dict[str, Any], op_type: str ) -> Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], str]: """ Build a single operation request. Args: op: Operation dictionary op_type: Operation type Returns: Tuple of (request, description) """ tab_id = op.get("tab_id") segment_id = op.get("segment_id") end_of_segment = op.get("end_of_segment", False) if op_type == "insert_text": request = create_insert_text_request( op.get("index"), op["text"], tab_id, segment_id=segment_id, end_of_segment=end_of_segment, ) description = ( f"insert text at end of segment '{segment_id or 'body'}'" if end_of_segment else f"insert text at {op['index']}" ) elif op_type == "delete_text": request = create_delete_range_request( op["start_index"], op["end_index"], tab_id, segment_id=segment_id, ) description = f"delete text {op['start_index']}-{op['end_index']}" elif op_type == "replace_text": delete_request = create_delete_range_request( op["start_index"], op["end_index"], tab_id, segment_id=segment_id, ) insert_request = create_insert_text_request( op["start_index"], op["text"], tab_id, segment_id=segment_id, ) request = [delete_request, insert_request] description = f"replace text {op['start_index']}-{op['end_index']} with '{op['text'][:20]}{'...' if len(op['text']) > 20 else ''}'" elif op_type == "format_text": request = create_format_text_request( op["start_index"], op["end_index"], op.get("bold"), op.get("italic"), op.get("underline"), op.get("strikethrough"), op.get("font_size"), op.get("font_family"), op.get("font_weight"), op.get("text_color"), op.get("background_color"), op.get("link_url"), op.get("clear_link"), op.get("baseline_offset"), op.get("small_caps"), tab_id, segment_id, ) if not request: raise ValueError("No formatting options provided") format_changes = [] for param, name in [ ("bold", "bold"), ("italic", "italic"), ("underline", "underline"), ("strikethrough", "strikethrough"), ("font_size", "font size"), ("font_family", "font family"), ("font_weight", "font weight"), ("text_color", "text color"), ("background_color", "background color"), ("link_url", "link"), ("clear_link", "clear link"), ("baseline_offset", "baseline offset"), ("small_caps", "small caps"), ]: if op.get(param) is not None: if param == "font_size": value = f"{op[param]}pt" elif param == "font_weight": value = f"{op[param]}w" else: value = op[param] format_changes.append(f"{name}: {value}") description = f"format text {op['start_index']}-{op['end_index']} ({', '.join(format_changes)})" elif op_type == "update_paragraph_style": request = create_update_paragraph_style_request( op["start_index"], op["end_index"], op.get("heading_level"), op.get("alignment"), op.get("line_spacing"), op.get("indent_first_line"), op.get("indent_start"), op.get("indent_end"), op.get("space_above"), op.get("space_below"), tab_id, op.get("named_style_type"), segment_id, op.get("direction"), op.get("keep_lines_together"), op.get("keep_with_next"), op.get("avoid_widow_and_orphan"), op.get("page_break_before"), op.get("spacing_mode"), op.get("shading_color"), ) if not request: raise ValueError("No paragraph style options provided") _PT_PARAMS = { "indent_first_line", "indent_start", "indent_end", "space_above", "space_below", } _SUFFIX = { "heading_level": lambda v: f"H{v}", "line_spacing": lambda v: f"{v}x", } style_changes = [] for param, name in [ ("heading_level", "heading"), ("alignment", "alignment"), ("line_spacing", "line spacing"), ("indent_first_line", "first line indent"), ("indent_start", "start indent"), ("indent_end", "end indent"), ("space_above", "space above"), ("space_below", "space below"), ("named_style_type", "named style"), ("direction", "direction"), ("keep_lines_together", "keep lines together"), ("keep_with_next", "keep with next"), ("avoid_widow_and_orphan", "avoid widow/orphan"), ("page_break_before", "page break before"), ("spacing_mode", "spacing mode"), ("shading_color", "shading"), ]: if op.get(param) is not None: raw = op[param] fmt = _SUFFIX.get(param) if fmt: value = fmt(raw) elif param in _PT_PARAMS: value = f"{raw}pt" else: value = raw style_changes.append(f"{name}: {value}") description = f"paragraph style {op['start_index']}-{op['end_index']} ({', '.join(style_changes)})" elif op_type == "update_table_cell_style": is_valid, error_msg = ( self.validation_manager.validate_table_cell_style_params( background_color=op.get("background_color"), border_color=op.get("border_color"), border_width=op.get("border_width"), padding_top=op.get("padding_top"), padding_bottom=op.get("padding_bottom"), padding_left=op.get("padding_left"), padding_right=op.get("padding_right"), content_alignment=op.get("content_alignment"), row_index=op.get("row_index"), column_index=op.get("column_index"), row_span=op.get("row_span"), column_span=op.get("column_span"), ) ) if not is_valid: raise ValueError(error_msg) request = create_update_table_cell_style_request( table_start_index=op["table_start_index"], background_color=op.get("background_color"), border_color=op.get("border_color"), border_width=op.get("border_width"), padding_top=op.get("padding_top"), padding_bottom=op.get("padding_bottom"), padding_left=op.get("padding_left"), padding_right=op.get("padding_right"), content_alignment=op.get("content_alignment"), row_index=op.get("row_index"), column_index=op.get("column_index"), row_span=op.get("row_span"), column_span=op.get("column_span"), tab_id=tab_id, ) if not request: raise ValueError("No table cell style options provided") style_changes = [] for param, name in [ ("background_color", "background"), ("border_color", "border color"), ("border_width", "border width"), ("padding_top", "padding top"), ("padding_bottom", "padding bottom"), ("padding_left", "padding left"), ("padding_right", "padding right"), ("content_alignment", "content alignment"), ]: if op.get(param) is not None: value = ( f"{op[param]}pt" if param in ( "border_width", "padding_top", "padding_bottom", "padding_left", "padding_right", ) else op[param] ) style_changes.append(f"{name}: {value}") if op.get("row_index") is not None: row_span = op.get("row_span", 1) column_span = op.get("column_span", 1) target = ( f"row {op['row_index']}, column {op['column_index']}, " f"span {row_span}x{column_span}" ) else: target = "entire table" description = ( f"table cell style at {op['table_start_index']} " f"({target}; {', '.join(style_changes)})" ) elif op_type == "insert_table": request = create_insert_table_request( op.get("index"), op["rows"], op["columns"], tab_id, segment_id=segment_id, end_of_segment=end_of_segment, ) description = ( f"insert {op['rows']}x{op['columns']} table at end of segment '{segment_id or 'body'}'" if end_of_segment else f"insert {op['rows']}x{op['columns']} table at {op['index']}" ) elif op_type == "insert_page_break": request = create_insert_page_break_request( op.get("index"), tab_id, end_of_segment=end_of_segment ) description = ( "insert page break at end of body" if end_of_segment else f"insert page break at {op['index']}" ) elif op_type == "insert_section_break": request = create_insert_section_break_request( op.get("index"), op.get("section_type", "NEXT_PAGE"), end_of_segment=end_of_segment, ) description = ( f"insert {op.get('section_type', 'NEXT_PAGE')} section break at end of body" if end_of_segment else f"insert {op.get('section_type', 'NEXT_PAGE')} section break at {op.get('index')}" ) elif op_type == "find_replace": request = create_find_replace_request( op["find_text"], op["replace_text"], op.get("match_case", False), tab_id ) description = f"find/replace '{op['find_text']}' → '{op['replace_text']}'" elif op_type == "create_bullet_list": list_type = op.get("list_type", "UNORDERED") if list_type not in ("UNORDERED", "ORDERED", "CHECKBOX", "NONE"): raise ValueError( f"Invalid list_type '{list_type}'. Must be 'UNORDERED', 'ORDERED', 'CHECKBOX', or 'NONE'" ) if list_type == "NONE": request = create_delete_bullet_list_request( op["start_index"], op["end_index"], tab_id, segment_id=segment_id ) description = f"remove bullets {op['start_index']}-{op['end_index']}" else: request = create_bullet_list_request( op["start_index"], op["end_index"], list_type, op.get("nesting_level"), op.get("paragraph_start_indices"), tab_id, op.get("bullet_preset"), segment_id=segment_id, ) if list_type == "UNORDERED": style = "bulleted" elif list_type == "CHECKBOX": style = "checkbox" else: style = "numbered" description = ( f"create {style} list {op['start_index']}-{op['end_index']}" ) if op.get("nesting_level"): description += f" (nesting level {op['nesting_level']})" if op.get("bullet_preset"): description += f" using {op['bullet_preset']}" elif op_type == "create_named_range": request = create_named_range_request( op["name"], op["start_index"], op["end_index"], tab_id=tab_id, segment_id=segment_id, ) description = ( f"create named range '{op['name']}' " f"{op['start_index']}-{op['end_index']}" ) elif op_type == "replace_named_range_content": request = create_replace_named_range_content_request( op["text"], named_range_id=op.get("named_range_id"), named_range_name=op.get("named_range_name"), tab_id=tab_id, ) target = op.get("named_range_id") or op.get("named_range_name") description = f"replace named range content for '{target}'" elif op_type == "delete_named_range": request = create_delete_named_range_request( named_range_id=op.get("named_range_id"), named_range_name=op.get("named_range_name"), tab_id=tab_id, ) target = op.get("named_range_id") or op.get("named_range_name") description = f"delete named range '{target}'" elif op_type == "update_document_style": request = create_update_document_style_request( tab_id=tab_id, background_color=op.get("background_color"), margin_top=op.get("margin_top"), margin_bottom=op.get("margin_bottom"), margin_left=op.get("margin_left"), margin_right=op.get("margin_right"), margin_header=op.get("margin_header"), margin_footer=op.get("margin_footer"), page_width=op.get("page_width"), page_height=op.get("page_height"), page_number_start=op.get("page_number_start"), use_even_page_header_footer=op.get("use_even_page_header_footer"), use_first_page_header_footer=op.get("use_first_page_header_footer"), flip_page_orientation=op.get("flip_page_orientation"), document_mode=op.get("document_mode"), ) if not request: raise ValueError("No document style options provided") description = "update document style" elif op_type == "update_section_style": request = create_update_section_style_request( op["start_index"], op["end_index"], margin_top=op.get("margin_top"), margin_bottom=op.get("margin_bottom"), margin_left=op.get("margin_left"), margin_right=op.get("margin_right"), margin_header=op.get("margin_header"), margin_footer=op.get("margin_footer"), page_number_start=op.get("page_number_start"), use_first_page_header_footer=op.get("use_first_page_header_footer"), flip_page_orientation=op.get("flip_page_orientation"), content_direction=op.get("content_direction"), column_count=op.get("column_count"), column_spacing=op.get("column_spacing"), column_separator_style=op.get("column_separator_style"), ) if not request: raise ValueError("No section style options provided") description = f"update section style {op['start_index']}-{op['end_index']}" elif op_type == "create_header_footer": request = create_create_header_footer_request( op["section_type"], op.get("header_footer_type", "DEFAULT"), op.get("section_break_index"), ) description = f"create {op['section_type']} ({op.get('header_footer_type', 'DEFAULT')})" elif op_type == "insert_image": request = create_insert_image_request( op.get("index"), op["image_uri"], op.get("width"), op.get("height"), tab_id, segment_id=segment_id, end_of_segment=end_of_segment, ) description = ( f"insert image at end of segment '{segment_id or 'body'}'" if end_of_segment else f"insert image at {op.get('index')}" ) elif op_type == "insert_doc_tab": request = create_insert_doc_tab_request( op["title"], op["index"], op.get("parent_tab_id") ) description = f"insert tab '{op['title']}' at {op['index']}" if op.get("parent_tab_id"): description += f" under parent tab {op['parent_tab_id']}" elif op_type == "delete_doc_tab": request = create_delete_doc_tab_request(op["tab_id"]) description = f"delete tab '{op['tab_id']}'" elif op_type == "update_doc_tab": request = create_update_doc_tab_request(op["tab_id"], op["title"]) description = f"rename tab '{op['tab_id']}' to '{op['title']}'" elif op_type == "insert_table_row": request = create_insert_table_row_request( table_start_index=op["table_start_index"], row_index=op["row_index"], insert_below=op.get("insert_below", True), tab_id=tab_id, ) direction = "below" if op.get("insert_below", True) else "above" description = f"insert row {direction} row {op['row_index']} in table at {op['table_start_index']}" elif op_type == "delete_table_row": request = create_delete_table_row_request( table_start_index=op["table_start_index"], row_index=op["row_index"], tab_id=tab_id, ) description = ( f"delete row {op['row_index']} from table at {op['table_start_index']}" ) elif op_type == "insert_table_column": request = create_insert_table_column_request( table_start_index=op["table_start_index"], column_index=op["column_index"], insert_right=op.get("insert_right", True), tab_id=tab_id, ) direction = "right of" if op.get("insert_right", True) else "left of" description = f"insert column {direction} column {op['column_index']} in table at {op['table_start_index']}" elif op_type == "delete_table_column": request = create_delete_table_column_request( table_start_index=op["table_start_index"], column_index=op["column_index"], tab_id=tab_id, ) description = f"delete column {op['column_index']} from table at {op['table_start_index']}" elif op_type == "merge_table_cells": request = create_merge_table_cells_request( table_start_index=op["table_start_index"], row_index=op["row_index"], column_index=op["column_index"], row_span=op["row_span"], column_span=op["column_span"], tab_id=tab_id, ) description = ( f"merge cells at ({op['row_index']},{op['column_index']}) " f"span {op['row_span']}x{op['column_span']} in table at {op['table_start_index']}" ) elif op_type == "unmerge_table_cells": request = create_unmerge_table_cells_request( table_start_index=op["table_start_index"], row_index=op["row_index"], column_index=op["column_index"], row_span=op["row_span"], column_span=op["column_span"], tab_id=tab_id, ) description = ( f"unmerge cells at ({op['row_index']},{op['column_index']}) " f"span {op['row_span']}x{op['column_span']} in table at {op['table_start_index']}" ) elif op_type == "update_table_column_properties": request = create_update_table_column_properties_request( table_start_index=op["table_start_index"], column_indices=op["column_indices"], width=op.get("width"), width_type=op.get("width_type"), tab_id=tab_id, ) if not request: raise ValueError( "update_table_column_properties requires at least one of: width, width_type" ) description = ( f"update column properties for columns {op['column_indices']} " f"in table at {op['table_start_index']}" ) else: supported_types = [ "insert_text", "delete_text", "replace_text", "format_text", "update_paragraph_style", "update_table_cell_style", "insert_table", "insert_page_break", "insert_section_break", "find_replace", "create_bullet_list", "create_named_range", "replace_named_range_content", "delete_named_range", "update_document_style", "update_section_style", "create_header_footer", "insert_image", "insert_doc_tab", "delete_doc_tab", "update_doc_tab", "insert_table_row", "delete_table_row", "insert_table_column", "delete_table_column", "merge_table_cells", "unmerge_table_cells", "update_table_column_properties", ] raise ValueError( f"Unsupported operation type '{op_type}'. Supported: {', '.join(supported_types)}" ) return request, description async def _execute_batch_requests( self, document_id: str, requests: list[dict[str, Any]] ) -> dict[str, Any]: """ Execute the batch requests against the Google Docs API. Args: document_id: Document ID requests: List of API requests Returns: API response """ return await asyncio.to_thread( self.service.documents() .batchUpdate(documentId=document_id, body={"requests": requests}) .execute ) def _extract_created_tabs(self, result: dict[str, Any]) -> list[dict[str, str]]: """ Extract tab IDs from insert_doc_tab replies in the batchUpdate response. Args: result: The batchUpdate API response Returns: List of dicts with tab_id and title for each created tab """ created_tabs = [] for reply in result.get("replies", []): # Google's Docs API returns the result under "addDocumentTab" # matching the request field name. Accept "createDocumentTab" too # in case the API surfaces that key in some legacy or future branch. for key in ("addDocumentTab", "createDocumentTab"): if key in reply: props = reply[key].get("tabProperties", {}) tab_id = props.get("tabId") title = props.get("title", "") if tab_id: created_tabs.append({"tab_id": tab_id, "title": title}) break return created_tabs def _build_operation_summary(self, operation_descriptions: list[str]) -> str: """ Build a concise summary of operations performed. Args: operation_descriptions: List of operation descriptions Returns: Summary string """ if not operation_descriptions: return "no operations" summary_items = operation_descriptions[:3] # Show first 3 operations summary = ", ".join(summary_items) if len(operation_descriptions) > 3: remaining = len(operation_descriptions) - 3 summary += f" and {remaining} more operation{'s' if remaining > 1 else ''}" return summary def _rewrite_execution_error( self, error_msg: str, operations: list[dict[str, Any]] ) -> str: """ Rewrite common API failures into actionable guidance for tool callers. """ lowered = error_msg.lower() requested_header_footer_creation = any( op.get("type") == "create_header_footer" for op in operations ) if ( requested_header_footer_creation and "already exists" in lowered and ("createheader" in lowered or "createfooter" in lowered) ): return ( "Batch operation failed: the requested header/footer already exists. " "For normal header or footer text, use update_doc_headers_footers " "instead of batch_update_doc with create_header_footer. " "Reserve create_header_footer for advanced section-break layouts." ) return f"Batch operation failed: {error_msg}" def get_supported_operations(self) -> dict[str, Any]: """ Get information about supported batch operations. Returns: Dictionary with supported operation types and their required parameters """ return { "supported_operations": { "insert_text": { "required": ["index", "text"], "description": "Insert text at specified index", }, "delete_text": { "required": ["start_index", "end_index"], "description": "Delete text in specified range", }, "replace_text": { "required": ["start_index", "end_index", "text"], "description": "Replace text in range with new text", }, "format_text": { "required": ["start_index", "end_index"], "optional": [ "bold", "italic", "underline", "strikethrough", "font_size", "font_family", "font_weight", "text_color", "background_color", "link_url", "clear_link", "baseline_offset", "small_caps", "segment_id", "tab_id", ], "description": "Apply formatting to text range", }, "update_paragraph_style": { "required": ["start_index", "end_index"], "optional": [ "heading_level", "alignment", "line_spacing", "indent_first_line", "indent_start", "indent_end", "space_above", "space_below", "named_style_type", "direction", "keep_lines_together", "keep_with_next", "avoid_widow_and_orphan", "page_break_before", "spacing_mode", "shading_color", "segment_id", "tab_id", ], "description": "Apply paragraph-level styling (headings, named styles like TITLE/SUBTITLE, alignment, spacing, indentation)", }, "update_table_cell_style": { "required": ["table_start_index"], "optional": [ "background_color", "border_color", "border_width", "padding_top", "padding_bottom", "padding_left", "padding_right", "content_alignment", "row_index", "column_index", "row_span", "column_span", ], "description": "Apply table cell styling to an entire table or a targeted cell range", }, "insert_table": { "required": ["rows", "columns"], "optional": ["index", "end_of_segment", "segment_id", "tab_id"], "description": "Insert table at specified index or end of a segment", }, "insert_page_break": { "required": [], "optional": ["index", "end_of_segment", "tab_id"], "description": "Insert page break at specified index or end of body", }, "insert_section_break": { "required": [], "optional": ["index", "end_of_segment", "section_type"], "description": "Insert a CONTINUOUS or NEXT_PAGE section break", }, "find_replace": { "required": ["find_text", "replace_text"], "optional": ["match_case"], "description": "Find and replace text throughout document", }, "create_bullet_list": { "required": ["start_index", "end_index"], "optional": [ "list_type", "nesting_level", "paragraph_start_indices", "bullet_preset", ], "description": "Apply or remove native bullet/numbered/checklist formatting (list_type: UNORDERED, ORDERED, CHECKBOX, or NONE to remove; nesting_level: 0-8)", }, "create_named_range": { "required": ["name", "start_index", "end_index"], "optional": ["tab_id", "segment_id"], "description": "Create a named range anchor over a range", }, "replace_named_range_content": { "required": ["text"], "optional": ["named_range_id", "named_range_name", "tab_id"], "description": "Replace the content of one or more named ranges", }, "delete_named_range": { "required": [], "optional": ["named_range_id", "named_range_name", "tab_id"], "description": "Delete a named range by ID or name", }, "update_document_style": { "required": [], "optional": [ "background_color", "margin_top", "margin_bottom", "margin_left", "margin_right", "margin_header", "margin_footer", "page_width", "page_height", "page_number_start", "use_even_page_header_footer", "use_first_page_header_footer", "flip_page_orientation", "document_mode", "tab_id", ], "description": "Update document-level layout settings such as margins, page size, headers/footers, and background", }, "update_section_style": { "required": ["start_index", "end_index"], "optional": [ "margin_top", "margin_bottom", "margin_left", "margin_right", "margin_header", "margin_footer", "page_number_start", "use_first_page_header_footer", "flip_page_orientation", "content_direction", "column_count", "column_spacing", "column_separator_style", ], "description": "Update section-specific layout such as margins, pagination, orientation, and columns", }, "create_header_footer": { "required": ["section_type"], "optional": ["header_footer_type", "section_break_index"], "description": "Create a header or footer, optionally tied to a section break", }, "insert_image": { "required": ["image_uri"], "optional": [ "index", "end_of_segment", "segment_id", "tab_id", "width", "height", ], "description": "Insert an inline image at a location or at the end of a segment", }, "insert_doc_tab": { "required": ["title", "index"], "description": "Insert a new document tab with given title at specified index", }, "delete_doc_tab": { "required": ["tab_id"], "description": "Delete a document tab by its ID", }, "update_doc_tab": { "required": ["tab_id", "title"], "description": "Rename a document tab", }, "insert_table_row": { "required": ["table_start_index", "row_index"], "optional": ["insert_below", "tab_id"], "description": "Insert a row above or below a reference row in a table", }, "delete_table_row": { "required": ["table_start_index", "row_index"], "optional": ["tab_id"], "description": "Delete a row from a table", }, "insert_table_column": { "required": ["table_start_index", "column_index"], "optional": ["insert_right", "tab_id"], "description": "Insert a column to the left or right of a reference column in a table", }, "delete_table_column": { "required": ["table_start_index", "column_index"], "optional": ["tab_id"], "description": "Delete a column from a table", }, "merge_table_cells": { "required": [ "table_start_index", "row_index", "column_index", "row_span", "column_span", ], "optional": ["tab_id"], "description": "Merge a rectangular range of cells in a table", }, "unmerge_table_cells": { "required": [ "table_start_index", "row_index", "column_index", "row_span", "column_span", ], "optional": ["tab_id"], "description": "Unmerge cells in a table that were previously merged", }, "update_table_column_properties": { "required": ["table_start_index", "column_indices"], "optional": ["width", "width_type", "tab_id"], "description": "Update column width and width type for specified columns in a table", }, }, "example_operations": [ {"type": "insert_text", "index": 1, "text": "Hello World"}, { "type": "format_text", "start_index": 1, "end_index": 12, "bold": True, }, {"type": "insert_table", "index": 20, "rows": 2, "columns": 3}, { "type": "update_paragraph_style", "start_index": 1, "end_index": 20, "heading_level": 1, "alignment": "CENTER", }, ], }