Skip to content

fix: decode XML references in DOCX artifacts - #7088

Open
MrCapricornLiu wants to merge 1 commit into
google:mainfrom
MrCapricornLiu:fix-docx-character-references
Open

fix: decode XML references in DOCX artifacts#7088
MrCapricornLiu wants to merge 1 commit into
google:mainfrom
MrCapricornLiu:fix-docx-character-references

Conversation

@MrCapricornLiu

Copy link
Copy Markdown

Loading a DOCX containing Research & Development currently sends Research & Development to the model. Decode XML predefined entities and decimal/hex character references once after extracting text, preserving the bounded ZIP reader and leaving unknown or invalid references unchanged.

Reproduction and expected behavior

On ADK 2.8.0 / main (2284c581), Linux, Python 3.12.13, run the added regression cases in:

pytest tests/unittests/tools/test_load_artifacts_tool.py -q

The added cases fail on the original implementation and pass with this change. No LiteLLM or live model endpoint is required; Runner verification uses a scripted BaseLlm response.

Testing Plan

Type checking matches the unmodified upstream baseline (831 existing errors; no new errors).

  • Relevant unit tests on current main (da10185e): 52 passed.
  • The Runner reproduction passes on current main. A wheel built against 2284c581 was also verified in a clean environment.
  • Earlier full-suite validation on 2284c581, Python 3.10–3.14: tests passed with the two GC-introspection cases run in separate processes (14,251 passing cases on Python 3.12). One unrelated MCP stdio teardown case was deselected after it also failed to finish on the unmodified base. The default uninterrupted tox run did not complete.
Runner setup and reproduction output

Save the following as repro.py, then run python repro.py after installing the locally built wheel.

import asyncio
import json
from google.adk.agents import Agent
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.adk.runners import Runner
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.genai import types
from pydantic import BaseModel, Field

class ScriptedModel(BaseLlm):
    model: str = "local-repro"
    responses: list[types.Content]
    requests: list[LlmRequest] = Field(default_factory=list)

    async def generate_content_async(self, llm_request, stream=False):
        index = len(self.requests)
        self.requests.append(llm_request.model_copy(deep=True))
        yield LlmResponse(content=self.responses[index])

def response(*parts):
    return types.Content(role="model", parts=list(parts))

async def run_agent(model, tools=(), artifacts=None, setup=None):
    sessions = InMemorySessionService()
    session = await sessions.create_session(app_name="repro", user_id="user")
    if setup:
        await setup(session)
    async with Runner(app_name="repro", agent=Agent(name="repro_agent", model=model, tools=list(tools)), session_service=sessions, artifact_service=artifacts) as runner:
        events = [event async for event in runner.run_async(user_id="user", session_id=session.id, new_message=types.Content(role="user", parts=[types.Part(text="Run the check.")]))]
        stored = await sessions.get_session(app_name="repro", user_id="user", session_id=session.id)
    return events, stored

import io
import zipfile
from google.adk.tools.load_artifacts_tool import load_artifacts_tool

async def main():
    artifacts = InMemoryArtifactService()
    payload = io.BytesIO()
    with zipfile.ZipFile(payload, "w") as archive:
        archive.writestr("word/document.xml", '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>Research &amp; Development: &#20013;&#x6587;</w:t></w:r></w:p></w:body></w:document>')
    async def setup(session):
        await artifacts.save_artifact(app_name="repro", user_id="user", session_id=session.id, filename="document.docx", artifact=types.Part(inline_data=types.Blob(data=payload.getvalue(), mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document")))
    model = ScriptedModel(responses=[response(types.Part(function_call=types.FunctionCall(name="load_artifacts", args={"artifact_names": ["document.docx"]}))), response(types.Part(text="done"))])
    await run_agent(model, [load_artifacts_tool], artifacts, setup)
    texts = [p.text for c in model.requests[1].contents for p in c.parts or [] if p.text]
    assert any("Research & Development: 中文" in text for text in texts), texts
    assert all("&amp;" not in text and "&#20013;" not in text for text in texts), texts
    print("PASS: saved DOCX -> load_artifacts tool call -> decoded text in next model request:", [t for t in texts if "Research" in t])

asyncio.run(main())
PASS: saved DOCX -> load_artifacts tool call -> decoded text in next model request: ['Research & Development: 中文']

Signed-off-by: Chenghao Liu <chliu@stu.pku.edu.cn>
@google-cla

google-cla Bot commented Sep 11, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

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