Skip to content

fix(read_extract): dedupe DOCX text-box text in _extract_docx - #67853

Open
JiataiWang wants to merge 2 commits into
NousResearch:mainfrom
JiataiWang:fix/docx-textbox-dedup
Open

fix(read_extract): dedupe DOCX text-box text in _extract_docx#67853
JiataiWang wants to merge 2 commits into
NousResearch:mainfrom
JiataiWang:fix/docx-textbox-dedup

Conversation

@JiataiWang

Copy link
Copy Markdown

Fixes #67851.

Problem

read_file on a .docx containing a text box (or any block-level content control) emitted the text-box contents twice.

Root cause

A Word text box stores its content as <w:p> paragraphs inside <w:txbxContent>, so paragraphs can nest. _extract_docx did:

for para in root.iter(f"{w}p"):        # recursive: also yields the NESTED <w:p>
    for node in para.iter():           # recursive: also descends INTO the nested <w:p>
        if node.tag == f"{w}t":
            ...

ElementTree.iter() walks the whole subtree, so the outer paragraph pulled the box's <w:t> in inline and the nested <w:p> was visited independently — emitting the same text twice.

Fix

ElementTree has no parent pointers, so build a child → parent map and attribute each text node to its nearest enclosing <w:p>. Text owned by a nested paragraph is skipped in the ancestor and emitted only when that paragraph is visited on its own. Each <w:p> now contributes exactly one line and each run of text appears once.

Documents without nested paragraphs are unaffected (the nearest <w:p> of every text node is its own paragraph, exactly as before).

Testing

  • Failing-first: the new test_text_box_not_duplicated fails on main (AssertionError: 2 != 1) and passes with this change.
  • Full file suite: python -m pytest tests/tools/test_read_extract.py19 passed.
  • Manually verified: plain paragraphs, tabs, and line breaks are byte-for-byte unchanged; a doubly-nested text box (box-in-box) extracts each fragment exactly once in document order.

🤖 Generated with Claude Code

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tools Tool registry, model_tools, toolsets tool/file File tools (read, write, patch, search) labels Jul 20, 2026

@teknium1 teknium1 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.

Thanks for the focused regression test and for tracing the duplicate to recursive paragraph traversal. The bug remains on current main at tools/read_extract.py:118-126.

Problems

  • The new ownership filter at tools/read_extract.py:146 skips BOXTEXT while the outer paragraph is emitted first (:138), then emits the nested paragraph later. For this PR's fixture (tests/tools/test_read_extract.py:170-174), that changes source order from Before → BOXTEXT → After to Before After → BOXTEXT.
  • tests/tools/test_read_extract.py:177-180 only checks uniqueness and presence, so it does not catch that reordering.

Suggested changes

  • Preserve the fixture's source/document order while eliminating the duplicate, and assert the resulting full output or relative fragment order in the regression test.

Automated hermes-sweeper review.

Comment thread tools/read_extract.py Outdated
Comment thread tests/tools/test_read_extract.py
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 30, 2026
JiataiWang and others added 2 commits August 2, 2026 18:33
A Word text box stores its content as <w:p> paragraphs inside
<w:txbxContent>, so paragraphs can nest. root.iter("w:p") yields those
nested paragraphs too, and the ancestor paragraph's iter() also descends
into them — so text-box text was extracted twice (once inline via the
ancestor, once when the nested paragraph was visited on its own).

Attribute each text node to its nearest enclosing <w:p> via a
child->parent map and skip text owned by a nested paragraph. Each <w:p>
now contributes exactly one line and each run of text appears once; no
change for documents without nested paragraphs.

Adds a failing-first regression test.

Fixes NousResearch#67851

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review feedback: the nearest-paragraph attribution emitted a text box's
text as its own line *after* the enclosing paragraph (Before After →
BOXTEXT), losing XML order. Replace it with a single document-order walk
that treats each <w:p> as a line boundary — a nested paragraph flushes
the line it interrupts, emits its own line(s), then the outer paragraph
resumes — so text stays in reading order (Before → BOXTEXT → After) and
is still emitted exactly once. Empty paragraphs remain blank lines.

Test now asserts relative order, not just uniqueness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@JiataiWang
JiataiWang force-pushed the fix/docx-textbox-dedup branch from 23e0195 to 5b5ade3 Compare August 3, 2026 01:39
@JiataiWang

Copy link
Copy Markdown
Author

Rebased onto current main (a4a91610) and resolved the test-file pruning conflict without restoring removed tests. The review feedback is now covered by a single document-order walk: nested text-box content remains in Before → BOXTEXT → After order, with a relative-order regression assertion. Canonical runner: scripts/run_tests.sh tests/tools/test_read_extract.py -q — 12/12 passed. The PR is mergeable again.

@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary

Two open PRs touch OOXML extraction, but only #67853 addresses issue #67851's nested-paragraph duplication: its document-order walk emits text-box content once without reordering it, whereas #61881 adds archive resource limits and leaves paragraph traversal unchanged.

Related pull requests

Suggested consolidation

Keep #67853 open with a salvage path: retain its focused document-order traversal and uniqueness/order regression test as the best existing fix for #67851 while routing it through normal backlog review. Keep #61881 separate for its archive-hardening scope; it is not a duplicate of #67853.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I67851(["issue #67851 (open)"])
    P67853["PR #67853 (open)"]
    P67853 -->|best fix| I67851
    class I67851 open
    class P67853 open
    class P67853 best
    class P67853 target
    click I67851 "https://github.com/NousResearch/hermes-agent/issues/67851"
    click P67853 "https://github.com/NousResearch/hermes-agent/pull/67853"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 2 pull requests and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 31 kB of PR diffs, 5 kB of issue/PR text, 5 kB of discussion (9 comments), 3 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/file File tools (read, write, patch, search) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: DOCX text-box (nested-paragraph) text extracted twice by read_file

4 participants