Skip to content

feat(docs): add get_doc_as_markdown tool with comment context - #490

Merged
taylorwilsdon merged 2 commits into
taylorwilsdon:mainfrom
MaxGhenis:feat/doc-markdown-export
Feb 19, 2026
Merged

feat(docs): add get_doc_as_markdown tool with comment context#490
taylorwilsdon merged 2 commits into
taylorwilsdon:mainfrom
MaxGhenis:feat/doc-markdown-export

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Feb 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a new get_doc_as_markdown tool that converts Google Docs to clean Markdown with optional inline comment annotations
  • Converts headings, bold/italic/strikethrough/code, links, ordered/unordered lists with nesting, and tables
  • Comments from Drive API v3 include quotedFileContent (anchor text) and can be rendered inline as footnotes or as an appendix
  • Registered as an "extended" tier tool under docs

Motivation

Google Docs content is currently only available as raw structured JSON or plain text. Markdown output is much more useful for LLM consumption, and inline comment context (with the highlighted anchor text) is critical for document review workflows.

Changes

  • gdocs/docs_markdown.py (new): Full converter from Docs API JSON to Markdown, plus comment parsing and formatting (inline footnotes with [^c1] style, appendix mode, Drive API comment parsing)
  • gdocs/docs_tools.py: New get_doc_as_markdown tool using @require_multiple_services for both Docs and Drive APIs
  • core/tool_tiers.yaml: Added get_doc_as_markdown to docs extended tier
  • tests/gdocs/test_docs_markdown.py (new): 18 tests covering text formatting, headings, tables, lists, empty docs, comment parsing, inline placement, and appendix formatting

Tool signature

get_doc_as_markdown(
    document_id: str,           # Doc ID or full URL
    include_comments: bool = True,
    comment_mode: str = "inline",   # "inline" | "appendix" | "none"
    include_resolved: bool = False,
)

Test plan

  • 18 new unit tests pass (pytest tests/gdocs/test_docs_markdown.py)
  • All 144 existing tests still pass
  • Manual test with real Google Docs (simple, complex with comments)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Export Google Docs to Markdown with styled text, headings, nested lists, tables, links, and code formatting.
    • Include comments inline or as a “Comments” appendix; option to exclude resolved comments.
    • Added a new public tool to perform the Docs→Markdown export.
  • Tests

    • Added comprehensive tests covering conversion, nested lists, tables, and comment parsing/formatting.

Adds a new `get_doc_as_markdown` tool that converts Google Docs to clean
Markdown preserving formatting (headings, bold/italic/strikethrough, links,
code spans, ordered/unordered lists with nesting, and tables).

Optionally overlays comments with their anchor text (quotedFileContent) —
the specific text each comment is attached to — in two modes:
- inline: footnote-style references placed at the anchor text location
- appendix: all comments grouped at the bottom with blockquoted anchors

This gives AI agents full document context in a single tool call, unlike
get_doc_content which strips all formatting to plain text.

New files:
- gdocs/docs_markdown.py: Converter + comment formatting logic
- tests/gdocs/test_docs_markdown.py: 18 tests

Tool tier: extended (alongside search_docs, export_doc_to_pdf, etc.)

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

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a new tool get_doc_as_markdown, a conversion module to render Google Docs JSON to Markdown (including headings, lists, tables, and comments), integrates the tool into docs_tools, registers it in tool tiers, and provides comprehensive unit tests for conversion and comment formatting.

Changes

Cohort / File(s) Summary
Configuration
core/tool_tiers.yaml
Added get_doc_as_markdown to the docs extended tier.
Markdown converter
gdocs/docs_markdown.py
New module implementing Google Docs JSON → Markdown conversion, comment parsing/formatting, and constants (MONO_FONTS, HEADING_MAP) plus public APIs: convert_doc_to_markdown, parse_drive_comments, format_comments_inline, format_comments_appendix.
Tool integration
gdocs/docs_tools.py
Added get_doc_as_markdown tool: extracts doc ID, fetches Docs API content, converts to Markdown, optionally paginates Drive comments, parses/filters comments, and formats them inline or as an appendix.
Tests
tests/gdocs/test_docs_markdown.py
New comprehensive tests covering plain/styled text, headings, lists, tables, empty docs, comment parsing (including resolved filtering) and both inline and appendix comment formatting.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Tool as get_doc_as_markdown (Tool)
    participant DocsAPI as Docs API
    participant DriveAPI as Drive API
    participant Converter as Markdown Converter

    Client->>Tool: Request(doc_id or URL, include_comments?, comment_mode)
    Tool->>DocsAPI: GET document JSON
    DocsAPI-->>Tool: Document body + metadata
    Tool->>Converter: convert_doc_to_markdown(document)
    Converter-->>Tool: Markdown content

    alt include_comments = true
        Tool->>DriveAPI: List comments (paginated)
        DriveAPI-->>Tool: Comments batches
        Tool->>Converter: parse_drive_comments(comments, include_resolved)
        Converter-->>Tool: Parsed comments
        alt comment_mode = "inline"
            Tool->>Converter: format_comments_inline(markdown, parsed_comments)
        else comment_mode = "appendix"
            Tool->>Converter: format_comments_appendix(parsed_comments)
        end
        Converter-->>Tool: Markdown + formatted comments
    end

    Tool-->>Client: Final Markdown
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibbled bytes beneath the moonlit lamp,

Turned Docs to Markdown with a joyful stamp.
Lists hop tidy, tables dance in rows,
Comments tucked like carrots where the footnote grows.
Hooray — a rabbit's patch where documentation glows!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is comprehensive and well-structured, covering motivation, changes, tool signature, and test plan. However, it does not follow the required repository template with sections like Type of Change, Testing checklist, and the maintainer edits acknowledgment. Restructure the description to follow the provided template, including Type of Change checkboxes, Testing section with test status, Checklist items, and the maintainer edits confirmation.
Docstring Coverage ⚠️ Warning Docstring coverage is 41.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main feature added: a new tool for converting Google Docs to Markdown with comment context support.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 1

🧹 Nitpick comments (4)
gdocs/docs_markdown.py (1)

189-197: Pipe characters in cell content are not escaped.

If a table cell contains a literal | character, it will break the Markdown table structure. Consider escaping pipe characters.

♻️ Proposed fix
 def _extract_cell_text(cell: dict[str, Any]) -> str:
     """Extract text from a table cell."""
     parts: list[str] = []
     for content_elem in cell.get("content", []):
         if "paragraph" in content_elem:
             text = _convert_paragraph_text(content_elem["paragraph"])
             if text.strip():
                 parts.append(text.strip())
-    return " ".join(parts)
+    cell_text = " ".join(parts)
+    return cell_text.replace("|", "\\|")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gdocs/docs_markdown.py` around lines 189 - 197, The _extract_cell_text
function currently returns raw cell text which can contain pipe characters that
break Markdown tables; update _extract_cell_text to escape any '|' characters
(replace '|' with '\|') in the text produced by _convert_paragraph_text before
appending/returning, ensuring table cells yield pipe-escaped strings; reference
the _extract_cell_text function and the call to _convert_paragraph_text so you
locate and modify the text sanitization step.
tests/gdocs/test_docs_markdown.py (3)

137-189: Good test structure with room for expanded coverage.

The tests effectively validate core functionality. Consider adding tests for:

  • Strikethrough formatting (~~text~~)
  • Code spans (monospace fonts → backticks)
  • Links
  • Combined formatting (bold + italic → ***text***)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/gdocs/test_docs_markdown.py` around lines 137 - 189, Add unit tests to
tests/gdocs/test_docs_markdown.py that cover strikethrough, inline code spans,
links, and combined bold+italic cases using the existing convert_doc_to_markdown
helper; specifically add methods (e.g., TestTextFormatting.test_strikethrough,
test_code_span, test_link, test_bold_italic_combination) that call
convert_doc_to_markdown with small document fixtures containing those elements
and assert the expected markdown outputs ("~~text~~", "`code`", "[label](url)",
and "***text***" respectively) to expand coverage of formatting edge cases.

101-131: Consider adding test fixtures for ordered lists and nested lists.

The current LIST_DOC fixture only covers flat unordered lists. Since the converter supports ordered lists and nesting, adding fixtures to test these scenarios would improve coverage.

📝 Example fixtures to add
ORDERED_LIST_DOC = {
    "title": "Ordered List Test",
    "lists": {
        "kix.list002": {
            "listProperties": {
                "nestingLevels": [
                    {"glyphType": "DECIMAL"},  # 1. 2. 3.
                ]
            }
        }
    },
    "body": {
        "content": [
            {"sectionBreak": {"sectionStyle": {}}},
            {
                "paragraph": {
                    "elements": [{"textRun": {"content": "First\n", "textStyle": {}}}],
                    "paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
                    "bullet": {"listId": "kix.list002", "nestingLevel": 0},
                }
            },
            {
                "paragraph": {
                    "elements": [{"textRun": {"content": "Second\n", "textStyle": {}}}],
                    "paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
                    "bullet": {"listId": "kix.list002", "nestingLevel": 0},
                }
            },
        ]
    },
}

# Then add test:
class TestLists:
    def test_unordered(self):
        md = convert_doc_to_markdown(LIST_DOC)
        assert "- Item one" in md
        assert "- Item two" in md

    def test_ordered(self):
        md = convert_doc_to_markdown(ORDERED_LIST_DOC)
        assert "1. First" in md
        assert "2. Second" in md
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/gdocs/test_docs_markdown.py` around lines 101 - 131, Add fixtures and
tests to cover ordered and nested lists: create an ORDERED_LIST_DOC and a
NESTED_LIST_DOC similar to LIST_DOC, setting listProperties.nestingLevels
glyphType to "DECIMAL" for ordered lists and adding multiple nestingLevels with
appropriate glyphType/glyphSymbol for nested bullets; then add test methods in
the TestLists class that call convert_doc_to_markdown(ORDERED_LIST_DOC) and
convert_doc_to_markdown(NESTED_LIST_DOC) and assert expected markdown like "1.
First", "2. Second" for ordered and correct nested indenting/bullets for
NESTED_LIST_DOC. Ensure you reference the same listId values (e.g.,
"kix.list002") in the paragraphs' bullet.listId fields so the converter resolves
list types and nesting correctly.

3-6: Consider using pytest's conftest.py or proper package structure instead of sys.path manipulation.

The sys.path.insert approach is fragile and may cause issues in different test execution contexts (IDE, CI, different working directories). A conftest.py with proper path setup or installing the package in editable mode (pip install -e .) would be more robust.

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

In `@tests/gdocs/test_docs_markdown.py` around lines 3 - 6, Remove the ad-hoc
sys.path manipulation (the sys.path.insert call using
os.path.abspath/os.path.join and __file__) from
tests/gdocs/test_docs_markdown.py and instead provide a robust import setup:
either add a tests/conftest.py that performs test-wide setup (or exposes the
project root via pytest hooks) so tests can import the package cleanly, or
install the package in editable mode (pip install -e .) in CI/dev environments;
update imports in test_docs_markdown.py to rely on the package namespace rather
than manipulating sys.path.
🤖 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 1620-1628: The function currently calls the Docs API using
document_id without extracting an ID if a full URL was passed and it doesn't
validate comment_mode; update the code that receives document_id to first
normalize it by extracting the Google Doc ID when a URL is provided (use a regex
to capture the /d/<id>/ or ?id=... patterns) and replace document_id with the
extracted ID before calling docs_service.documents().get(...).execute; then
validate comment_mode against the allowed values (e.g., "none", "appendix",
"inline") and raise a ValueError for any invalid value instead of silently
falling through (adjust handling where include_comments and comment_mode are
used). Use the existing document_id and comment_mode symbols to locate where to
insert the URL-to-ID extraction and the validation check.

---

Nitpick comments:
In `@gdocs/docs_markdown.py`:
- Around line 189-197: The _extract_cell_text function currently returns raw
cell text which can contain pipe characters that break Markdown tables; update
_extract_cell_text to escape any '|' characters (replace '|' with '\|') in the
text produced by _convert_paragraph_text before appending/returning, ensuring
table cells yield pipe-escaped strings; reference the _extract_cell_text
function and the call to _convert_paragraph_text so you locate and modify the
text sanitization step.

In `@tests/gdocs/test_docs_markdown.py`:
- Around line 137-189: Add unit tests to tests/gdocs/test_docs_markdown.py that
cover strikethrough, inline code spans, links, and combined bold+italic cases
using the existing convert_doc_to_markdown helper; specifically add methods
(e.g., TestTextFormatting.test_strikethrough, test_code_span, test_link,
test_bold_italic_combination) that call convert_doc_to_markdown with small
document fixtures containing those elements and assert the expected markdown
outputs ("~~text~~", "`code`", "[label](url)", and "***text***" respectively) to
expand coverage of formatting edge cases.
- Around line 101-131: Add fixtures and tests to cover ordered and nested lists:
create an ORDERED_LIST_DOC and a NESTED_LIST_DOC similar to LIST_DOC, setting
listProperties.nestingLevels glyphType to "DECIMAL" for ordered lists and adding
multiple nestingLevels with appropriate glyphType/glyphSymbol for nested
bullets; then add test methods in the TestLists class that call
convert_doc_to_markdown(ORDERED_LIST_DOC) and
convert_doc_to_markdown(NESTED_LIST_DOC) and assert expected markdown like "1.
First", "2. Second" for ordered and correct nested indenting/bullets for
NESTED_LIST_DOC. Ensure you reference the same listId values (e.g.,
"kix.list002") in the paragraphs' bullet.listId fields so the converter resolves
list types and nesting correctly.
- Around line 3-6: Remove the ad-hoc sys.path manipulation (the sys.path.insert
call using os.path.abspath/os.path.join and __file__) from
tests/gdocs/test_docs_markdown.py and instead provide a robust import setup:
either add a tests/conftest.py that performs test-wide setup (or exposes the
project root via pytest hooks) so tests can import the package cleanly, or
install the package in editable mode (pip install -e .) in CI/dev environments;
update imports in test_docs_markdown.py to rely on the package namespace rather
than manipulating sys.path.

Comment thread gdocs/docs_tools.py
…ping

Address CodeRabbit review feedback:
- Extract doc ID from full Google Docs URLs (e.g. /d/<id>/)
- Validate comment_mode against allowed values
- Escape pipe characters in table cell text

Co-Authored-By: Claude Opus 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.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@gdocs/docs_tools.py`:
- Around line 1617-1624: The review indicates the URL extraction (url_match on
document_id with regex /d/([\w-]+)) and comment_mode validation (valid_modes
tuple and check against comment_mode) are correct and this is a duplicate review
comment; no code changes are needed—remove the duplicate review/comment from the
PR thread or mark this comment as approved so only one approval remains and
avoid changing document_id, url_match, valid_modes, or comment_mode logic.

@taylorwilsdon taylorwilsdon self-assigned this Feb 19, 2026
@taylorwilsdon taylorwilsdon added the enhancement New feature or request label Feb 19, 2026
@taylorwilsdon

Copy link
Copy Markdown
Owner

Very nice, thanks! Appreciate the quick followup on the coderabbit comment as well.

@taylorwilsdon
taylorwilsdon merged commit 8b8d74c into taylorwilsdon:main Feb 19, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants