#2661·agentscope

[Bug]: WordParser splices a text box into the paragraph, and reads it twice when it has a VML fallback

Author: L4XBCreated Sep 16, 2026Updated Sep 16, 2026

Prerequisites

  • I have searched the existing issues and discussions, and this is not a duplicate.
  • This is a bug, not a usage question.

Background / Description

WordParser's _extract_text_from_paragraph starts by concatenating every w:t under the paragraph (rag/_parser/_word.py):

python
text = ""
for t_elem in para._element.findall(".//" + qn("w:t")):
    if t_elem.text:
        text += t_elem.text

.// is a descendant search, so it also reaches the w:t elements inside a text box, which live under w:txbxContent in the drawing inside a run. Two things follow, both silent.

1. The text box is spliced into the sentence with no separator. A paragraph reading "Paragraph text" that anchors a callout reading "Callout" comes back as Paragraph textCallout. textCallout is a word that appears in no document and is what gets embedded.

2. The text box is read twice. Word writes it twice inside mc:AlternateContent: a wps:txbx under mc:Choice, and the same text as a VML text box under mc:Fallback. Both hold a w:txbxContent, so the sweep above sees the callout twice. The function's later v:textbox pass would have been a third read, but it is unreachable — it only runs if not text, which is false for any paragraph that has text.

Error Messages

bash
No exception is raised; the text is corrupted in place.

Steps to Reproduce

  1. Code (the second template is the shape Word actually writes):
python
import asyncio
import io

import docx
from lxml import etree

from agentscope.rag._parser._word import WordParser

NS = " ".join(
    f'xmlns:{p}="{u}"'
    for p, u in (
        ("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main"),
        ("mc", "http://schemas.openxmlformats.org/markup-compatibility/2006"),
        ("wps", "http://schemas.microsoft.com/office/word/2010/wordprocessingShape"),
        ("a", "http://schemas.openxmlformats.org/drawingml/2006/main"),
        ("wp", "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"),
        ("v", "urn:schemas-microsoft-com:vml"),
    )
)

ALTERNATE = f"""
<w:p {NS}>
  <w:r><w:t>PARAGRAPH TEXT</w:t></w:r>
  <w:r><mc:AlternateContent>
    <mc:Choice Requires="wps"><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0">
      <wp:extent cx="2743200" cy="914400"/><wp:docPr id="1" name="Text Box 1"/>
      <a:graphic><a:graphicData uri="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
        <wps:wsp><wps:txbx><w:txbxContent>
          <w:p><w:r><w:t>CALLOUT</w:t></w:r></w:p>
        </w:txbxContent></wps:txbx></wps:wsp>
      </a:graphicData></a:graphic>
    </wp:inline></w:drawing></mc:Choice>
    <mc:Fallback><w:pict><v:shape id="_x0000_s1026" type="#_x0000_t202">
      <v:textbox><w:txbxContent>
        <w:p><w:r><w:t>CALLOUT</w:t></w:r></w:p>
      </w:txbxContent></v:textbox>
    </v:shape></w:pict></mc:Fallback>
  </mc:AlternateContent></w:r>
</w:p>
"""


def build(xml: str) -> bytes:
    document = docx.Document()
    body = document.element.body
    body.insert(len(body) - 1, etree.fromstring(xml.strip()))
    document.add_paragraph("AFTER")
    buffer = io.BytesIO()
    document.save(buffer)
    return buffer.getvalue()


async def main() -> None:
    sections = await WordParser().parse(build(ALTERNATE), "doc.docx")
    print(repr("\n".join(s.content.text for s in sections)))


asyncio.run(main())
  1. Run: python repro.py

  2. Observed:

'PARAGRAPH TEXTCALLOUTCALLOUT\nAFTER'

With a plain wps:txbx (no mc:AlternateContent) the same script prints 'PARAGRAPH TEXTCALLOUT\nAFTER' — one copy, still glued to the sentence.

Expected: 'PARAGRAPH TEXT\nCALLOUT\nAFTER' in both cases.

Environment

  • AgentScope Version: 2.0.8 (main @ 0b157a31)
  • Python Version: 3.11
  • OS: macOS 15 (python-docx is platform independent, so this is not OS specific)

Source: agentscope-ai/agentscope