Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
18c58db
feat: Add multimodal support for Office documents
takeruhukushima Nov 1, 2025
95e6e25
solve dependencies problem
takeruhukushima Nov 1, 2025
2f32c8a
fix bug
takeruhukushima Nov 1, 2025
f7cc49d
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Nov 1, 2025
e42e636
Merge remote-tracking branch 'origin/feature/new-doc-types'
takeruhukushima Nov 1, 2025
c801103
fix:pre-commit fail src/paperqa/readers.py
takeruhukushima Nov 2, 2025
5e49ca1
add .docx,.pptx,.xlsx in settings.py
takeruhukushima Nov 2, 2025
65c5097
refactor(chunks):consolidating the chunk code for office and pdf
takeruhukushima Nov 2, 2025
5ead779
edit README.md:add .docx, .xlsx, .pptx, and code files (e.g., .py, .t…
takeruhukushima Nov 2, 2025
a7c5e3a
refactor: Unify chunking algorithm name for PDF and office documents
takeruhukushima Nov 2, 2025
5fe86c1
feat: Add unstructured version to office document parsing metadata
takeruhukushima Nov 2, 2025
d4619bd
feat: Implement lazy import for unstructured in office document parsing
takeruhukushima Nov 2, 2025
0be33fe
feat: Add unit test for office document parsing
takeruhukushima Nov 2, 2025
0775523
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Nov 2, 2025
84c6bf2
feat: Update test_parse_office_doc for Gemini models and RAG query
takeruhukushima Nov 2, 2025
e218d91
add mailmap takerufukushima
takeruhukushima Nov 2, 2025
4933f16
fix pre-commit error
takeruhukushima Nov 2, 2025
77b95cc
Merge branch 'feature/new-doc-types' of https://github.com/takeruhuku…
takeruhukushima Nov 2, 2025
280c381
Fix: Address linting issues in test_paperqa.py
takeruhukushima Nov 2, 2025
d95ed9f
feat: Improve questions and assertions in test_parse_office_doc
takeruhukushima Nov 3, 2025
057c7f8
feat: Enhance office document parsing tests and assertions
takeruhukushima Nov 3, 2025
f4975fc
Minor tweaks to test_parse_office_doc
jamesbraza Nov 3, 2025
7bfad14
Updating assertions in other tests for this PR's changes
jamesbraza Nov 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ dev = [
"ipython>=8", # Pin to keep recent
"litellm>=1.71", # Lower pin for aiohttp transport adoption
"mypy>=1.8", # Pin for mutable-override
"paper-qa[docling,image,ldp,memory,pypdf-media,pymupdf,typing,zotero,local,qdrant]",
"paper-qa[docling,image,ldp,memory,pypdf-media,pymupdf,typing,zotero,local,qdrant,office]",
"prek",
"pydantic~=2.11", # Pin for start of model_fields deprecation
"pylint-pydantic",
Expand Down Expand Up @@ -110,6 +110,9 @@ zotero = [
"paper-qa-pymupdf",
"pyzotero",
]
office = [
"unstructured[docx,xlsx,pptx]",
]

[project.scripts]
pqa = "paperqa.agents:main"
Expand Down
54 changes: 53 additions & 1 deletion src/paperqa/readers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import tiktoken
from html2text import __version__ as html2text_version
from html2text import html2text
from unstructured.documents.elements import Image, Table
from unstructured.partition.auto import partition
Copy link
Collaborator

@jamesbraza jamesbraza Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take a look around the repo for other examples of lazy imports, let's just lazily import these in the first line of parse_office_doc:

try:
    from unstructured.documents.elements import Image, Table
    from unstructured.partition.auto import partition
except ImportError as exc:
    raise ImportError(
        "TODO some message mentioning to install `paper-qa[office]`"
    ) from exc


from paperqa.types import (
ChunkMetadata,
Expand Down Expand Up @@ -171,6 +173,53 @@ def parse_text(
)


def parse_office_doc(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make a unit test for this in test_paperqa.py? Feel free to use another LLM besides OpenAI (e.g. Anthropic, OpenRouter) for your testing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[success] 90.64% tests/test_paperqa.py::test_parse_office_doc[dummy.docx]: 1.5548s
[success] 6.33% tests/test_paperqa.py::test_parse_office_doc[dummy.xlsx]: 0.1086s
[success] 3.03% tests/test_paperqa.py::test_parse_office_doc[dummy.pptx]: 0.0520s

Results (5.19s):
3 passed

I'm not confident, but the test passed. I'll commit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And sorry for dummy .docx and .xlsx written in Japanese.

path: str | os.PathLike,
page_size_limit: int | None = None,
**kwargs,
) -> ParsedText:
"""Parse office documents (.docx, .xlsx, .pptx) using unstructured, extracting text and images."""

elements = partition(str(path), **kwargs)

content_dict = {}
media_list = []
current_text = ""
media_index = 0

for el in elements:
if isinstance(el, Image):
if el.metadata.image_data:
image_data = el.metadata.image_data
# Create a ParsedMedia object
parsed_media = ParsedMedia(
index=media_index,
data=image_data,
info={"suffix": el.metadata.image_mime_type},
)
media_list.append(parsed_media)
media_index += 1
elif isinstance(el, Table):
# For tables, we could get the HTML representation for better structure
current_text += el.metadata.text_as_html + "\n\n"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Null Text Concatenation Crash in HTML Rendering

Potential TypeError when el.metadata.text_as_html is None. The code concatenates el.metadata.text_as_html with a string without checking if it's None first. If the unstructured library returns None for text_as_html on certain Table elements, this will raise a TypeError: unsupported operand type(s) for +: 'NoneType' and 'str'.

Fix in Cursor Fix in Web

else:
current_text += str(el) + "\n\n"

# For office docs, we can treat the whole document as a single "page"
content_dict["1"] = (current_text, media_list)

return ParsedText(
content=content_dict,
metadata=ParsedMetadata(
parsing_libraries=["unstructured"],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you include the version of unstructured here? Take a look at how the Docling reader does it (at packages/paper-qa-docling)

paperqa_version=pqa_version,
total_parsed_text_length=len(current_text),
count_parsed_media=len(media_list),
name=f"office_doc|path={path}",
),
)


def chunk_text(
parsed_text: ParsedText,
doc: Doc,
Expand Down Expand Up @@ -276,7 +325,7 @@ def chunk_code_text(

IMAGE_EXTENSIONS = tuple({".png", ".jpg", ".jpeg"})
# When HTML reader supports images, add here
ENRICHMENT_EXTENSIONS = tuple({".pdf", *IMAGE_EXTENSIONS})
ENRICHMENT_EXTENSIONS = tuple({".pdf", ".docx", ".xlsx", ".pptx", *IMAGE_EXTENSIONS})


@overload
Expand Down Expand Up @@ -383,6 +432,9 @@ async def read_doc( # noqa: PLR0912
)
elif str_path.endswith(IMAGE_EXTENSIONS):
parsed_text = await parse_image(path, **parser_kwargs)
elif str_path.endswith((".docx", ".xlsx", ".pptx")):
# TODO: Make parse_office_doc async
parsed_text = await asyncio.to_thread(parse_office_doc, path, **parser_kwargs)
else:
parsed_text = await asyncio.to_thread(
parse_text, path, split_lines=True, **parser_kwargs
Expand Down
Loading
Loading