Skip to content

feat: add suggestion-aware doc text tools with section chunking - #629

Closed
pgonzale60 wants to merge 6 commits into
taylorwilsdon:mainfrom
pgonzale60:feature/doc-text-suggestions
Closed

feat: add suggestion-aware doc text tools with section chunking#629
pgonzale60 wants to merge 6 commits into
taylorwilsdon:mainfrom
pgonzale60:feature/doc-text-suggestions

Conversation

@pgonzale60

@pgonzale60 pgonzale60 commented Mar 28, 2026

Copy link
Copy Markdown

Summary

  • Adds list_doc_sections tool — returns a table of contents based on heading structure, or chunk info for headingless documents
  • Adds get_doc_text tool — retrieves document text in original mode (excluding pending insertions, preserving pending deletions) or accepted mode (insertions included, deletions excluded)
  • Supports section-based access via section_index and chunk-based access via chunk_index/chunk_size for long documents
  • New gdocs/docs_text.py module with extract_sections and render_elements helpers
  • 15 unit tests covering all extraction and suggestion-filtering logic

Test plan

  • uv run pytest tests/test_docs_text.py -v — all 15 tests pass
  • Call list_doc_sections on a doc with headings — verify numbered TOC returned
  • Call list_doc_sections on a headingless doc — verify char count and chunk info returned
  • Call get_doc_text with mode="original" — verify suggested insertions absent
  • Call get_doc_text with mode="accepted" — verify suggested deletions absent
  • Call get_doc_text with out-of-range section_index — verify clear error message
  • Call get_doc_text on a .docx file — verify error directing to get_doc_content

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Google Contacts tools: list, search, view, create, update, delete contacts.
    • Google Docs text extraction: section- and chunk-based retrieval with suggestion-aware rendering.
    • Configurable OAuth callback port and updated startup/display of the callback endpoint.
  • Bug Fixes

    • Comments loading now uses paginated fetching to improve reliability/coverage.
  • Tests

    • Added unit tests covering Docs text extraction and suggestion modes.

pgonzale60 and others added 5 commits December 11, 2025 12:55
Added comprehensive Google Contacts integration with 6 core tools:

Tools implemented:
- list_contacts: List all contacts with pagination support
- search_contacts: Search contacts by name, email, or phone
- get_contact: Get detailed contact information
- create_contact: Create new contacts with name, email, phone, organization
- update_contact: Update existing contact information
- delete_contact: Permanently delete contacts

Technical changes:
- Created gcontacts module following existing service patterns
- Added People API scopes (contacts.readonly and contacts)
- Integrated with auth system and service decorator
- Added contacts to tool tiers configuration
  * Core tier: list, search, get (read-only)
  * Extended tier: create, update (write operations)
  * Complete tier: delete (destructive operation)
- Updated main.py to register contacts module
- Added contacts to CLI argument choices

Usage:
  uv run main.py --tools contacts
  uv run main.py --tool-tier core --tools contacts

Requires enabling People API in Google Cloud Console:
https://console.cloud.google.com/flows/enableapi?apiid=people.googleapis.com
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…areness

- New gdocs/docs_text.py helper: extract_sections (heading-based chunking)
  and render_elements (original vs accepted suggestion modes)
- list_doc_sections returns TOC or chunk info for headingless docs
- get_doc_text supports mode=original|accepted, section_index, chunk_index
- 15 unit tests covering all extraction logic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds Google Contacts (People API) MCP tools, suggestion-aware Google Docs text extraction and sectioning utilities, and configurable OAuth callback port support via WORKSPACE_MCP_OAUTH_CALLBACK_PORT. Also loads an additional .env.oauth21 file and paginates comment reads.

Changes

Cohort / File(s) Summary
OAuth config & startup
auth/oauth_config.py, fastmcp_server.py, main.py
Added callback_port / callback_base_url (from WORKSPACE_MCP_OAUTH_CALLBACK_PORT), switched redirect URI construction to use callback base, load .env.oauth21 (non-overriding), and pass callback port into stdio OAuth startup/display.
Auth scopes & services
auth/scopes.py, auth/service_decorator.py
Added Google Contacts scopes (CONTACTS_READONLY_SCOPE, CONTACTS_SCOPE) and CONTACTS_SCOPES; registered contacts in TOOL_SCOPES_MAP; added people service config and contacts_read/contacts_write scope groups.
Google Contacts tools
gcontacts/__init__.py, gcontacts/contacts_tools.py, core/tool_tiers.yaml
New contacts MCP tools: list_contacts, search_contacts, get_contact, create_contact, update_contact, delete_contact. Tools include pagination, normalized JSON responses, error handling, and tool tier entries.
Google Docs text extraction & tools
gdocs/docs_text.py, gdocs/docs_tools.py, docs/superpowers/specs/...
New helpers render_elements and extract_sections to render suggestion-aware text and group by headings; added list_doc_sections and get_doc_text read-only tools with section/chunk scoping and mode validation.
Core: comments pagination
core/comments.py
Refactored _read_comments_impl to paginate using pageToken/pageSize=100, accumulating comments across pages.
Tests
tests/test_docs_text.py
Added unit tests for gdocs.docs_text (render_elements, extract_sections, and suggestion view mode mapping).

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client
participant FastMCP
participant DriveAPI as Drive API
participant DocsAPI as Docs API
participant DocsText as gdocs.docs_text
Client->>FastMCP: list_doc_sections(document_id, mode=original)
FastMCP->>DriveAPI: Get file metadata (mimeType)
DriveAPI-->>FastMCP: mimeType
FastMCP->>DocsAPI: Get document body (suggestionsViewMode=original)
DocsAPI-->>FastMCP: body elements
FastMCP->>DocsText: extract_sections(body elements)
DocsText-->>FastMCP: sections list
FastMCP-->>Client: formatted sections / guidance

mermaid
sequenceDiagram
participant Client
participant FastMCP
participant PeopleAPI as People API
Client->>FastMCP: list_contacts(page_size, page_token)
FastMCP->>PeopleAPI: connections().list(personId=people/me,...,pageToken)
PeopleAPI-->>FastMCP: connections + nextPageToken
FastMCP->>PeopleAPI: (loop) connections().list(nextPageToken)
FastMCP-->>Client: normalized JSON contacts + nextPageToken

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • #602 — Related changes to Docs suggestion rendering and gdocs tooling.
  • #510 — Related additions/changes to OAuth scope constants and scope mapping.
  • #531 — Related work touching contacts tooling and potential consolidation of contact APIs.

Suggested labels

enhancement

Poem

🐰
I hopped through scopes and docs today,
Added contacts, sections, and a port that may sway,
Callback tuned, pages paged, suggestions kept neat,
Tools lined up for workspace feats—
A rabbit’s small cheer for every API play.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding suggestion-aware doc text extraction tools with section/chunk scoping support.
Description check ✅ Passed The PR description is largely complete with clear summary, test plan, and helpful context, though some template sections like 'Type of Change' checklist and 'Checklist' items are not formally marked.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feature/doc-text-suggestions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
tests/test_docs_text.py (2)

102-105: Remove redundant import.

render_elements is already imported at line 6. This local re-import is unnecessary.

🧹 Proposed fix
     def test_depth_guard(self):
         # Should return empty string at depth > 5
-        from gdocs.docs_text import render_elements
         assert render_elements([make_paragraph([make_text_run("x\n")])], "original", depth=6) == ""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_docs_text.py` around lines 102 - 105, The test function
test_depth_guard contains a redundant local import of render_elements; remove
the line "from gdocs.docs_text import render_elements" inside test_depth_guard
so the function uses the render_elements already imported at the top of the
file, leaving the rest of the test (the call to render_elements and the
assertion) unchanged.

2-4: Prefer proper package configuration over sys.path manipulation.

Direct sys.path manipulation is fragile and can cause issues in different environments. Consider using a pyproject.toml or setup.py with the package installed in editable mode (pip install -e .), which allows imports to work naturally.

If this pattern is required for CI compatibility, consider using pytest's pythonpath configuration in pyproject.toml or pytest.ini instead.

♻️ Alternative using pytest configuration

In pyproject.toml:

[tool.pytest.ini_options]
pythonpath = ["."]

Then the test file can simply be:

 """Unit tests for gdocs/docs_text.py"""
-import sys
-import os
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
 from gdocs.docs_text import render_elements, extract_sections
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_docs_text.py` around lines 2 - 4, The test file
tests/test_docs_text.py contains brittle sys.path manipulation via
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
remove that line and rely on proper package installation or pytest configuration
instead—either install the package in editable mode (pip install -e .) so
imports work naturally, or add pythonpath = ["."] under
[tool.pytest.ini_options] in pyproject.toml (or equivalent pytest.ini) so tests
can import without modifying sys.path.
gdocs/docs_text.py (1)

35-48: Depth guard uses strict inequality - verify boundary behavior.

The guard if depth > 5 returns empty at depth 6, meaning depth 5 is the last level that renders content. For deeply nested tables (tables within table cells), this allows 6 levels of recursion (0-5). This seems reasonable but should be documented.

Consider adding a brief comment explaining the chosen limit (e.g., "Google Docs rarely has tables nested more than 5 levels deep").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gdocs/docs_text.py` around lines 35 - 48, The depth guard in render_elements
currently uses "if depth > 5" which permits recursion for depths 0–5 and stops
at 6; add a concise explanatory comment next to the guard inside the
render_elements function (or above it) stating the intentional limit and
rationale (e.g., "limit recursion to 5 levels; Google Docs rarely nests tables
deeper than 5"), so readers understand the boundary behavior and that depth 5 is
the last rendered level.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/comments.py`:
- Around line 137-153: The current loop in core/comments.py which uses
service.comments().list to fetch all pages (variables: comments, page_token,
kwargs) must be changed to enforce a hard cap (e.g., max_items) and support
cursor-based continuation: add a max_items parameter to the enclosing function
or tool contract, stop fetching once len(comments) >= max_items, truncate the
returned comments array to max_items, and return the current page_token (or a
nextCursor) so callers can continue pagination later; ensure you do not
accumulate all pages into memory and do not perform long-running reads beyond
the request budget by breaking the loop early and signaling continuation.

In `@docs/superpowers/specs/2026-03-28-doc-text-with-suggestions-design.md`:
- Around line 96-104: The fenced code blocks showing the response format
examples are missing language specifiers; update both examples that contain the
blocks with captions "[Section 2/8: \"Introduction\" | mode: original]" and
"[Chunk 1/5 | mode: accepted | chunk_size: 10000]" to use a plain-text specifier
(e.g., change the opening triple backticks to ```text) so markdownlint is
satisfied and the examples render consistently.

In `@gcontacts/contacts_tools.py`:
- Around line 125-126: Multiple functions in gcontacts.contacts_tools.py are
importing json inside their bodies (e.g., the locations around lines 125-126,
213-214, 304-305, 398-399, 507-508, 551-552) which is inefficient; move a single
import json to the top of the module and delete the local "import json"
statements from each function (those that call json.dumps(response, indent=2)
and similar). Ensure the module-level import is placed with other imports and
that all functions continue using json.* without local imports.
- Line 44: Existing logger calls leak PII by including user_google_email (e.g.,
the logger.info in list_contacts); update all logger statements that interpolate
user_google_email to remove the email and instead log non-PII context such as
the tool name, operation (e.g., "list_contacts invoked"), or a session/trace id;
search for logger.* calls in contacts_tools.py that mention user_google_email
(including the instances flagged) and replace the message to omit the email
while preserving useful context and any existing log level or structured fields.
- Around line 479-482: The early return uses json.dumps when update_mask is
empty, but json is imported later in the function which causes a NameError; fix
by importing json at module level (preferred) or at least before the
early-return check inside the function so that the call to json.dumps in the
update flow (referencing update_mask) can execute without error—ensure the
import is placed before any use of json in contacts_tools.py where functions
like the update routine reference update_mask.

---

Nitpick comments:
In `@gdocs/docs_text.py`:
- Around line 35-48: The depth guard in render_elements currently uses "if depth
> 5" which permits recursion for depths 0–5 and stops at 6; add a concise
explanatory comment next to the guard inside the render_elements function (or
above it) stating the intentional limit and rationale (e.g., "limit recursion to
5 levels; Google Docs rarely nests tables deeper than 5"), so readers understand
the boundary behavior and that depth 5 is the last rendered level.

In `@tests/test_docs_text.py`:
- Around line 102-105: The test function test_depth_guard contains a redundant
local import of render_elements; remove the line "from gdocs.docs_text import
render_elements" inside test_depth_guard so the function uses the
render_elements already imported at the top of the file, leaving the rest of the
test (the call to render_elements and the assertion) unchanged.
- Around line 2-4: The test file tests/test_docs_text.py contains brittle
sys.path manipulation via sys.path.insert(0,
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); remove that line
and rely on proper package installation or pytest configuration instead—either
install the package in editable mode (pip install -e .) so imports work
naturally, or add pythonpath = ["."] under [tool.pytest.ini_options] in
pyproject.toml (or equivalent pytest.ini) so tests can import without modifying
sys.path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c971b265-e966-4e8d-89d7-4246d59f97d5

📥 Commits

Reviewing files that changed from the base of the PR and between 58278de and b3ad45a.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • auth/oauth_config.py
  • auth/scopes.py
  • auth/service_decorator.py
  • core/comments.py
  • core/tool_tiers.yaml
  • docs/superpowers/specs/2026-03-28-doc-text-with-suggestions-design.md
  • fastmcp_server.py
  • gcontacts/__init__.py
  • gcontacts/contacts_tools.py
  • gdocs/docs_text.py
  • gdocs/docs_tools.py
  • main.py
  • tests/test_docs_text.py

Comment thread core/comments.py
Comment on lines +137 to +153
comments = []
page_token = None
while True:
kwargs = dict(
fileId=file_id,
fields="comments(id,content,author,createdTime,modifiedTime,resolved,replies(content,author,id,createdTime,modifiedTime))"
).execute
)

comments = response.get('comments', [])
pageSize=100,
fields="nextPageToken,comments(id,content,author,createdTime,modifiedTime,resolved,replies(content,author,id,createdTime,modifiedTime))"
)
if page_token:
kwargs["pageToken"] = page_token
response = await asyncio.to_thread(
service.comments().list(**kwargs).execute
)
comments.extend(response.get('comments', []))
page_token = response.get('nextPageToken')
if not page_token:
break

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bound paginated reads to avoid oversized/slow tool responses.

Line 137-Line 153 now fetches and accumulates every page into one response. On large files, this can exceed practical MCP payload size and request-time budgets. Please add a hard cap (max_items) plus cursor-based continuation in the tool contract (or at minimum truncate and signal continuation token).

Suggested direction
 async def _read_comments_impl(service, app_name: str, file_id: str) -> str:
@@
-    comments = []
-    page_token = None
+    comments = []
+    page_token = None
+    max_items = 250
+    truncated = False
@@
-        comments.extend(response.get('comments', []))
+        page_comments = response.get('comments', [])
+        comments.extend(page_comments)
+        if len(comments) >= max_items:
+            comments = comments[:max_items]
+            truncated = True
+            page_token = response.get('nextPageToken')
+            break
         page_token = response.get('nextPageToken')
         if not page_token:
             break
@@
-    output = [f"Found {len(comments)} comments in {app_name} {file_id}:\\n"]
+    output = [f"Found {len(comments)} comments in {app_name} {file_id}:\\n"]
+    if truncated or page_token:
+        output.append("Results truncated; add cursor/max_items parameters to continue pagination.")
+        output.append("")

As per coding guidelines, "Use pagination helpers (list_page_size, nextCursor) for large result sets to prevent oversized LLM responses" and "Avoid long-running operations (>30s) inside request context; instead stream partial results or schedule background tasks."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/comments.py` around lines 137 - 153, The current loop in
core/comments.py which uses service.comments().list to fetch all pages
(variables: comments, page_token, kwargs) must be changed to enforce a hard cap
(e.g., max_items) and support cursor-based continuation: add a max_items
parameter to the enclosing function or tool contract, stop fetching once
len(comments) >= max_items, truncate the returned comments array to max_items,
and return the current page_token (or a nextCursor) so callers can continue
pagination later; ensure you do not accumulate all pages into memory and do not
perform long-running reads beyond the request budget by breaking the loop early
and signaling continuation.

Comment on lines +96 to +104
```
[Section 2/8: "Introduction" | mode: original]
<text>
```
or for chunks:
```
[Chunk 1/5 | mode: accepted | chunk_size: 10000]
<text>
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language specifiers to fenced code blocks.

The response format examples are missing language specifiers, which was flagged by markdownlint. Adding text or plaintext as the language will satisfy the linter and improve documentation consistency.

📝 Proposed fix
 **Response format:** Text content prefixed with metadata header:
-```
+```text
 [Section 2/8: "Introduction" | mode: original]
 <text>

or for chunks:
- +text
[Chunk 1/5 | mode: accepted | chunk_size: 10000]

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
[Section 2/8: "Introduction" | mode: original]
<text>
```
or for chunks:
```
[Chunk 1/5 | mode: accepted | chunk_size: 10000]
<text>
```
**Response format:** Text content prefixed with metadata header:
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 96-96: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 101-101: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-03-28-doc-text-with-suggestions-design.md` around
lines 96 - 104, The fenced code blocks showing the response format examples are
missing language specifiers; update both examples that contain the blocks with
captions "[Section 2/8: \"Introduction\" | mode: original]" and "[Chunk 1/5 |
mode: accepted | chunk_size: 10000]" to use a plain-text specifier (e.g., change
the opening triple backticks to ```text) so markdownlint is satisfied and the
examples render consistently.

Returns:
str: JSON string containing list of contacts with names, email addresses, phone numbers, and pagination info.
"""
logger.info(f"[list_contacts] Invoked. Email: '{user_google_email}'")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid logging user email addresses (PII).

Multiple log statements include user_google_email, which is personally identifiable information. As per coding guidelines: "Never log or leak secrets, refresh tokens, or PII in exceptions or event streams."

Consider logging only non-PII context such as the tool name or a session identifier.

🛡️ Proposed fix example
-    logger.info(f"[list_contacts] Invoked. Email: '{user_google_email}'")
+    logger.info("[list_contacts] Invoked")

Apply similar changes to all affected log statements.

Also applies to: 156-156, 242-243, 343-343, 443-443, 539-539

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gcontacts/contacts_tools.py` at line 44, Existing logger calls leak PII by
including user_google_email (e.g., the logger.info in list_contacts); update all
logger statements that interpolate user_google_email to remove the email and
instead log non-PII context such as the tool name, operation (e.g.,
"list_contacts invoked"), or a session/trace id; search for logger.* calls in
contacts_tools.py that mention user_google_email (including the instances
flagged) and replace the message to omit the email while preserving useful
context and any existing log level or structured fields.

Comment on lines +125 to +126
import json
return json.dumps(response, indent=2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Move json import to module level.

The json module is imported inside each function. This is inefficient and unconventional. Moving it to the module level improves readability and follows standard Python practices.

♻️ Proposed fix

Add at module level:

 import asyncio
+import json
 import logging
 from typing import Any, Dict, List, Optional

Then remove all the import json statements inside functions.

Also applies to: 213-214, 304-305, 398-399, 507-508, 551-552

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gcontacts/contacts_tools.py` around lines 125 - 126, Multiple functions in
gcontacts.contacts_tools.py are importing json inside their bodies (e.g., the
locations around lines 125-126, 213-214, 304-305, 398-399, 507-508, 551-552)
which is inefficient; move a single import json to the top of the module and
delete the local "import json" statements from each function (those that call
json.dumps(response, indent=2) and similar). Ensure the module-level import is
placed with other imports and that all functions continue using json.* without
local imports.

Comment on lines +479 to +482
if not update_mask:
return json.dumps({
"error": "No fields to update. Please provide at least one field to update."
}, indent=2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

json module used before import — will raise NameError.

At line 480, json.dumps is called, but json is not imported until line 507 in this function. This will cause a NameError at runtime when no fields are provided to update.

🐛 Proposed fix — move import to top of function or module level

Either add at module level (preferred):

 import asyncio
 import logging
+import json
 from typing import Any, Dict, List, Optional

Or add before the early return:

         if not update_mask:
+            import json
             return json.dumps({
                 "error": "No fields to update. Please provide at least one field to update."
             }, indent=2)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gcontacts/contacts_tools.py` around lines 479 - 482, The early return uses
json.dumps when update_mask is empty, but json is imported later in the function
which causes a NameError; fix by importing json at module level (preferred) or
at least before the early-return check inside the function so that the call to
json.dumps in the update flow (referencing update_mask) can execute without
error—ensure the import is placed before any use of json in contacts_tools.py
where functions like the update routine reference update_mask.

…ering

The Docs API suggestionsViewMode parameter handles suggestion filtering
server-side, making client-side parsing of suggestedInsertionIds /
suggestedDeletionIds unnecessary.

- render_elements simplified to plain text extraction (no mode param)
- SUGGESTIONS_VIEW_MODE map added to docs_text.py
- Both tools pass suggestionsViewMode to documents().get()
- Tests updated (14 passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
gdocs/docs_tools.py (1)

1329-1345: Reject simultaneous section_index and chunk_index to avoid ambiguous requests.

Current behavior silently prioritizes section_index when both are set. Returning a clear validation error makes the API contract explicit.

Proposed fix
     body_elements = doc_data.get("body", {}).get("content", [])
     doc_name = file_metadata["name"]

+    if section_index is not None and chunk_index is not None:
+        return "Provide either section_index or chunk_index, not both."
+
     # --- Section-based access ---
     if section_index is not None:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gdocs/docs_tools.py` around lines 1329 - 1345, Add input validation to reject
requests where both section_index and chunk_index are provided: at the start of
the function handling these options (the block that currently checks "if
section_index is not None:" and later "if chunk_index is not None:"), detect if
both section_index and chunk_index are not None and return a clear validation
error string (or raise a ValueError/Http error consistent with existing API
patterns) explaining that both cannot be set simultaneously; update any calling
comments and preserve existing behavior of extract_sections, render_elements and
subsequent section/chunk handling when only one is provided.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@gdocs/docs_tools.py`:
- Around line 1195-1201: The new tools list_doc_sections and get_doc_text are
defined but not registered in the docs tool tier registry, so add entries for
both to core/tool_tiers.yaml under the appropriate docs/tools tier; update the
registry to include the tool names (list_doc_sections, get_doc_text), their
service_type ("docs"), and any required metadata (scopes/visibility/tier)
consistent with existing tool entries so they become discoverable at runtime.
- Line 1278: Validate the chunk_size parameter before any chunk math: in the
function that takes chunk_size (the parameter named chunk_size) add a guard that
ensures isinstance(chunk_size, int) and chunk_size > 0 (or coerce to int and
then check) and raise a clear ValueError if invalid; this prevents division/ceil
operations later (the block that divides/uses chunk_size around the chunking
logic) from raising runtime errors or producing invalid behavior.
- Around line 1246-1252: The returned guidance assumes num_chunks > 0 and yields
an invalid range when char_len == 0; update the block that computes num_chunks
(using num_chunks = -(-char_len // chunk_size)) to handle the zero-length case:
if num_chunks == 0, return a message stating the document
(file_metadata["name"]) is empty and no chunks are available instead of
"chunk_index=0..-1"; otherwise keep the existing message that shows Total
characters, Recommended chunk_size and the valid range
"chunk_index=0..{num_chunks - 1}". Ensure you reference the same variables
(num_chunks, char_len, chunk_size, file_metadata) and the get_doc_text
chunk_index guidance in the two branches.

---

Nitpick comments:
In `@gdocs/docs_tools.py`:
- Around line 1329-1345: Add input validation to reject requests where both
section_index and chunk_index are provided: at the start of the function
handling these options (the block that currently checks "if section_index is not
None:" and later "if chunk_index is not None:"), detect if both section_index
and chunk_index are not None and return a clear validation error string (or
raise a ValueError/Http error consistent with existing API patterns) explaining
that both cannot be set simultaneously; update any calling comments and preserve
existing behavior of extract_sections, render_elements and subsequent
section/chunk handling when only one is provided.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cffbef6b-cc30-40e3-9730-e459c48d4a04

📥 Commits

Reviewing files that changed from the base of the PR and between b3ad45a and 475c5a7.

📒 Files selected for processing (3)
  • gdocs/docs_text.py
  • gdocs/docs_tools.py
  • tests/test_docs_text.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_docs_text.py

Comment thread gdocs/docs_tools.py
Comment on lines +1195 to +1201
@server.tool()
@handle_http_errors("list_doc_sections", is_read_only=True, service_type="docs")
@require_multiple_services([
{"service_type": "drive", "scopes": "drive_read", "param_name": "drive_service"},
{"service_type": "docs", "scopes": "docs_read", "param_name": "docs_service"}
])
async def list_doc_sections(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Register the new tools in core/tool_tiers.yaml or they remain undiscoverable.

list_doc_sections and get_doc_text are introduced here, but they are not present in the docs tool tier registry from core/tool_tiers.yaml (context snippet). This will prevent tier-based exposure at runtime.

Also applies to: 1264-1269

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gdocs/docs_tools.py` around lines 1195 - 1201, The new tools
list_doc_sections and get_doc_text are defined but not registered in the docs
tool tier registry, so add entries for both to core/tool_tiers.yaml under the
appropriate docs/tools tier; update the registry to include the tool names
(list_doc_sections, get_doc_text), their service_type ("docs"), and any required
metadata (scopes/visibility/tier) consistent with existing tool entries so they
become discoverable at runtime.

Comment thread gdocs/docs_tools.py
Comment on lines +1246 to +1252
num_chunks = -(-char_len // chunk_size) # ceiling division
return (
f'Document "{file_metadata["name"]}" has no headings.\n'
f"Total characters: {char_len}\n"
f"Recommended chunk_size: {chunk_size} ({num_chunks} chunks)\n"
f"Use get_doc_text with chunk_index=0..{num_chunks - 1}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle empty headingless documents explicitly in chunk guidance.

Line 1246 computes num_chunks = 0 for empty docs, but Line 1251 then returns chunk_index=0..-1, which is an invalid range.

Proposed fix
         char_len = len(full_text)
         chunk_size = 10000
         num_chunks = -(-char_len // chunk_size)  # ceiling division
+        if num_chunks == 0:
+            return (
+                f'Document "{file_metadata["name"]}" has no headings and no text content.\n'
+                "Use get_doc_text to confirm mode-specific output if needed."
+            )
         return (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gdocs/docs_tools.py` around lines 1246 - 1252, The returned guidance assumes
num_chunks > 0 and yields an invalid range when char_len == 0; update the block
that computes num_chunks (using num_chunks = -(-char_len // chunk_size)) to
handle the zero-length case: if num_chunks == 0, return a message stating the
document (file_metadata["name"]) is empty and no chunks are available instead of
"chunk_index=0..-1"; otherwise keep the existing message that shows Total
characters, Recommended chunk_size and the valid range
"chunk_index=0..{num_chunks - 1}". Ensure you reference the same variables
(num_chunks, char_len, chunk_size, file_metadata) and the get_doc_text
chunk_index guidance in the two branches.

Comment thread gdocs/docs_tools.py
mode: str,
section_index: int = None,
chunk_index: int = None,
chunk_size: int = 10000,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate chunk_size as a positive integer before chunk math.

Line 1347 divides by chunk_size with no guard; chunk_size <= 0 can fail with runtime errors or invalid chunk behavior.

Proposed fix
     if mode not in ("original", "accepted"):
         return 'Invalid mode. Use "original" or "accepted".'
+    if chunk_size <= 0:
+        return "Invalid chunk_size. Use a positive integer."

Also applies to: 1347-1358

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gdocs/docs_tools.py` at line 1278, Validate the chunk_size parameter before
any chunk math: in the function that takes chunk_size (the parameter named
chunk_size) add a guard that ensures isinstance(chunk_size, int) and chunk_size
> 0 (or coerce to int and then check) and raise a clear ValueError if invalid;
this prevents division/ceil operations later (the block that divides/uses
chunk_size around the chunking logic) from raising runtime errors or producing
invalid behavior.

@taylorwilsdon

Copy link
Copy Markdown
Owner

Something funky is going on with this branch you've got, it's way off the main. Will switch to draft until you're ready!

@taylorwilsdon
taylorwilsdon marked this pull request as draft March 28, 2026 21:31
@pgonzale60

Copy link
Copy Markdown
Author

Closing in favour of a clean rebase onto current upstream main.

@pgonzale60 pgonzale60 closed this Mar 29, 2026
@pgonzale60

Copy link
Copy Markdown
Author

Thank you for reviewing this. Sorry for the spam!
Updating to the latest version I find all the functionality I was looking for. No further PR.

All the best,
Pablo

@pgonzale60
pgonzale60 deleted the feature/doc-text-suggestions branch March 29, 2026 06:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants