feat(docs): add get_doc_as_markdown tool with comment context - #490
Conversation
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>
📝 WalkthroughWalkthroughAdds a new tool Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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_DOCfixture 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'sconftest.pyor proper package structure instead ofsys.pathmanipulation.The
sys.path.insertapproach is fragile and may cause issues in different test execution contexts (IDE, CI, different working directories). Aconftest.pywith 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.
…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>
There was a problem hiding this comment.
🤖 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.
|
Very nice, thanks! Appreciate the quick followup on the coderabbit comment as well. |
Summary
get_doc_as_markdowntool that converts Google Docs to clean Markdown with optional inline comment annotationsquotedFileContent(anchor text) and can be rendered inline as footnotes or as an appendixMotivation
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: Newget_doc_as_markdowntool using@require_multiple_servicesfor both Docs and Drive APIscore/tool_tiers.yaml: Addedget_doc_as_markdownto docs extended tiertests/gdocs/test_docs_markdown.py(new): 18 tests covering text formatting, headings, tables, lists, empty docs, comment parsing, inline placement, and appendix formattingTool signature
Test plan
pytest tests/gdocs/test_docs_markdown.py)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests