From 3637d6da6e51cd8b96a9a4f91eb71e8db21a8b20 Mon Sep 17 00:00:00 2001 From: Matt Robinson Date: Wed, 10 May 2023 04:37:17 -0400 Subject: [PATCH] feat: add loader for open office odt files (#4405) # ODF File Loader Adds a data loader for handling Open Office ODT files. Requires `unstructured>=0.6.3`. ### Testing The following should work using the `fake.odt` example doc from the [`unstructured` repo](https://github.com/Unstructured-IO/unstructured). ```python from langchain.document_loaders import UnstructuredODTLoader loader = UnstructuredODTLoader(file_path="fake.odt", mode="elements") loader.load() loader = UnstructuredODTLoader(file_path="fake.odt", mode="single") loader.load() ``` --- .../examples/example_data/fake.odt | Bin 0 -> 8950 bytes .../document_loaders/examples/odt.ipynb | 76 ++++++++++++++++++ langchain/document_loaders/__init__.py | 2 + langchain/document_loaders/odt.py | 22 +++++ langchain/document_loaders/unstructured.py | 9 +++ .../document_loaders/test_odt.py | 12 +++ tests/integration_tests/examples/fake.odt | Bin 0 -> 8950 bytes 7 files changed, 121 insertions(+) create mode 100644 docs/modules/indexes/document_loaders/examples/example_data/fake.odt create mode 100644 docs/modules/indexes/document_loaders/examples/odt.ipynb create mode 100644 langchain/document_loaders/odt.py create mode 100644 tests/integration_tests/document_loaders/test_odt.py create mode 100644 tests/integration_tests/examples/fake.odt diff --git a/docs/modules/indexes/document_loaders/examples/example_data/fake.odt b/docs/modules/indexes/document_loaders/examples/example_data/fake.odt new file mode 100644 index 0000000000000000000000000000000000000000..9050499723817a5d4514fe683bd29d7b8c73e3e5 GIT binary patch literal 8950 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 literal 0 HcmV?d00001 diff --git a/docs/modules/indexes/document_loaders/examples/odt.ipynb b/docs/modules/indexes/document_loaders/examples/odt.ipynb new file mode 100644 index 00000000..9bdee7aa --- /dev/null +++ b/docs/modules/indexes/document_loaders/examples/odt.ipynb @@ -0,0 +1,76 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "22a849cc", + "metadata": {}, + "source": [ + "## Unstructured ODT Loader\n", + "\n", + "The `UnstructuredODTLoader` can be used to load Open Office ODT files." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e6616e3a", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.document_loaders import UnstructuredODTLoader" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a654e4d9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Document(page_content='Lorem ipsum dolor sit amet.', metadata={'source': 'example_data/fake.odt', 'filename': 'example_data/fake.odt', 'category': 'Title'})" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "loader = UnstructuredODTLoader(\"example_data/fake.odt\", mode=\"elements\")\n", + "docs = loader.load()\n", + "docs[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ab94bde", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/langchain/document_loaders/__init__.py b/langchain/document_loaders/__init__.py index f2ff3d9c..d408add3 100644 --- a/langchain/document_loaders/__init__.py +++ b/langchain/document_loaders/__init__.py @@ -53,6 +53,7 @@ from langchain.document_loaders.notebook import NotebookLoader from langchain.document_loaders.notion import NotionDirectoryLoader from langchain.document_loaders.notiondb import NotionDBLoader from langchain.document_loaders.obsidian import ObsidianLoader +from langchain.document_loaders.odt import UnstructuredODTLoader from langchain.document_loaders.onedrive import OneDriveLoader from langchain.document_loaders.pdf import ( MathpixPDFLoader, @@ -190,6 +191,7 @@ __all__ = [ "UnstructuredHTMLLoader", "UnstructuredImageLoader", "UnstructuredMarkdownLoader", + "UnstructuredODTLoader", "UnstructuredPDFLoader", "UnstructuredPowerPointLoader", "UnstructuredRTFLoader", diff --git a/langchain/document_loaders/odt.py b/langchain/document_loaders/odt.py new file mode 100644 index 00000000..b8eedb31 --- /dev/null +++ b/langchain/document_loaders/odt.py @@ -0,0 +1,22 @@ +"""Loader that loads Open Office ODT files.""" +from typing import Any, List + +from langchain.document_loaders.unstructured import ( + UnstructuredFileLoader, + validate_unstructured_version, +) + + +class UnstructuredODTLoader(UnstructuredFileLoader): + """Loader that uses unstructured to load open office ODT files.""" + + def __init__( + self, file_path: str, mode: str = "single", **unstructured_kwargs: Any + ): + validate_unstructured_version(min_unstructured_version="0.6.3") + super().__init__(file_path=file_path, mode=mode, **unstructured_kwargs) + + def _get_elements(self) -> List: + from unstructured.partition.odt import partition_odt + + return partition_odt(filename=self.file_path, **self.unstructured_kwargs) diff --git a/langchain/document_loaders/unstructured.py b/langchain/document_loaders/unstructured.py index 83fccbf1..276c8551 100644 --- a/langchain/document_loaders/unstructured.py +++ b/langchain/document_loaders/unstructured.py @@ -23,6 +23,15 @@ def satisfies_min_unstructured_version(min_version: str) -> bool: return unstructured_version_tuple >= min_version_tuple +def validate_unstructured_version(min_unstructured_version: str) -> None: + """Raises an error if the unstructured version does not exceed the + specified minimum.""" + if not satisfies_min_unstructured_version(min_unstructured_version): + raise ValueError( + f"unstructured>={min_unstructured_version} is required in this loader." + ) + + class UnstructuredBaseLoader(BaseLoader, ABC): """Loader that uses unstructured to load files.""" diff --git a/tests/integration_tests/document_loaders/test_odt.py b/tests/integration_tests/document_loaders/test_odt.py new file mode 100644 index 00000000..0aa833ce --- /dev/null +++ b/tests/integration_tests/document_loaders/test_odt.py @@ -0,0 +1,12 @@ +from pathlib import Path + +from langchain.document_loaders import UnstructuredODTLoader + + +def test_unstructured_odt_loader() -> None: + """Test unstructured loader.""" + file_path = Path(__file__).parent.parent / "examples/fake.odt" + loader = UnstructuredODTLoader(str(file_path)) + docs = loader.load() + + assert len(docs) == 1 diff --git a/tests/integration_tests/examples/fake.odt b/tests/integration_tests/examples/fake.odt new file mode 100644 index 0000000000000000000000000000000000000000..9050499723817a5d4514fe683bd29d7b8c73e3e5 GIT binary patch literal 8950 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 literal 0 HcmV?d00001