diff --git a/docs/modules/indexes/document_loaders/examples/evernote.ipynb b/docs/modules/indexes/document_loaders/examples/evernote.ipynb index 2c994d3c..ff9f1477 100644 --- a/docs/modules/indexes/document_loaders/examples/evernote.ipynb +++ b/docs/modules/indexes/document_loaders/examples/evernote.ipynb @@ -9,39 +9,43 @@ "\n", ">[EverNote](https://evernote.com/) is intended for archiving and creating notes in which photos, audio and saved web content can be embedded. Notes are stored in virtual \"notebooks\" and can be tagged, annotated, edited, searched, and exported.\n", "\n", - "This notebook shows how to load `EverNote` file from disk." + "This notebook shows how to load an `Evernote` [export](https://help.evernote.com/hc/en-us/articles/209005557-Export-notes-and-notebooks-as-ENEX-or-HTML) file (.enex) from disk.\n", + "\n", + "A document will be created for each note in the export." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "id": "1a53ece0", "metadata": { "tags": [] }, "outputs": [], "source": [ - "#!pip install pypandoc\n", - "import pypandoc\n", - "\n", - "pypandoc.download_pandoc()" + "# lxml and html2text are required to parse EverNote notes\n", + "# !pip install lxml\n", + "# !pip install html2text" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "id": "88df766f", "metadata": { + "pycharm": { + "name": "#%%\n" + }, "tags": [] }, "outputs": [ { "data": { "text/plain": [ - "[Document(page_content='testing this\\n\\nwhat happens?\\n\\nto the world?\\n', metadata={'source': 'example_data/testing.enex'})]" + "[Document(page_content='testing this\\n\\nwhat happens?\\n\\nto the world?**Jan - March 2022**', metadata={'source': 'example_data/testing.enex'})]" ] }, - "execution_count": 4, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -49,9 +53,34 @@ "source": [ "from langchain.document_loaders import EverNoteLoader\n", "\n", + "# By default all notes are combined into a single Document\n", "loader = EverNoteLoader(\"example_data/testing.enex\")\n", "loader.load()" ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "97a58fde", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Document(page_content='testing this\\n\\nwhat happens?\\n\\nto the world?', metadata={'title': 'testing', 'created': time.struct_time(tm_year=2023, tm_mon=2, tm_mday=9, tm_hour=3, tm_min=47, tm_sec=46, tm_wday=3, tm_yday=40, tm_isdst=-1), 'updated': time.struct_time(tm_year=2023, tm_mon=2, tm_mday=9, tm_hour=3, tm_min=53, tm_sec=28, tm_wday=3, tm_yday=40, tm_isdst=-1), 'note-attributes.author': 'Harrison Chase', 'source': 'example_data/testing.enex'}),\n", + " Document(page_content='**Jan - March 2022**', metadata={'title': 'Summer Training Program', 'created': time.struct_time(tm_year=2022, tm_mon=12, tm_mday=27, tm_hour=1, tm_min=59, tm_sec=48, tm_wday=1, tm_yday=361, tm_isdst=-1), 'note-attributes.author': 'Mike McGarry', 'note-attributes.source': 'mobile.iphone', 'source': 'example_data/testing.enex'})]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# It's likely more useful to return a Document for each note\n", + "loader = EverNoteLoader(\"example_data/testing.enex\", load_single_document=False)\n", + "loader.load()" + ] } ], "metadata": { @@ -70,7 +99,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" + "version": "3.9.7" } }, "nbformat": 4, diff --git a/docs/modules/indexes/document_loaders/examples/example_data/testing.enex b/docs/modules/indexes/document_loaders/examples/example_data/testing.enex index edff3e7a..3faa399a 100644 --- a/docs/modules/indexes/document_loaders/examples/example_data/testing.enex +++ b/docs/modules/indexes/document_loaders/examples/example_data/testing.enex @@ -13,4 +13,16 @@
testing this
what happens?
to the world?
]]> + + Summer Training Program + 20221227T015948Z + + Mike McGarry + mobile.iphone + + + +
Jan - March 2022
]]> +
+
diff --git a/langchain/document_loaders/evernote.py b/langchain/document_loaders/evernote.py index a7529f37..be9a05ac 100644 --- a/langchain/document_loaders/evernote.py +++ b/langchain/document_loaders/evernote.py @@ -3,80 +3,146 @@ https://gist.github.com/foxmask/7b29c43a161e001ff04afdb2f181e31c """ import hashlib +import logging from base64 import b64decode from time import strptime -from typing import Any, Dict, List +from typing import Any, Dict, Iterator, List, Optional from langchain.docstore.document import Document from langchain.document_loaders.base import BaseLoader -def _parse_content(content: str) -> str: - from pypandoc import convert_text - - text = convert_text(content, "org", format="html") - return text - - -def _parse_resource(resource: list) -> dict: - rsc_dict: Dict[str, Any] = {} - for elem in resource: - if elem.tag == "data": - # Some times elem.text is None - rsc_dict[elem.tag] = b64decode(elem.text) if elem.text else b"" - rsc_dict["hash"] = hashlib.md5(rsc_dict[elem.tag]).hexdigest() - else: - rsc_dict[elem.tag] = elem.text - - return rsc_dict - - -def _parse_note(note: List) -> dict: - note_dict: Dict[str, Any] = {} - resources = [] - for elem in note: - if elem.tag == "content": - note_dict[elem.tag] = _parse_content(elem.text) - # A copy of original content - note_dict["content-raw"] = elem.text - elif elem.tag == "resource": - resources.append(_parse_resource(elem)) - elif elem.tag == "created" or elem.tag == "updated": - note_dict[elem.tag] = strptime(elem.text, "%Y%m%dT%H%M%SZ") - else: - note_dict[elem.tag] = elem.text - - note_dict["resource"] = resources - - return note_dict - - -def _parse_note_xml(xml_file: str) -> str: - """Parse Evernote xml.""" - # Without huge_tree set to True, parser may complain about huge text node - # Try to recover, because there may be " ", which will cause - # "XMLSyntaxError: Entity 'nbsp' not defined" - from lxml import etree - - context = etree.iterparse( - xml_file, encoding="utf-8", strip_cdata=False, huge_tree=True, recover=True - ) - result_string = "" - for action, elem in context: - if elem.tag == "note": - result_string += _parse_note(elem)["content"] - return result_string - - class EverNoteLoader(BaseLoader): - """Loader to load in EverNote files..""" - - def __init__(self, file_path: str): + """EverNote Loader. + Loads an EverNote notebook export file e.g. my_notebook.enex into Documents. + Instructions on producing this file can be found at + https://help.evernote.com/hc/en-us/articles/209005557-Export-notes-and-notebooks-as-ENEX-or-HTML + + Currently only the plain text in the note is extracted and stored as the contents + of the Document, any non content metadata (e.g. 'author', 'created', 'updated' etc. + but not 'content-raw' or 'resource') tags on the note will be extracted and stored + as metadata on the Document. + + Args: + file_path (str): The path to the notebook export with a .enex extension + load_single_document (bool): Whether or not to concatenate the content of all + notes into a single long Document. + If this is set to True (default) then the only metadata on the document will be + the 'source' which contains the file name of the export. + """ # noqa: E501 + + def __init__(self, file_path: str, load_single_document: bool = True): """Initialize with file path.""" self.file_path = file_path + self.load_single_document = load_single_document def load(self) -> List[Document]: - """Load document from EverNote file.""" - text = _parse_note_xml(self.file_path) - metadata = {"source": self.file_path} - return [Document(page_content=text, metadata=metadata)] + """Load documents from EverNote export file.""" + documents = [ + Document( + page_content=note["content"], + metadata={ + **{ + key: value + for key, value in note.items() + if key not in ["content", "content-raw", "resource"] + }, + **{"source": self.file_path}, + }, + ) + for note in self._parse_note_xml(self.file_path) + if note.get("content") is not None + ] + + if not self.load_single_document: + return documents + + return [ + Document( + page_content="".join([document.page_content for document in documents]), + metadata={"source": self.file_path}, + ) + ] + + @staticmethod + def _parse_content(content: str) -> str: + try: + import html2text + + return html2text.html2text(content).strip() + except ImportError as e: + logging.error( + "Could not import `html2text`. Although it is not a required package " + "to use Langchain, using the EverNote loader requires `html2text`. " + "Please install `html2text` via `pip install html2text` and try again." + ) + raise e + + @staticmethod + def _parse_resource(resource: list) -> dict: + rsc_dict: Dict[str, Any] = {} + for elem in resource: + if elem.tag == "data": + # Sometimes elem.text is None + rsc_dict[elem.tag] = b64decode(elem.text) if elem.text else b"" + rsc_dict["hash"] = hashlib.md5(rsc_dict[elem.tag]).hexdigest() + else: + rsc_dict[elem.tag] = elem.text + + return rsc_dict + + @staticmethod + def _parse_note(note: List, prefix: Optional[str] = None) -> dict: + note_dict: Dict[str, Any] = {} + resources = [] + + def add_prefix(element_tag: str) -> str: + if prefix is None: + return element_tag + return f"{prefix}.{element_tag}" + + for elem in note: + if elem.tag == "content": + note_dict[elem.tag] = EverNoteLoader._parse_content(elem.text) + # A copy of original content + note_dict["content-raw"] = elem.text + elif elem.tag == "resource": + resources.append(EverNoteLoader._parse_resource(elem)) + elif elem.tag == "created" or elem.tag == "updated": + note_dict[elem.tag] = strptime(elem.text, "%Y%m%dT%H%M%SZ") + elif elem.tag == "note-attributes": + additional_attributes = EverNoteLoader._parse_note( + elem, elem.tag + ) # Recursively enter the note-attributes tag + note_dict.update(additional_attributes) + else: + note_dict[elem.tag] = elem.text + + if len(resources) > 0: + note_dict["resource"] = resources + + return {add_prefix(key): value for key, value in note_dict.items()} + + @staticmethod + def _parse_note_xml(xml_file: str) -> Iterator[Dict[str, Any]]: + """Parse Evernote xml.""" + # Without huge_tree set to True, parser may complain about huge text node + # Try to recover, because there may be " ", which will cause + # "XMLSyntaxError: Entity 'nbsp' not defined" + try: + from lxml import etree + except ImportError as e: + logging.error( + "Could not import `lxml`. Although it is not a required package to use " + "Langchain, using the EverNote loader requires `lxml`. Please install " + "`lxml` via `pip install lxml` and try again." + ) + raise e + + context = etree.iterparse( + xml_file, encoding="utf-8", strip_cdata=False, huge_tree=True, recover=True + ) + + for action, elem in context: + if elem.tag == "note": + yield EverNoteLoader._parse_note(elem) diff --git a/poetry.lock b/poetry.lock index afef457d..98a6c21f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -10330,11 +10330,11 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ cffi = ["cffi (>=1.11)"] [extras] -all = ["O365", "aleph-alpha-client", "anthropic", "arxiv", "atlassian-python-api", "azure-cosmos", "azure-identity", "beautifulsoup4", "clickhouse-connect", "cohere", "deeplake", "docarray", "duckduckgo-search", "elasticsearch", "faiss-cpu", "google-api-python-client", "google-search-results", "gptcache", "gql", "hnswlib", "html2text", "huggingface_hub", "jina", "jinja2", "jq", "lancedb", "lark", "manifest-ml", "networkx", "nlpcloud", "nltk", "nomic", "openai", "opensearch-py", "pdfminer-six", "pexpect", "pgvector", "pinecone-client", "pinecone-text", "protobuf", "psycopg2-binary", "pyowm", "pypdf", "pytesseract", "pyvespa", "qdrant-client", "redis", "sentence-transformers", "spacy", "steamship", "tensorflow-text", "tiktoken", "torch", "transformers", "weaviate-client", "wikipedia", "wolframalpha"] +all = ["O365", "aleph-alpha-client", "anthropic", "arxiv", "atlassian-python-api", "azure-cosmos", "azure-identity", "beautifulsoup4", "clickhouse-connect", "cohere", "deeplake", "docarray", "duckduckgo-search", "elasticsearch", "faiss-cpu", "google-api-python-client", "google-search-results", "gptcache", "gql", "hnswlib", "html2text", "huggingface_hub", "jina", "jinja2", "jq", "lancedb", "lark", "lxml", "manifest-ml", "networkx", "nlpcloud", "nltk", "nomic", "openai", "opensearch-py", "pdfminer-six", "pexpect", "pgvector", "pinecone-client", "pinecone-text", "protobuf", "psycopg2-binary", "pyowm", "pypdf", "pytesseract", "pyvespa", "qdrant-client", "redis", "sentence-transformers", "spacy", "steamship", "tensorflow-text", "tiktoken", "torch", "transformers", "weaviate-client", "wikipedia", "wolframalpha"] azure = ["azure-core", "azure-cosmos", "azure-identity", "openai"] cohere = ["cohere"] embeddings = ["sentence-transformers"] -extended-testing = ["atlassian-python-api", "beautifulsoup4", "beautifulsoup4", "chardet", "jq", "lxml", "pandas", "pdfminer-six", "pymupdf", "pypdf", "pypdfium2", "telethon", "tqdm", "zep-python"] +extended-testing = ["atlassian-python-api", "beautifulsoup4", "beautifulsoup4", "chardet", "html2text", "jq", "lxml", "pandas", "pdfminer-six", "pymupdf", "pypdf", "pypdfium2", "telethon", "tqdm", "zep-python"] hnswlib = ["docarray", "hnswlib", "protobuf"] in-memory-store = ["docarray"] llms = ["anthropic", "cohere", "huggingface_hub", "manifest-ml", "nlpcloud", "openai", "torch", "transformers"] @@ -10345,4 +10345,4 @@ text-helpers = ["chardet"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "2b19b9deca7f83ca14af1f7bc7808bbe7873a91ce4c95381eaad8ea84fe04c0b" +content-hash = "cd116e8f127ccca1c6f700ef17863bae2f101384677448276fe0962dc3fc4cf6" diff --git a/pyproject.toml b/pyproject.toml index b1988200..918a4093 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -183,7 +183,8 @@ in_memory_store = ["docarray"] hnswlib = ["docarray", "protobuf", "hnswlib"] embeddings = ["sentence-transformers"] azure = ["azure-identity", "azure-cosmos", "openai", "azure-core"] -all = ["anthropic", "cohere", "openai", "nlpcloud", "huggingface_hub", "jina", "manifest-ml", "elasticsearch", "opensearch-py", "google-search-results", "faiss-cpu", "sentence-transformers", "transformers", "spacy", "nltk", "wikipedia", "beautifulsoup4", "tiktoken", "torch", "jinja2", "pinecone-client", "pinecone-text", "weaviate-client", "redis", "google-api-python-client", "wolframalpha", "qdrant-client", "tensorflow-text", "pypdf", "networkx", "nomic", "aleph-alpha-client", "deeplake", "pgvector", "psycopg2-binary", "pyowm", "pytesseract", "html2text", "atlassian-python-api", "gptcache", "duckduckgo-search", "arxiv", "azure-identity", "clickhouse-connect", "azure-cosmos", "lancedb", "lark", "pexpect", "pyvespa", "O365", "jq", "docarray", "protobuf", "hnswlib", "steamship", "pdfminer-six", "gql"] +all = ["anthropic", "cohere", "openai", "nlpcloud", "huggingface_hub", "jina", "manifest-ml", "elasticsearch", "opensearch-py", "google-search-results", "faiss-cpu", "sentence-transformers", "transformers", "spacy", "nltk", "wikipedia", "beautifulsoup4", "tiktoken", "torch", "jinja2", "pinecone-client", "pinecone-text", "weaviate-client", "redis", "google-api-python-client", "wolframalpha", "qdrant-client", "tensorflow-text", "pypdf", "networkx", "nomic", "aleph-alpha-client", "deeplake", "pgvector", "psycopg2-binary", "pyowm", "pytesseract", "html2text", "atlassian-python-api", "gptcache", "duckduckgo-search", "arxiv", "azure-identity", "clickhouse-connect", "azure-cosmos", "lancedb", "lark", "pexpect", "pyvespa", "O365", "jq", "docarray", "protobuf", "hnswlib", "steamship", "pdfminer-six", "gql", "lxml"] + # An extra used to be able to add extended testing. # Please use new-line on formatting to make it easier to add new packages without # merge-conflicts @@ -201,7 +202,8 @@ extended_testing = [ "beautifulsoup4", "pandas", "telethon", - "zep-python" + "zep-python", + "html2text" ] [tool.ruff] diff --git a/tests/unit_tests/document_loaders/sample_documents/__init__.py b/tests/unit_tests/document_loaders/sample_documents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/document_loaders/sample_documents/empty_export.enex b/tests/unit_tests/document_loaders/sample_documents/empty_export.enex new file mode 100644 index 00000000..ffae65c5 --- /dev/null +++ b/tests/unit_tests/document_loaders/sample_documents/empty_export.enex @@ -0,0 +1,4 @@ + + + + diff --git a/tests/unit_tests/document_loaders/sample_documents/sample_notebook.enex b/tests/unit_tests/document_loaders/sample_documents/sample_notebook.enex new file mode 100644 index 00000000..b3f32ea4 --- /dev/null +++ b/tests/unit_tests/document_loaders/sample_documents/sample_notebook.enex @@ -0,0 +1,28 @@ + + + + + Test + 20230511T011217Z + 20240714T011228Z + + Michael McGarry + + + +
abc
]]> +
+
+ + Summer Training Program + 20221227T015948Z + + Mike McGarry + mobile.iphone + + + +
Jan - March 2022
]]> +
+
+
\ No newline at end of file diff --git a/tests/unit_tests/document_loaders/sample_documents/sample_notebook_2.enex b/tests/unit_tests/document_loaders/sample_documents/sample_notebook_2.enex new file mode 100644 index 00000000..52418ae7 --- /dev/null +++ b/tests/unit_tests/document_loaders/sample_documents/sample_notebook_2.enex @@ -0,0 +1,16 @@ + + + + + Summer Training Program + 20221227T015948Z + mobile.iphone + + Mike McGarry + + + +
Jan - March 2022
]]> +
+
+
\ No newline at end of file diff --git a/tests/unit_tests/document_loaders/sample_documents/sample_notebook_emptynote.enex b/tests/unit_tests/document_loaders/sample_documents/sample_notebook_emptynote.enex new file mode 100644 index 00000000..603fdba4 --- /dev/null +++ b/tests/unit_tests/document_loaders/sample_documents/sample_notebook_emptynote.enex @@ -0,0 +1,14 @@ + + + + + Summer Training Program + 20221227T015948Z + mobile.iphone + + Mike McGarry + + + + + \ No newline at end of file diff --git a/tests/unit_tests/document_loaders/sample_documents/sample_notebook_missingcontenttag.enex b/tests/unit_tests/document_loaders/sample_documents/sample_notebook_missingcontenttag.enex new file mode 100644 index 00000000..71fc5343 --- /dev/null +++ b/tests/unit_tests/document_loaders/sample_documents/sample_notebook_missingcontenttag.enex @@ -0,0 +1,12 @@ + + + + + Summer Training Program + 20221227T015948Z + mobile.iphone + + Mike McGarry + + + \ No newline at end of file diff --git a/tests/unit_tests/document_loaders/sample_documents/sample_notebook_missingmetadata.enex b/tests/unit_tests/document_loaders/sample_documents/sample_notebook_missingmetadata.enex new file mode 100644 index 00000000..c56ef796 --- /dev/null +++ b/tests/unit_tests/document_loaders/sample_documents/sample_notebook_missingmetadata.enex @@ -0,0 +1,9 @@ + + + + + + I only have content, no metadata + + + \ No newline at end of file diff --git a/tests/unit_tests/document_loaders/sample_documents/sample_notebook_with_media.enex b/tests/unit_tests/document_loaders/sample_documents/sample_notebook_with_media.enex new file mode 100644 index 00000000..581e2914 --- /dev/null +++ b/tests/unit_tests/document_loaders/sample_documents/sample_notebook_with_media.enex @@ -0,0 +1,120 @@ + + + + + Tea Mug Design + 20180719T085818Z + 20230513T110142Z + + Michael McGarry + 43.777825 + 11.249122222222221 + mobile.iphone + + + +


When you pick this mug up with your thumb on top and middle finger through the loop, your ring finger slides into the mug under the loop where it is too hot to touch and burns you.

If you try and pick it up with your thumb and index finger you can’t hold the mug. 

]]> +
+ + +/9j/4AAQSkZJRgABAQEASABIAAD/4SGyRXhpZgAASUkqAAgAAAALAA8BAgAGAAAAkgAAABABAgAKAAAAmAAAABIBAwABAAAAAQAAABoBBQABAAAAogAAABsBBQABAAAAqgAAACgBAwABAAAAAgAAADEBAgANAAAAsgAAADIBAgAUAAAAwAAAABMCAwABAAAAAQAAAGmHBAABAAAA1AAAACWIBAABAAAAdgYAAKwHAABBcHBsZQBpUGhvbmUgNnMA +SAAAAAEAAABIAAAAAQAAAEdJTVAgMi4xMC4yMgAAMjAyMzowNToxOSAxMjoyMzo1NAAgAJqCBQABAAAAWgIAAJ2CBQABAAAAYgIAACKIAwABAAAAAgAAACeIAwABAAAAMgAAAACQBwAEAAAAMDIyMQOQAgAUAAAAagIAAASQAgAUAAAAfgIAAAGRBwAEAAAAAQIDAAGSCgABAAAAkgIAAAKSBQABAAAAmgIAAAOSCgABAAAAogIAAASSCgABAAAA +qgIAAAeSAwABAAAAAwAAAAmSAwABAAAAEAAAAAqSBQABAAAAsgIAABSSAwAEAAAAugIAAHySBwBqAwAAwgIAAJGSAgAEAAAANjU1AJKSAgAEAAAANjU1AACgBwAEAAAAMDEwMAGgAwABAAAAAQAAAAKgBAABAAAAwA8AAAOgBAABAAAA0AsAABeiAwABAAAAAgAAAAGjBwABAAAAAQAAAAKkAwABAAAAAAAAAAOkAwABAAAAAAAAAAWkAwABAAAA +HQAAAAakAwABAAAAAAAAADKkBQAEAAAALAYAADOkAgAGAAAATAYAADSkAgAjAAAAUgYAAAAAAAABAAAAIQAAAAsAAAAFAAAAMjAxODowNzoxOSAxMDo1NzoyOQAyMDE4OjA3OjE5IDEwOjU3OjI5APkIAADGAQAALx8AALUNAAB/BwAACQIAAAAAAAABAAAAUwAAABQAAAB2DAEH8QL0AkFwcGxlIGlPUwAAAU1NAA4AAQAJAAAAAQAAAAkAAgAH +AAACLgAAALwAAwAHAAAAaAAAAuoABAAJAAAAAQAAAAEABQAJAAAAAQAAAKcABgAJAAAAAQAAAKoABwAJAAAAAQAAAAEACAAKAAAAAwAAA1IACQAJAAAAAQAAERMADgAJAAAAAQAAAAAAFAAJAAAAAQAAAAQAFwAJAAAAAQAAAAAAGQAJAAAAAQAAAAAAHwAJAAAAAQAAAAAAAAAAYnBsaXN0MDBPEQIAcQFiAcIARAGZAL0A3ADnANEAygAdAaEB +6AGMAf4AxgGJAVgBHQEYAbYAhAFHAckArQC8AP0AvwBgAT8CWwFlAYABVgEvAd8BrQEMAeMA5gDyAPoA+ACeACoBCAFhAfsAbQEzARIBRAJxAbAAhwCKAJcAuQAiAZABrgCkAJsAwQBgAR4BkgCyANUAxQDLALsAvgC9AMcAGAETAfIAZABHAFABNAGUAJUAhACCAG0AaQBmAHEAsADaAHUArABwAFcANQEhAZkAjwCEAH0AbQBqAGcAcgC0AO8A +hQDMAGwAXgCZAJUAmgCLAIIAeABsAGkAZwBzALYA5QDcAJ4AggBnAIwAlQCaAIsAggB1AGwAaQBlAHUAugDHAKkAqgCsAKoAigCPAI4AhQCBAHYAbgBoAGYAdQC4AKEApgDDAMQAwgB9AH8AfABsAHAAcgBuAGYAZABsAIEAhwCXAMAA3ADYAHMAdABtAGIAPQBKAFQAUgBPAFUAXAB9AI4AsQDaAO0AcABwAGEAXwA/ACYAJgApAC8ATQBfAHYA +iwCmANUA7wBuAGkAXABTAF8AOAAzADoAUQBiAGkAdACGAKcAzQDhAG0AZABdAFUAbABNAFEAWgBiAG4AbwB0AIMAmgC1AMgAVwBZAFwAXgBtAHkAawBwAHUAgAB0AGIAUQBJAEsASwAACAAAAAAAAAIBAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAIMYnBsaXN0MDDUAQIDBAUGBwhVZmxhZ3NVdmFsdWVZdGltZXNjYWxlVWVwb2NoEAETAAEiu4VX +CRsSO5rKABAACBEXHSctLzg9AAAAAAAAAQEAAAAAAAAACQAAAAAAAAAAAAAAAAAAAD////RTAAAM5P///mMAApoe///z2QAAHNtTAAAAFAAAAFMAAAAUAAAACwAAAAUAAAALAAAABQAAAEFwcGxlAGlQaG9uZSA2cyBiYWNrIGNhbWVyYSA0LjE1bW0gZi8yLjIAAA8AAQACAAIAAABOAAAAAgAFAAMAAAAwBwAAAwACAAIAAABFAAAABAAFAAMA +AABIBwAABQABAAEAAAAAAAAABgAFAAEAAABgBwAABwAFAAMAAABoBwAADAACAAIAAABLAAAADQAFAAEAAACABwAAEAACAAIAAABUAAAAEQAFAAEAAACIBwAAFwACAAIAAABUAAAAGAAFAAEAAACQBwAAHQACAAsAAACYBwAAHwACAAcAAACkBwAAAAAAACsAAAABAAAALgAAAAEAAACxDwAAZAAAAAsAAAABAAAADgAAAAEAAAA0FgAAZAAAAES1 +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 +PWtFEXMf/9n/4Q9maHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA0LjQuMC1FeGl2MiI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3 +dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOmlwdGNFeHQ9Imh0dHA6Ly9pcHRjLm9yZy9zdGQvSXB0YzR4bXBFeHQvMjAwOC0wMi0yOS8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDov +L25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpwbHVzPSJodHRwOi8vbnMudXNlcGx1cy5vcmcvbGRmL3htcC8xLjAvIiB4bWxuczpHSU1QPSJodHRwOi8vd3d3LmdpbXAub3JnL3htcC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6eG1wPSJodHRwOi8v +bnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bXBNTTpEb2N1bWVudElEPSJnaW1wOmRvY2lkOmdpbXA6NmU2ZjA1YWYtMmIxNi00MzVhLWIyOTUtOWEyYWMzZDE1MzFmIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjgwNmY5ODJhLTk4M2YtNGVmYi04OWI0LTk1OTQ2ZGFjMDUzMiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlk +OjU5ZDY1NTllLTY3MjUtNDQwZC1iNGUyLWJlMjAzMzk3ZDdkYiIgR0lNUDpBUEk9IjIuMCIgR0lNUDpQbGF0Zm9ybT0iTWFjIE9TIiBHSU1QOlRpbWVTdGFtcD0iMTY4NDQ2MzA0MTM1OTkxNCIgR0lNUDpWZXJzaW9uPSIyLjEwLjIyIiBkYzpGb3JtYXQ9ImltYWdlL2pwZWciIHhtcDpDcmVhdG9yVG9vbD0iR0lNUCAyLjEwIj4gPGlwdGNF +eHQ6TG9jYXRpb25DcmVhdGVkPiA8cmRmOkJhZy8+IDwvaXB0Y0V4dDpMb2NhdGlvbkNyZWF0ZWQ+IDxpcHRjRXh0OkxvY2F0aW9uU2hvd24+IDxyZGY6QmFnLz4gPC9pcHRjRXh0OkxvY2F0aW9uU2hvd24+IDxpcHRjRXh0OkFydHdvcmtPck9iamVjdD4gPHJkZjpCYWcvPiA8L2lwdGNFeHQ6QXJ0d29ya09yT2JqZWN0PiA8aXB0Y0V4dDpS +ZWdpc3RyeUlkPiA8cmRmOkJhZy8+IDwvaXB0Y0V4dDpSZWdpc3RyeUlkPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6Y2hhbmdlZD0iLyIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDplODdhNDhhZi1mZmYxLTRlOGUtYTllYi1kOTFjM2IwNmFkNGQiIHN0RXZ0OnNvZnR3 +YXJlQWdlbnQ9IkdpbXAgMi4xMCAoTWFjIE9TKSIgc3RFdnQ6d2hlbj0iMjAyMy0wNS0xM1QyMTowMDozNysxMDowMCIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6Nzc1NmU4NTktY2M3ZS00OGE3LTlkZWUtOGU5NjcyNTkyM2YzIiBzdEV2dDpzb2Z0 +d2FyZUFnZW50PSJHaW1wIDIuMTAgKE1hYyBPUykiIHN0RXZ0OndoZW49IjIwMjMtMDUtMTlUMTI6MjQ6MDErMTA6MDAiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDxwbHVzOkltYWdlU3VwcGxpZXI+IDxyZGY6U2VxLz4gPC9wbHVzOkltYWdlU3VwcGxpZXI+IDxwbHVzOkltYWdlQ3JlYXRvcj4gPHJkZjpTZXEvPiA8L3BsdXM6 +SW1hZ2VDcmVhdG9yPiA8cGx1czpDb3B5cmlnaHRPd25lcj4gPHJkZjpTZXEvPiA8L3BsdXM6Q29weXJpZ2h0T3duZXI+IDxwbHVzOkxpY2Vuc29yPiA8cmRmOlNlcS8+IDwvcGx1czpMaWNlbnNvcj4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +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 + + image/jpeg + + IMG_4686v3.JPG + en-cache://tokenKey%3D%22AuthToken%3AUser%3A113979823%22+fff281d8-1e2b-214a-61f5-a152ca30a105+8ab763800efcb0865f5d55e8a0e43eb2+https://www.evernote.com/shard/s612/res/a5e074a6-0ed8-9df5-4843-a375d7c8d257 + + +
+
diff --git a/tests/unit_tests/document_loaders/test_evernote_loader.py b/tests/unit_tests/document_loaders/test_evernote_loader.py new file mode 100644 index 00000000..6d829010 --- /dev/null +++ b/tests/unit_tests/document_loaders/test_evernote_loader.py @@ -0,0 +1,176 @@ +import os +import pathlib +import time + +import pytest + +from langchain.document_loaders import EverNoteLoader + + +@pytest.mark.requires("lxml", "html2text") +class TestEverNoteLoader: + @staticmethod + def example_notebook_path(notebook_name: str) -> str: + current_dir = pathlib.Path(__file__).parent + return os.path.join(current_dir, "sample_documents", notebook_name) + + def test_loadnotebook_eachnoteisindividualdocument(self) -> None: + loader = EverNoteLoader( + self.example_notebook_path("sample_notebook.enex"), False + ) + documents = loader.load() + assert len(documents) == 2 + + def test_loadnotebook_eachnotehasexpectedcontentwithleadingandtrailingremoved( + self, + ) -> None: + documents = EverNoteLoader( + self.example_notebook_path("sample_notebook.enex"), False + ).load() + + content_note1 = documents[0].page_content + assert content_note1 == "abc" + + content_note2 = documents[1].page_content + assert content_note2 == "**Jan - March 2022**" + + def test_loademptynotebook_emptylistreturned(self) -> None: + documents = EverNoteLoader( + self.example_notebook_path("empty_export.enex"), False + ).load() + assert len(documents) == 0 + + def test_loadnotewithemptycontent_emptydocumentcontent(self) -> None: + documents = EverNoteLoader( + self.example_notebook_path("sample_notebook_emptynote.enex"), False + ).load() + note = documents[0] + assert note.page_content == "" + + def test_loadnotewithmissingcontenttag_emptylistreturned( + self, + ) -> None: + documents = EverNoteLoader( + self.example_notebook_path("sample_notebook_missingcontenttag.enex"), False + ).load() + assert len(documents) == 0 + + def test_loadnotewithnometadata_documentreturnedwithsourceonly( + self, + ) -> None: + documents = EverNoteLoader( + self.example_notebook_path("sample_notebook_missingmetadata.enex"), False + ).load() + note = documents[0] + + assert note.page_content == "I only have content, no metadata" + + assert len(note.metadata) == 1 + assert "source" in note.metadata + assert "sample_notebook_missingmetadata.enex" in note.metadata["source"] + + def test_loadnotebookwithimage_notehasplaintextonlywithresourcesremoved( + self, + ) -> None: + documents = EverNoteLoader( + self.example_notebook_path("sample_notebook_with_media.enex"), False + ).load() + + note = documents[0] + assert ( + note.page_content + == """\ +When you pick this mug up with your thumb on top and middle finger through the +loop, your ring finger slides into the mug under the loop where it is too hot +to touch and burns you. + + + +If you try and pick it up with your thumb and index finger you can’t hold the +mug.""" + ) + + def test_loadnotebook_eachnotehasexpectedmetadata(self) -> None: + documents = EverNoteLoader( + self.example_notebook_path("sample_notebook.enex"), False + ).load() + metadata_note1 = documents[0].metadata + + assert "title" in metadata_note1.keys() + assert "created" in metadata_note1.keys() + assert "updated" in metadata_note1.keys() + assert "note-attributes.author" in metadata_note1.keys() + assert ( + "content" not in metadata_note1.keys() + ) # This should be in the content of the document instead + assert ( + "content-raw" not in metadata_note1.keys() + ) # This is too large to be stored as metadata + assert ( + "resource" not in metadata_note1.keys() + ) # This is too large to be stored as metadata + + assert metadata_note1["title"] == "Test" + assert metadata_note1["note-attributes.author"] == "Michael McGarry" + + assert isinstance(metadata_note1["created"], time.struct_time) + assert isinstance(metadata_note1["updated"], time.struct_time) + + assert metadata_note1["created"].tm_year == 2023 + assert metadata_note1["created"].tm_mon == 5 + assert metadata_note1["created"].tm_mday == 11 + + assert metadata_note1["updated"].tm_year == 2024 + assert metadata_note1["updated"].tm_mon == 7 + assert metadata_note1["updated"].tm_mday == 14 + + metadata_note2 = documents[1].metadata + + assert "title" in metadata_note2.keys() + assert "created" in metadata_note2.keys() + assert "updated" not in metadata_note2.keys() + assert "note-attributes.author" in metadata_note2.keys() + assert "note-attributes.source" in metadata_note2.keys() + assert "content" not in metadata_note2.keys() + assert "content-raw" not in metadata_note2.keys() + assert ( + "resource" not in metadata_note2.keys() + ) # This is too large to be stored as metadata + + assert metadata_note2["title"] == "Summer Training Program" + assert metadata_note2["note-attributes.author"] == "Mike McGarry" + assert metadata_note2["note-attributes.source"] == "mobile.iphone" + + assert isinstance(metadata_note2["created"], time.struct_time) + + assert metadata_note2["created"].tm_year == 2022 + assert metadata_note2["created"].tm_mon == 12 + assert metadata_note2["created"].tm_mday == 27 + + def test_loadnotebookwithconflictingsourcemetadatatag_sourceoffilepreferred( + self, + ) -> None: + documents = EverNoteLoader( + self.example_notebook_path("sample_notebook_2.enex"), False + ).load() + assert "sample_notebook_2.enex" in documents[0].metadata["source"] + assert "mobile.iphone" not in documents[0].metadata["source"] + + def test_returnsingledocument_loadnotebook_eachnoteiscombinedinto1document( + self, + ) -> None: + loader = EverNoteLoader( + self.example_notebook_path("sample_notebook.enex"), True + ) + documents = loader.load() + assert len(documents) == 1 + + def test_returnsingledocument_loadnotebook_notecontentiscombinedinto1document( + self, + ) -> None: + loader = EverNoteLoader( + self.example_notebook_path("sample_notebook.enex"), True + ) + documents = loader.load() + note = documents[0] + assert note.page_content == "abc**Jan - March 2022**"