Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion core/tool_tier_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

logger = logging.getLogger(__name__)

TierLevel = Literal["core", "extended", "complete"]
TierLevel = Literal["core", "extended", "complete", "leo"]


class ToolTierLoader:
Expand Down Expand Up @@ -109,6 +109,15 @@ def get_tools_up_to_tier(
Returns:
List of tool names up to the specified tier level
"""
# Special handling for standalone tiers (e.g., "leo")
# These tiers are not cumulative and contain their own tool set
standalone_tiers = ["leo"]

if tier in standalone_tiers:
# For standalone tiers, return only the tools in that tier
return self.get_tools_for_tier(tier, services)

# For cumulative tiers (core, extended, complete), accumulate tools
tier_order = ["core", "extended", "complete"]
max_tier_index = tier_order.index(tier)

Expand Down
46 changes: 46 additions & 0 deletions core/tool_tiers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,19 @@ gmail:
- batch_modify_gmail_message_labels
- start_google_auth

leo:
- search_gmail_messages
- get_gmail_message_content
- get_gmail_messages_content_batch
- start_google_auth

drive:
core:
- search_drive_files
- get_drive_file_content
- get_drive_file_download_url
- create_drive_file
- import_to_google_doc
- share_drive_file
- get_drive_shareable_link
extended:
Expand All @@ -36,6 +43,18 @@ drive:
- get_drive_file_permissions
- check_drive_file_public_access

leo:
- search_drive_files
- get_drive_file_content
- get_drive_file_download_url
- create_drive_file
- import_to_google_doc
- share_drive_file
- get_drive_shareable_link
- list_drive_items
- update_drive_file
- get_drive_file_permissions

calendar:
core:
- list_calendars
Expand All @@ -57,6 +76,8 @@ docs:
- find_and_replace_doc
- list_docs_in_folder
- insert_doc_elements
- format_paragraph_style
- apply_heading_style
complete:
- insert_doc_image
- update_doc_headers_footers
Expand All @@ -69,6 +90,24 @@ docs:
- reply_to_document_comment
- resolve_document_comment

leo:
- get_doc_content
- create_doc
- modify_doc_text
- export_doc_to_pdf
- search_docs
- find_and_replace_doc
- list_docs_in_folder
- insert_doc_elements
- format_paragraph_style
- apply_heading_style
- insert_doc_image
- update_doc_headers_footers
- batch_update_doc
- inspect_doc_structure
- create_table_with_data
- debug_table_structure

sheets:
core:
- create_spreadsheet
Expand Down Expand Up @@ -117,6 +156,13 @@ slides:
- reply_to_presentation_comment
- resolve_presentation_comment

leo:
- create_presentation
- get_presentation
- batch_update_presentation
- get_page
- get_page_thumbnail

tasks:
core:
- get_task
Expand Down
190 changes: 190 additions & 0 deletions gdocs/docs_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,196 @@ async def export_doc_to_pdf(
return f"Error: Failed to upload PDF to Drive: {str(e)}. PDF was generated successfully ({pdf_size:,} bytes) but could not be saved to Drive."


# ==============================================================================
# STYLING TOOLS - Text and Paragraph Formatting
# ==============================================================================



@server.tool()
@handle_http_errors("format_paragraph_style", service_type="docs")
@require_google_service("docs", "docs_write")
async def format_paragraph_style(
service: Any,
user_google_email: str,
document_id: str,
start_index: int,
end_index: int,
alignment: str = None,
line_spacing: float = None,
indent_first_line: float = None,
indent_start: float = None,
indent_end: float = None,
space_above: float = None,
space_below: float = None,
) -> str:
"""
Apply paragraph-level formatting to a range in a Google Doc.

This tool modifies paragraph properties like alignment, spacing, and indentation
without changing the text content.

Args:
user_google_email: User's Google email address
document_id: Document ID to modify
start_index: Start position (1-based)
end_index: End position (exclusive)
alignment: Text alignment - 'START' (left), 'CENTER', 'END' (right), or 'JUSTIFIED'
line_spacing: Line spacing multiplier (1.0 = single, 1.5 = 1.5x, 2.0 = double)
indent_first_line: First line indent in points (e.g., 36 for 0.5 inch)
indent_start: Left/start indent in points
indent_end: Right/end indent in points
space_above: Space above paragraph in points (e.g., 12 for one line)
space_below: Space below paragraph in points

Returns:
str: Confirmation message with formatting details
"""
logger.info(
f"[format_paragraph_style] Doc={document_id}, Range: {start_index}-{end_index}"
)

# Validate range
if start_index < 1:
return "Error: start_index must be >= 1"
if end_index <= start_index:
return "Error: end_index must be greater than start_index"

# Build paragraph style object
paragraph_style = {}
fields = []

if alignment is not None:
valid_alignments = ["START", "CENTER", "END", "JUSTIFIED"]
alignment_upper = alignment.upper()
if alignment_upper not in valid_alignments:
return f"Error: Invalid alignment '{alignment}'. Must be one of: {valid_alignments}"
paragraph_style["alignment"] = alignment_upper
fields.append("alignment")

if line_spacing is not None:
if line_spacing <= 0:
return "Error: line_spacing must be positive"
paragraph_style["lineSpacing"] = line_spacing * 100 # Convert to percentage
fields.append("lineSpacing")

if indent_first_line is not None:
paragraph_style["indentFirstLine"] = {"magnitude": indent_first_line, "unit": "PT"}
fields.append("indentFirstLine")

if indent_start is not None:
paragraph_style["indentStart"] = {"magnitude": indent_start, "unit": "PT"}
fields.append("indentStart")

if indent_end is not None:
paragraph_style["indentEnd"] = {"magnitude": indent_end, "unit": "PT"}
fields.append("indentEnd")

if space_above is not None:
paragraph_style["spaceAbove"] = {"magnitude": space_above, "unit": "PT"}
fields.append("spaceAbove")

if space_below is not None:
paragraph_style["spaceBelow"] = {"magnitude": space_below, "unit": "PT"}
fields.append("spaceBelow")

if not paragraph_style:
return f"No paragraph formatting changes specified for document {document_id}"

# Create batch update request
requests = [
{
"updateParagraphStyle": {
"range": {"startIndex": start_index, "endIndex": end_index},
"paragraphStyle": paragraph_style,
"fields": ",".join(fields),
}
}
]

await asyncio.to_thread(
service.documents()
.batchUpdate(documentId=document_id, body={"requests": requests})
.execute
)

format_summary = ", ".join(f"{f}={paragraph_style.get(f, 'set')}" for f in fields)
link = f"https://docs.google.com/document/d/{document_id}/edit"
return f"Applied paragraph formatting ({format_summary}) to range {start_index}-{end_index} in document {document_id}. Link: {link}"


@server.tool()
@handle_http_errors("apply_heading_style", service_type="docs")
@require_google_service("docs", "docs_write")
async def apply_heading_style(
service: Any,
user_google_email: str,
document_id: str,
start_index: int,
end_index: int,
heading_level: int,
) -> str:
"""
Apply a Google Docs named heading style (H1-H6) to a paragraph range.

This uses Google Docs' built-in heading styles which automatically apply
consistent formatting based on the document's style settings. Use this
for semantic document structure rather than manual font size changes.

Args:
user_google_email: User's Google email address
document_id: Document ID to modify
start_index: Start position (1-based)
end_index: End position (exclusive) - should cover the entire paragraph
heading_level: Heading level 1-6 (1 = H1/Title, 2 = H2/Subtitle, etc.)
Use 0 to reset to NORMAL_TEXT style

Returns:
str: Confirmation message
"""
logger.info(
f"[apply_heading_style] Doc={document_id}, Range: {start_index}-{end_index}, Level: {heading_level}"
)

# Validate range
if start_index < 1:
return "Error: start_index must be >= 1"
if end_index <= start_index:
return "Error: end_index must be greater than start_index"

# Validate heading level
if heading_level < 0 or heading_level > 6:
return "Error: heading_level must be between 0 (normal text) and 6"

# Map heading level to Google Docs named style
if heading_level == 0:
heading_style = "NORMAL_TEXT"
else:
heading_style = f"HEADING_{heading_level}"

# Create batch update request
requests = [
{
"updateParagraphStyle": {
"range": {"startIndex": start_index, "endIndex": end_index},
"paragraphStyle": {"namedStyleType": heading_style},
"fields": "namedStyleType",
}
}
]

await asyncio.to_thread(
service.documents()
.batchUpdate(documentId=document_id, body={"requests": requests})
.execute
)

link = f"https://docs.google.com/document/d/{document_id}/edit"
return f"Applied {heading_style} style to range {start_index}-{end_index} in document {document_id}. Link: {link}"




# Create comment management tools for documents
_comment_tools = create_comment_tools("document", "document_id")

Expand Down
Loading