Spaces:
Runtime error
Runtime error
| """ | |
| 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_find_replace_request, | |
| create_insert_table_request, | |
| create_insert_page_break_request, | |
| validate_operation, | |
| ) | |
| 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 | |
| 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: | |
| # 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 | |
| } | |
| summary = self._build_operation_summary(operation_descriptions) | |
| return ( | |
| True, | |
| f"Successfully executed {len(operations)} operations ({summary})", | |
| metadata, | |
| ) | |
| except Exception as e: | |
| logger.error(f"Failed to execute batch operations: {str(e)}") | |
| return False, f"Batch operation failed: {str(e)}", {} | |
| 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) | |
| """ | |
| if op_type == "insert_text": | |
| request = create_insert_text_request(op["index"], op["text"]) | |
| description = f"insert text at {op['index']}" | |
| elif op_type == "delete_text": | |
| request = create_delete_range_request(op["start_index"], op["end_index"]) | |
| description = f"delete text {op['start_index']}-{op['end_index']}" | |
| elif op_type == "replace_text": | |
| # Replace is delete + insert (must be done in this order) | |
| delete_request = create_delete_range_request( | |
| op["start_index"], op["end_index"] | |
| ) | |
| insert_request = create_insert_text_request(op["start_index"], op["text"]) | |
| # Return both requests as a list | |
| 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("font_size"), | |
| op.get("font_family"), | |
| op.get("text_color"), | |
| op.get("background_color"), | |
| ) | |
| if not request: | |
| raise ValueError("No formatting options provided") | |
| # Build format description | |
| format_changes = [] | |
| for param, name in [ | |
| ("bold", "bold"), | |
| ("italic", "italic"), | |
| ("underline", "underline"), | |
| ("font_size", "font size"), | |
| ("font_family", "font family"), | |
| ("text_color", "text color"), | |
| ("background_color", "background color"), | |
| ]: | |
| if op.get(param) is not None: | |
| value = f"{op[param]}pt" if param == "font_size" else op[param] | |
| format_changes.append(f"{name}: {value}") | |
| description = f"format text {op['start_index']}-{op['end_index']} ({', '.join(format_changes)})" | |
| elif op_type == "insert_table": | |
| request = create_insert_table_request( | |
| op["index"], op["rows"], op["columns"] | |
| ) | |
| description = f"insert {op['rows']}x{op['columns']} table at {op['index']}" | |
| elif op_type == "insert_page_break": | |
| request = create_insert_page_break_request(op["index"]) | |
| description = f"insert page break at {op['index']}" | |
| elif op_type == "find_replace": | |
| request = create_find_replace_request( | |
| op["find_text"], op["replace_text"], op.get("match_case", False) | |
| ) | |
| description = f"find/replace '{op['find_text']}' → '{op['replace_text']}'" | |
| else: | |
| supported_types = [ | |
| "insert_text", | |
| "delete_text", | |
| "replace_text", | |
| "format_text", | |
| "insert_table", | |
| "insert_page_break", | |
| "find_replace", | |
| ] | |
| 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 _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 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", | |
| "font_size", | |
| "font_family", | |
| "text_color", | |
| "background_color", | |
| ], | |
| "description": "Apply formatting to text range", | |
| }, | |
| "insert_table": { | |
| "required": ["index", "rows", "columns"], | |
| "description": "Insert table at specified index", | |
| }, | |
| "insert_page_break": { | |
| "required": ["index"], | |
| "description": "Insert page break at specified index", | |
| }, | |
| "find_replace": { | |
| "required": ["find_text", "replace_text"], | |
| "optional": ["match_case"], | |
| "description": "Find and replace text throughout document", | |
| }, | |
| }, | |
| "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}, | |
| ], | |
| } | |