feat(sheets): expose cell notes in read_sheet_values

This commit is contained in:
Bortlesboat
2026-03-15 17:33:02 -04:00
parent 92b4a7847f
commit 2e0d6393f4
2 changed files with 118 additions and 1 deletions

View File

@@ -877,3 +877,81 @@ def _build_gradient_rule(
rule_body["gradientRule"]["midpoint"] = gradient_points[1] rule_body["gradientRule"]["midpoint"] = gradient_points[1]
rule_body["gradientRule"]["maxpoint"] = gradient_points[2] rule_body["gradientRule"]["maxpoint"] = gradient_points[2]
return rule_body return rule_body
def _extract_cell_notes_from_grid(spreadsheet: dict) -> list[dict[str, str]]:
"""
Extract cell notes from spreadsheet grid data.
Returns a list of dictionaries with:
- "cell": cell A1 reference
- "note": the note text
"""
notes: list[dict[str, str]] = []
for sheet in spreadsheet.get("sheets", []) or []:
sheet_title = sheet.get("properties", {}).get("title") or "Unknown"
for grid in sheet.get("data", []) or []:
start_row = _coerce_int(grid.get("startRow"), default=0)
start_col = _coerce_int(grid.get("startColumn"), default=0)
for row_offset, row_data in enumerate(grid.get("rowData", []) or []):
if not row_data:
continue
for col_offset, cell_data in enumerate(
row_data.get("values", []) or []
):
if not cell_data:
continue
note = cell_data.get("note")
if not note:
continue
notes.append(
{
"cell": _format_a1_cell(
sheet_title,
start_row + row_offset,
start_col + col_offset,
),
"note": note,
}
)
return notes
async def _fetch_sheet_notes(
service, spreadsheet_id: str, a1_range: str
) -> list[dict[str, str]]:
"""Fetch cell notes for the given range via spreadsheets.get with includeGridData."""
response = await asyncio.to_thread(
service.spreadsheets()
.get(
spreadsheetId=spreadsheet_id,
ranges=[a1_range],
includeGridData=True,
fields="sheets(properties(title),data(startRow,startColumn,rowData(values(note))))",
)
.execute
)
return _extract_cell_notes_from_grid(response)
def _format_sheet_notes_section(
*, notes: list[dict[str, str]], range_label: str, max_details: int = 25
) -> str:
"""
Format a list of cell notes into a human-readable section.
"""
if not notes:
return ""
lines = []
for item in notes[:max_details]:
cell = item.get("cell") or "(unknown cell)"
note = item.get("note") or "(empty note)"
lines.append(f"- {cell}: {note}")
suffix = (
f"\n... and {len(notes) - max_details} more notes"
if len(notes) > max_details
else ""
)
return f"\n\nCell notes in range '{range_label}':\n" + "\n".join(lines) + suffix

View File

@@ -22,9 +22,11 @@ from gsheets.sheets_helpers import (
_build_gradient_rule, _build_gradient_rule,
_fetch_detailed_sheet_errors, _fetch_detailed_sheet_errors,
_fetch_sheet_hyperlinks, _fetch_sheet_hyperlinks,
_fetch_sheet_notes,
_fetch_sheets_with_rules, _fetch_sheets_with_rules,
_format_conditional_rules_section, _format_conditional_rules_section,
_format_sheet_hyperlink_section, _format_sheet_hyperlink_section,
_format_sheet_notes_section,
_format_sheet_error_section, _format_sheet_error_section,
_parse_a1_range, _parse_a1_range,
_parse_condition_values, _parse_condition_values,
@@ -179,6 +181,7 @@ async def read_sheet_values(
spreadsheet_id: str, spreadsheet_id: str,
range_name: str = "A1:Z1000", range_name: str = "A1:Z1000",
include_hyperlinks: bool = False, include_hyperlinks: bool = False,
include_notes: bool = False,
) -> str: ) -> str:
""" """
Reads values from a specific range in a Google Sheet. Reads values from a specific range in a Google Sheet.
@@ -189,6 +192,8 @@ async def read_sheet_values(
range_name (str): The range to read (e.g., "Sheet1!A1:D10", "A1:D10"). Defaults to "A1:Z1000". range_name (str): The range to read (e.g., "Sheet1!A1:D10", "A1:D10"). Defaults to "A1:Z1000".
include_hyperlinks (bool): If True, also fetch hyperlink metadata for the range. include_hyperlinks (bool): If True, also fetch hyperlink metadata for the range.
Defaults to False to avoid expensive includeGridData requests. Defaults to False to avoid expensive includeGridData requests.
include_notes (bool): If True, also fetch cell notes for the range.
Defaults to False to avoid expensive includeGridData requests.
Returns: Returns:
str: The formatted values from the specified range. str: The formatted values from the specified range.
@@ -247,6 +252,40 @@ async def read_sheet_values(
MAX_HYPERLINK_FETCH_CELLS, MAX_HYPERLINK_FETCH_CELLS,
) )
notes_section = ""
if include_notes:
notes_range = _a1_range_for_values(resolved_range, values)
if not notes_range:
logger.info(
"[read_sheet_values] Skipping notes fetch for range '%s': unable to determine tight bounds",
resolved_range,
)
else:
cell_count = _a1_range_cell_count(notes_range) or sum(
len(row) for row in values
)
if cell_count <= MAX_HYPERLINK_FETCH_CELLS:
try:
notes = await _fetch_sheet_notes(
service, spreadsheet_id, notes_range
)
notes_section = _format_sheet_notes_section(
notes=notes, range_label=notes_range
)
except Exception as exc:
logger.warning(
"[read_sheet_values] Failed fetching notes for range '%s': %s",
notes_range,
exc,
)
else:
logger.info(
"[read_sheet_values] Skipping notes fetch for large range '%s' (%d cells > %d limit)",
notes_range,
cell_count,
MAX_HYPERLINK_FETCH_CELLS,
)
detailed_errors_section = "" detailed_errors_section = ""
if _values_contain_sheets_errors(values): if _values_contain_sheets_errors(values):
try: try:
@@ -277,7 +316,7 @@ async def read_sheet_values(
) )
logger.info(f"Successfully read {len(values)} rows for {user_google_email}.") logger.info(f"Successfully read {len(values)} rows for {user_google_email}.")
return text_output + hyperlink_section + detailed_errors_section return text_output + hyperlink_section + notes_section + detailed_errors_section
@server.tool() @server.tool()