diff --git a/examples/sample_files/sample.md b/examples/sample_files/sample.md new file mode 100644 index 00000000..e02535b7 --- /dev/null +++ b/examples/sample_files/sample.md @@ -0,0 +1,2 @@ + +## A test document for markdown diff --git a/examples/sample_files/sample_docx.docx b/examples/sample_files/sample_docx.docx new file mode 100644 index 00000000..3a740ac9 Binary files /dev/null and b/examples/sample_files/sample_docx.docx differ diff --git a/examples/sample_files/sample_pdf_1.pdf b/examples/sample_files/sample_pdf_1.pdf new file mode 100644 index 00000000..87259b89 Binary files /dev/null and b/examples/sample_files/sample_pdf_1.pdf differ diff --git a/examples/sample_files/sample_pptx.pptx b/examples/sample_files/sample_pptx.pptx new file mode 100644 index 00000000..1f6e3791 Binary files /dev/null and b/examples/sample_files/sample_pptx.pptx differ diff --git a/examples/super_components.ipynb b/examples/super_components.ipynb new file mode 100644 index 00000000..135753ee --- /dev/null +++ b/examples/super_components.ipynb @@ -0,0 +1,497 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-11T09:03:33.257832Z", + "start_time": "2025-02-11T09:03:31.598703Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: python-docx in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (1.1.2)\r\n", + "Requirement already satisfied: trafilatura in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (2.0.0)\r\n", + "Requirement already satisfied: markdown-it-py in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (3.0.0)\r\n", + "Requirement already satisfied: mdit_plain in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (1.0.1)\r\n", + "Requirement already satisfied: pypdf in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (5.2.0)\r\n", + "Requirement already satisfied: python-pptx in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (1.0.2)\r\n", + "Requirement already satisfied: openpyxl in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (3.1.5)\r\n", + "Requirement already satisfied: nltk in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (3.9.1)\r\n", + "Requirement already satisfied: lxml>=3.1.0 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from python-docx) (5.3.0)\r\n", + "Requirement already satisfied: typing-extensions>=4.9.0 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from python-docx) (4.12.2)\r\n", + "Requirement already satisfied: certifi in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from trafilatura) (2024.12.14)\r\n", + "Requirement already satisfied: charset_normalizer>=3.4.0 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from trafilatura) (3.4.1)\r\n", + "Requirement already satisfied: courlan>=1.3.2 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from trafilatura) (1.3.2)\r\n", + "Requirement already satisfied: htmldate>=1.9.2 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from trafilatura) (1.9.3)\r\n", + "Requirement already satisfied: justext>=3.0.1 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from trafilatura) (3.0.1)\r\n", + "Requirement already satisfied: urllib3<3,>=1.26 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from trafilatura) (2.3.0)\r\n", + "Requirement already satisfied: mdurl~=0.1 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from markdown-it-py) (0.1.2)\r\n", + "Requirement already satisfied: Pillow>=3.3.2 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from python-pptx) (11.1.0)\r\n", + "Requirement already satisfied: XlsxWriter>=0.5.7 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from python-pptx) (3.2.2)\r\n", + "Requirement already satisfied: et-xmlfile in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from openpyxl) (2.0.0)\r\n", + "Requirement already satisfied: click in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from nltk) (8.1.8)\r\n", + "Requirement already satisfied: joblib in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from nltk) (1.4.2)\r\n", + "Requirement already satisfied: regex>=2021.8.3 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from nltk) (2024.11.6)\r\n", + "Requirement already satisfied: tqdm in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from nltk) (4.67.1)\r\n", + "Requirement already satisfied: babel>=2.16.0 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from courlan>=1.3.2->trafilatura) (2.16.0)\r\n", + "Requirement already satisfied: tld>=0.13 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from courlan>=1.3.2->trafilatura) (0.13)\r\n", + "Requirement already satisfied: dateparser>=1.1.2 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from htmldate>=1.9.2->trafilatura) (1.2.0)\r\n", + "Requirement already satisfied: python-dateutil>=2.9.0.post0 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from htmldate>=1.9.2->trafilatura) (2.9.0.post0)\r\n", + "Requirement already satisfied: pytz in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from dateparser>=1.1.2->htmldate>=1.9.2->trafilatura) (2024.2)\r\n", + "Requirement already satisfied: tzlocal in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from dateparser>=1.1.2->htmldate>=1.9.2->trafilatura) (5.2)\r\n", + "Requirement already satisfied: lxml-html-clean in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from lxml[html_clean]>=4.4.2->justext>=3.0.1->trafilatura) (0.4.1)\r\n", + "Requirement already satisfied: six>=1.5 in /Users/mathislucka/Library/Application Support/hatch/env/virtual/haystack-experimental/0WxCzKa9/test/lib/python3.13/site-packages (from python-dateutil>=2.9.0.post0->htmldate>=1.9.2->trafilatura) (1.17.0)\r\n" + ] + } + ], + "source": [ + "!pip install python-docx trafilatura markdown-it-py mdit_plain pypdf python-pptx openpyxl nltk" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-11T09:12:12.253083Z", + "start_time": "2025-02-11T09:12:12.250834Z" + } + }, + "outputs": [], + "source": [ + "from haystack import Pipeline\n", + "from haystack.document_stores.in_memory import InMemoryDocumentStore\n", + "\n", + "from haystack_experimental.super_components.converters import MultiFileConverter\n", + "from haystack_experimental.super_components.indexers import SentenceTransformersDocumentIndexer\n", + "from haystack_experimental.super_components.preprocessors import DocumentPreProcessor" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# SuperComponents\n", + "\n", + "Supercomponents in general behave like any other component. They have init params, optional from_dict() and to_dict() methods as usual. The init params typically determine how the internal pipeline is constructed (e.g. which components are used).\n", + "\n", + "Supercomponents can make it easier to build common pipeline patterns.\n", + "\n", + "Here, we are building an indexing pipeline for a RAG system.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-11T09:12:13.829578Z", + "start_time": "2025-02-11T09:12:13.822121Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\n", + "🚅 Components\n", + " - converter: MultiFileConverter\n", + " - preprocessor: DocumentPreProcessor\n", + " - indexer: DocumentIndexer\n", + "🛤️ Connections\n", + " - converter.documents -> preprocessor.documents (List[Document])\n", + " - preprocessor.documents -> indexer.documents (List[Document])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file_converter = MultiFileConverter()\n", + "preprocessor = DocumentPreProcessor()\n", + "document_store = InMemoryDocumentStore()\n", + "indexer = SentenceTransformersDocumentIndexer(document_store=document_store)\n", + "\n", + "pipe = Pipeline()\n", + "\n", + "pipe.add_component(\"converter\", file_converter)\n", + "pipe.add_component(\"preprocessor\", preprocessor)\n", + "pipe.add_component(\"indexer\", indexer)\n", + "\n", + "pipe.connect(\"converter.documents\", \"preprocessor.documents\")\n", + "pipe.connect(\"preprocessor.documents\", \"indexer.documents\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-11T09:12:15.424871Z", + "start_time": "2025-02-11T09:12:14.822152Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pipe.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our pipeline handles CSV, DOCX, HTML, JSON, Markdown, TXT, PDF, PPTX, and XLSX.\n", + "\n", + "Let's test it out!" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-11T09:12:20.299682Z", + "start_time": "2025-02-11T09:12:19.697516Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Converting markdown files to Documents: 100%|██████████| 1/1 [00:00<00:00, 234.69it/s]\n", + "Batches: 100%|██████████| 1/1 [00:00<00:00, 1.80it/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote 6 documents to the document store.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "example_files = [\n", + " \"sample_files/sample_docx.docx\",\n", + " \"sample_files/sample_pptx.pptx\",\n", + " \"sample_files/sample.md\",\n", + " \"sample_files/sample_pdf_1.pdf\",\n", + "]\n", + "\n", + "result = pipe.run({\"sources\": example_files})\n", + "\n", + "print(f\"Wrote {result['indexer']['documents_written']} documents to the document store.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's view the documents!\n", + "\n", + "You can see that they are split, and embedded." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-11T09:12:54.651275Z", + "start_time": "2025-02-11T09:12:54.645947Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[Document(id=575e058e31109793279bb32e9eca01b0ecef8a4a687a8eb235d77b11f1746956, content: 'Sample Docx File The US has \"passed the peak\" on new coronavirus cases, President Donald Trump said ...', meta: {'file_path': 'sample_docx.docx', 'docx': DOCXMetadata(author='Saha, Anirban', category='', comments='', content_status='', created='2020-07-14T08:14:00+00:00', identifier='', keywords='', language='', last_modified_by='Saha, Anirban', last_printed=None, modified='2020-07-14T08:16:00+00:00', revision=1, subject='', title='', version=''), 'source_id': '86ba10e1b023f2dfdd576079468a060522f31be54cf6e41515d2311522002b06', 'page_number': 1, 'split_id': 0, 'split_idx_start': 0}, embedding: vector of size 768),\n", + " Document(id=afce88a93fde936bd049139ff5802dcbe4e1af65b9040e88523ee868b50aec16, content: 'type: intro\n", + " date: 1.1.2023 pip install farm-haystack What to build with Haystack Ask questions in na...', meta: {'file_path': 'sample.md', 'source_id': '9a881cceb8042a3db81c9390bf8c7ba28979030823bf0382788430bf6ee73af6', 'page_number': 1, 'split_id': 0, 'split_idx_start': 0}, embedding: vector of size 768),\n", + " Document(id=a5bdfffc7637bc6156e319f760655196093577d8478f98f78d4993b4a338e12e, content: 'production & improve your models continuously | | |\n", + " |-|-|\n", + " | :ledger: Docs | Usage, Guides, API docum...', meta: {'file_path': 'sample.md', 'source_id': '9a881cceb8042a3db81c9390bf8c7ba28979030823bf0382788430bf6ee73af6', 'page_number': 1, 'split_id': 1, 'split_idx_start': 1495}, embedding: vector of size 768),\n", + " Document(id=a3b622a10f684a51dd5edb79557eada73bc5ae29e64f94b99284b964d943cb21, content: 'A sample PDF le History and standardization\n", + " Format (PDF) Adobe Systems made the PDF specication av...', meta: {'file_path': 'sample_pdf_1.pdf', 'source_id': 'e7779175a0a5a7841df3f6153651ea94c895e8f31d65b2c269bf17cb6bea39a7', 'page_number': 1, 'split_id': 0, 'split_idx_start': 0}, embedding: vector of size 768),\n", + " Document(id=7c3d052b5d6514d9c23d920c9cbfaa549be434cf755f4a029cb45d4508eded14, content: 'Many of them are also not supported by popular third-party implementations of PDF.\n", + " Column 1 Column 2...', meta: {'file_path': 'sample_pdf_1.pdf', 'source_id': 'e7779175a0a5a7841df3f6153651ea94c895e8f31d65b2c269bf17cb6bea39a7', 'page_number': 1, 'split_id': 1, 'split_idx_start': 1317}, embedding: vector of size 768),\n", + " Document(id=3de510d8a1b6e3d8fc9dc1127b6b358caf2f5a2ac962de3a267fd14a9e86c1bc, content: 'Sample Title Slide\n", + " Jane Doe\n", + " Title of First Slide\n", + " This is a bullet point\n", + " This is another bullet point...', meta: {'file_path': 'sample_pptx.pptx', 'source_id': '163edc48d112b11d55c6b809fb4f632e0d1f9a73f44f3343b40fdd9317e2ea9a', 'page_number': 1, 'split_id': 0, 'split_idx_start': 0}, embedding: vector of size 768)]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "document_store.filter_documents()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Expanding SuperComponents\n", + "What makes SuperComponents special is the ability to expand it by calling their `to_super_component_dict()` method. This converts the component to a generic `SuperComponent` that contains the pipeline constructed by the SuperComponent. From there on the pipeline can be changed in any way.\n", + "\n", + "Let's use this feature to check how this pipeline would look like without using super components." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-11T09:23:10.530152Z", + "start_time": "2025-02-11T09:23:10.521099Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\n", + "🚅 Components\n", + " - router: FileTypeRouter\n", + " - docx: DOCXToDocument\n", + " - html: HTMLToDocument\n", + " - json: JSONConverter\n", + " - md: MarkdownToDocument\n", + " - txt: TextFileToDocument\n", + " - pdf: PyPDFToDocument\n", + " - pptx: PPTXToDocument\n", + " - xlsx: XLSXToDocument\n", + " - joiner: DocumentJoiner\n", + " - csv: CSVToDocument\n", + "🛤️ Connections\n", + " - router.text/csv -> csv.sources (List[Union[str, Path, ByteStream]])\n", + " - router.application/vnd.openxmlformats-officedocument.wordprocessingml.document -> docx.sources (List[Union[str, Path, ByteStream]])\n", + " - router.text/html -> html.sources (List[Union[str, Path, ByteStream]])\n", + " - router.application/json -> json.sources (List[Union[str, Path, ByteStream]])\n", + " - router.text/markdown -> md.sources (List[Union[str, Path, ByteStream]])\n", + " - router.text/plain -> txt.sources (List[Union[str, Path, ByteStream]])\n", + " - router.application/pdf -> pdf.sources (List[Union[str, Path, ByteStream]])\n", + " - router.application/vnd.openxmlformats-officedocument.presentationml.presentation -> pptx.sources (List[Union[str, Path, ByteStream]])\n", + " - router.application/vnd.openxmlformats-officedocument.spreadsheetml.sheet -> xlsx.sources (List[Union[str, Path, ByteStream]])\n", + " - docx.documents -> joiner.documents (List[Document])\n", + " - html.documents -> joiner.documents (List[Document])\n", + " - json.documents -> joiner.documents (List[Document])\n", + " - md.documents -> joiner.documents (List[Document])\n", + " - txt.documents -> joiner.documents (List[Document])\n", + " - pdf.documents -> joiner.documents (List[Document])\n", + " - pptx.documents -> joiner.documents (List[Document])\n", + " - xlsx.documents -> joiner.documents (List[Document])\n", + " - csv.documents -> joiner.documents (List[Document])" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file_converter_serialized = file_converter._to_super_component_dict()\n", + "Pipeline.from_dict(file_converter_serialized[\"init_parameters\"][\"pipeline\"])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That's a lot of components just for file conversion.\n", + "\n", + "\n", + "The full pipeline, that has the same functionality would look like this. 😵\t" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "ExecuteTime": { + "end_time": "2025-02-11T09:40:41.120319Z", + "start_time": "2025-02-11T09:40:41.102739Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\n", + "🚅 Components\n", + " - file_classifier: FileTypeRouter\n", + " - text_converter: TextFileToDocument\n", + " - pdf_converter: PyPDFToDocument\n", + " - markdown_converter: TextFileToDocument\n", + " - html_converter: HTMLToDocument\n", + " - docx_converter: DOCXToDocument\n", + " - pptx_converter: PPTXToDocument\n", + " - xlsx_converter: XLSXToDocument\n", + " - csv_converter: CSVToDocument\n", + " - joiner: DocumentJoiner\n", + " - joiner_xlsx: DocumentJoiner\n", + " - splitter: DocumentSplitter\n", + " - cleaner: DocumentCleaner\n", + " - document_embedder: SentenceTransformersDocumentEmbedder\n", + " - writer: DocumentWriter\n", + "🛤️ Connections\n", + " - file_classifier.text/plain -> text_converter.sources (List[Union[str, Path, ByteStream]])\n", + " - file_classifier.application/pdf -> pdf_converter.sources (List[Union[str, Path, ByteStream]])\n", + " - file_classifier.text/markdown -> markdown_converter.sources (List[Union[str, Path, ByteStream]])\n", + " - file_classifier.text/html -> html_converter.sources (List[Union[str, Path, ByteStream]])\n", + " - file_classifier.application/vnd.openxmlformats-officedocument.wordprocessingml.document -> docx_converter.sources (List[Union[str, Path, ByteStream]])\n", + " - file_classifier.application/vnd.openxmlformats-officedocument.presentationml.presentation -> pptx_converter.sources (List[Union[str, Path, ByteStream]])\n", + " - file_classifier.application/vnd.openxmlformats-officedocument.spreadsheetml.sheet -> xlsx_converter.sources (List[Union[str, Path, ByteStream]])\n", + " - file_classifier.text/csv -> csv_converter.sources (List[Union[str, Path, ByteStream]])\n", + " - text_converter.documents -> joiner.documents (List[Document])\n", + " - pdf_converter.documents -> joiner.documents (List[Document])\n", + " - markdown_converter.documents -> joiner.documents (List[Document])\n", + " - html_converter.documents -> joiner.documents (List[Document])\n", + " - docx_converter.documents -> joiner.documents (List[Document])\n", + " - pptx_converter.documents -> joiner.documents (List[Document])\n", + " - xlsx_converter.documents -> joiner.documents (List[Document])\n", + " - csv_converter.documents -> joiner.documents (List[Document])\n", + " - joiner.documents -> cleaner.documents (List[Document])\n", + " - cleaner.documents -> splitter.documents (List[Document])\n", + " - document_embedder.documents -> writer.documents (List[Document])" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from haystack import Pipeline\n", + "from haystack.components.converters.csv import CSVToDocument\n", + "from haystack.components.converters.docx import DOCXToDocument\n", + "from haystack.components.converters.html import HTMLToDocument\n", + "from haystack.components.converters.pptx import PPTXToDocument\n", + "from haystack.components.converters.pypdf import PyPDFToDocument\n", + "from haystack.components.converters.txt import TextFileToDocument\n", + "from haystack.components.converters.xlsx import XLSXToDocument\n", + "from haystack.components.embedders.sentence_transformers_document_embedder import SentenceTransformersDocumentEmbedder\n", + "from haystack.components.joiners.document_joiner import DocumentJoiner\n", + "from haystack.components.preprocessors.document_cleaner import DocumentCleaner\n", + "from haystack.components.preprocessors.document_splitter import DocumentSplitter\n", + "from haystack.components.routers.file_type_router import FileTypeRouter\n", + "from haystack.components.writers.document_writer import DocumentWriter\n", + "from haystack.document_stores.in_memory import InMemoryDocumentStore\n", + "\n", + "file_classifier = FileTypeRouter(\n", + " mime_types=[\n", + " \"text/plain\",\n", + " \"application/pdf\",\n", + " \"text/markdown\",\n", + " \"text/html\",\n", + " \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n", + " \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n", + " \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n", + " \"text/csv\"\n", + " ])\n", + "text_converter = TextFileToDocument(encoding=\"utf-8\")\n", + "pdf_converter = PyPDFToDocument()\n", + "markdown_converter = TextFileToDocument(encoding=\"utf-8\")\n", + "html_converter = HTMLToDocument()\n", + "docx_converter = DOCXToDocument()\n", + "pptx_converter = PPTXToDocument()\n", + "xlsx_converter = XLSXToDocument()\n", + "csv_converter = CSVToDocument(encoding=\"utf-8\")\n", + "joiner = DocumentJoiner(join_mode=\"concatenate\", sort_by_score=False)\n", + "joiner_xlsx = DocumentJoiner(join_mode=\"concatenate\", sort_by_score=False)\n", + "splitter = DocumentSplitter(split_by=\"word\", split_length=250)\n", + "cleaner = DocumentCleaner()\n", + "document_embedder = SentenceTransformersDocumentEmbedder()\n", + "document_store = InMemoryDocumentStore()\n", + "writer = DocumentWriter(document_store=document_store)\n", + "\n", + "pipeline = Pipeline()\n", + "pipeline.add_component(\"file_classifier\", file_classifier)\n", + "pipeline.add_component(\"text_converter\", text_converter)\n", + "pipeline.add_component(\"pdf_converter\", pdf_converter)\n", + "pipeline.add_component(\"markdown_converter\", markdown_converter)\n", + "pipeline.add_component(\"html_converter\", html_converter)\n", + "pipeline.add_component(\"docx_converter\", docx_converter)\n", + "pipeline.add_component(\"pptx_converter\", pptx_converter)\n", + "pipeline.add_component(\"xlsx_converter\", xlsx_converter)\n", + "pipeline.add_component(\"csv_converter\", csv_converter)\n", + "pipeline.add_component(\"joiner\", joiner)\n", + "pipeline.add_component(\"joiner_xlsx\", joiner_xlsx)\n", + "pipeline.add_component(\"splitter\", splitter)\n", + "pipeline.add_component(\"cleaner\", cleaner)\n", + "pipeline.add_component(\"document_embedder\", document_embedder)\n", + "pipeline.add_component(\"writer\", writer)\n", + "pipeline.connect(\"file_classifier.text/plain\", \"text_converter.sources\")\n", + "pipeline.connect(\"file_classifier.application/pdf\", \"pdf_converter.sources\")\n", + "pipeline.connect(\"file_classifier.text/markdown\", \"markdown_converter.sources\")\n", + "pipeline.connect(\"file_classifier.text/html\", \"html_converter.sources\")\n", + "pipeline.connect(\"file_classifier.application/vnd.openxmlformats-officedocument.wordprocessingml.document\", \"docx_converter.sources\")\n", + "pipeline.connect(\"file_classifier.application/vnd.openxmlformats-officedocument.presentationml.presentation\", \"pptx_converter.sources\")\n", + "pipeline.connect(\"file_classifier.application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\", \"xlsx_converter.sources\")\n", + "pipeline.connect(\"file_classifier.text/csv\", \"csv_converter.sources\")\n", + "pipeline.connect(\"text_converter.documents\", \"joiner.documents\")\n", + "pipeline.connect(\"pdf_converter.documents\", \"joiner.documents\")\n", + "pipeline.connect(\"markdown_converter.documents\", \"joiner.documents\")\n", + "pipeline.connect(\"html_converter.documents\", \"joiner.documents\")\n", + "pipeline.connect(\"docx_converter.documents\", \"joiner.documents\")\n", + "pipeline.connect(\"pptx_converter.documents\", \"joiner.documents\")\n", + "pipeline.connect(\"joiner.documents\", \"cleaner.documents\")\n", + "pipeline.connect(\"cleaner.documents\", \"splitter.documents\")\n", + "pipeline.connect(\"xlsx_converter.documents\", \"joiner.documents\")\n", + "pipeline.connect(\"csv_converter.documents\", \"joiner.documents\")\n", + "pipeline.connect(\"document_embedder.documents\", \"writer.documents\")\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.13.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/haystack_experimental/super_components/__init__.py b/haystack_experimental/super_components/__init__.py new file mode 100644 index 00000000..c1764a6e --- /dev/null +++ b/haystack_experimental/super_components/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/haystack_experimental/super_components/converters/__init__.py b/haystack_experimental/super_components/converters/__init__.py new file mode 100644 index 00000000..7ff8a4a1 --- /dev/null +++ b/haystack_experimental/super_components/converters/__init__.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from haystack_experimental.super_components.converters.multi_file_converter import MultiFileConverter + +_all_ = ["MultiFileConverter"] diff --git a/haystack_experimental/super_components/converters/multi_file_converter.py b/haystack_experimental/super_components/converters/multi_file_converter.py new file mode 100644 index 00000000..9faf1155 --- /dev/null +++ b/haystack_experimental/super_components/converters/multi_file_converter.py @@ -0,0 +1,175 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from enum import Enum +from typing import Any, Dict + +from haystack import Pipeline, component, default_from_dict, default_to_dict +from haystack.components.converters import ( + CSVToDocument, + DOCXToDocument, + HTMLToDocument, + JSONConverter, + MarkdownToDocument, + PPTXToDocument, + PyPDFToDocument, + TextFileToDocument, + XLSXToDocument, +) +from haystack.components.joiners import DocumentJoiner +from haystack.components.routers import FileTypeRouter + +from haystack_experimental.core.super_component import SuperComponent + + +class ConverterMimeType(str, Enum): + CSV = "text/csv" + DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + HTML = "text/html" + JSON = "application/json" + MD = "text/markdown" + TEXT = "text/plain" + PDF = "application/pdf" + PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation" + XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + + +@component +class MultiFileConverter(SuperComponent): + """ + A file converter that handles conversion of multiple file types. + + The MultiFileConverter handles the following file types: + - CSV + - DOCX + - HTML + - JSON + - MD + - TEXT + - PDF (no OCR) + - PPTX + - XLSX + + Usage: + ``` + converter = MultiFileConverter() + converter.run(sources=["test.txt", "test.pdf"], meta={}) + ``` + """ + + def __init__( # noqa: PLR0915 + self, + encoding: str = "utf-8", + json_content_key: str = "content", + ) -> None: + """ + Initialize the MultiFileConverter. + + :param encoding: The encoding to use when reading files. + :param json_content_key: The key to use as content-field in a document when converting json-files. + """ + self.encoding = encoding + self.json_content_key = json_content_key + + # initialize components + router = FileTypeRouter( + mime_types=[ + ConverterMimeType.CSV.value, + ConverterMimeType.DOCX.value, + ConverterMimeType.HTML.value, + ConverterMimeType.JSON.value, + ConverterMimeType.MD.value, + ConverterMimeType.TEXT.value, + ConverterMimeType.PDF.value, + ConverterMimeType.PPTX.value, + ConverterMimeType.XLSX.value, + ], + # Ensure common extensions are registered. Tests on Windows fail otherwise. + additional_mimetypes = { + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx", + "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx" + } + ) + + csv = CSVToDocument(encoding=self.encoding) + docx = DOCXToDocument() + html = HTMLToDocument() + json = JSONConverter(content_key=self.json_content_key) + md = MarkdownToDocument() + txt = TextFileToDocument(encoding=self.encoding) + pdf = PyPDFToDocument() + pptx = PPTXToDocument() + xlsx = XLSXToDocument() + + joiner = DocumentJoiner() + + + + # Create pipeline and add components + pp = Pipeline() + + pp.add_component("router", router) + + pp.add_component("docx", docx) + pp.add_component("html", html) + pp.add_component("json", json) + pp.add_component("md", md) + pp.add_component("txt", txt) + pp.add_component("pdf", pdf) + pp.add_component("pptx", pptx) + pp.add_component("xlsx", xlsx) + pp.add_component("joiner", joiner) + pp.add_component("csv", csv) + + pp.connect(f"router.{ConverterMimeType.CSV.value}", "csv") + pp.connect(f"router.{ConverterMimeType.DOCX.value}", "docx") + pp.connect(f"router.{ConverterMimeType.HTML.value}", "html") + pp.connect(f"router.{ConverterMimeType.JSON.value}", "json") + pp.connect(f"router.{ConverterMimeType.MD.value}", "md") + pp.connect(f"router.{ConverterMimeType.TEXT.value}", "txt") + pp.connect(f"router.{ConverterMimeType.PDF.value}", "pdf") + pp.connect(f"router.{ConverterMimeType.PPTX.value}", "pptx") + pp.connect(f"router.{ConverterMimeType.XLSX.value}", "xlsx") + + pp.connect("docx.documents", "joiner.documents") + pp.connect("html.documents", "joiner.documents") + pp.connect("json.documents", "joiner.documents") + pp.connect("md.documents", "joiner.documents") + pp.connect("txt.documents", "joiner.documents") + pp.connect("pdf.documents", "joiner.documents") + pp.connect("pptx.documents", "joiner.documents") + + pp.connect("csv.documents", "joiner.documents") + pp.connect("xlsx.documents", "joiner.documents") + + + output_mapping = {"joiner.documents": "documents"} + input_mapping = { + "sources": ["router.sources"], + "meta": ["router.meta"] + } + + super(MultiFileConverter, self).__init__( + pipeline=pp, + output_mapping=output_mapping, + input_mapping=input_mapping + ) + + def to_dict(self) -> Dict[str, Any]: + """ + Serialize this instance to a dictionary. + """ + return default_to_dict( + self, + encoding=self.encoding, + json_content_key=self.json_content_key, + ) + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "MultiFileConverter": + """ + Load this instance from a dictionary. + """ + return default_from_dict(cls, data) diff --git a/haystack_experimental/super_components/indexers/__init__.py b/haystack_experimental/super_components/indexers/__init__.py new file mode 100644 index 00000000..0f617cbd --- /dev/null +++ b/haystack_experimental/super_components/indexers/__init__.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from haystack_experimental.super_components.indexers.sentence_transformers_document_indexer import ( + SentenceTransformersDocumentIndexer, +) + +__all__ = [ + "SentenceTransformersDocumentIndexer", +] diff --git a/haystack_experimental/super_components/indexers/sentence_transformers_document_indexer.py b/haystack_experimental/super_components/indexers/sentence_transformers_document_indexer.py new file mode 100644 index 00000000..ead1db18 --- /dev/null +++ b/haystack_experimental/super_components/indexers/sentence_transformers_document_indexer.py @@ -0,0 +1,202 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any, Dict, List, Literal, Optional + +from haystack import Pipeline, component, default_from_dict, default_to_dict +from haystack.components.embedders import SentenceTransformersDocumentEmbedder +from haystack.components.writers import DocumentWriter +from haystack.document_stores.types import DocumentStore, DuplicatePolicy +from haystack.utils import ( + ComponentDevice, + Secret, + deserialize_document_store_in_init_params_inplace, + deserialize_secrets_inplace, +) +from haystack.utils.hf import deserialize_hf_model_kwargs, serialize_hf_model_kwargs + +from haystack_experimental.core.super_component import SuperComponent + + +@component +class SentenceTransformersDocumentIndexer(SuperComponent): + """ + A document indexer that takes a list of documents, embeds them using SentenceTransformers, and stores them. + + Usage: + + ```python + >>> from haystack import Document + >>> from haystack.document_stores.in_memory import InMemoryDocumentStore + >>> document_store = InMemoryDocumentStore() + >>> doc = Document(content="I love pizza!") + >>> indexer = SentenceTransformersDocumentIndexer(document_store=document_store) + >>> indexer.warm_up() + >>> result = indexer.run(documents=[doc]) + >>> print(result) + {'documents_written': 1} + >>> document_store.count_documents() + 1 + ``` + """ + + def __init__( # pylint: disable=R0917 + self, + document_store: DocumentStore, + model: str = "sentence-transformers/all-mpnet-base-v2", + device: Optional[ComponentDevice] = None, + token: Optional[Secret] = Secret.from_env_var(["HF_API_TOKEN", "HF_TOKEN"], strict=False), + prefix: str = "", + suffix: str = "", + batch_size: int = 32, + progress_bar: bool = True, + normalize_embeddings: bool = False, + meta_fields_to_embed: Optional[List[str]] = None, + embedding_separator: str = "\n", + trust_remote_code: bool = False, + truncate_dim: Optional[int] = None, + model_kwargs: Optional[Dict[str, Any]] = None, + tokenizer_kwargs: Optional[Dict[str, Any]] = None, + config_kwargs: Optional[Dict[str, Any]] = None, + precision: Literal["float32", "int8", "uint8", "binary", "ubinary"] = "float32", + duplicate_policy: DuplicatePolicy = DuplicatePolicy.OVERWRITE, + ) -> None: + """ + Initialize the SentenceTransformersDocumentIndexer component. + + :param document_store: The document store where the documents should be stored. + :param model: The embedding model to use (local path or Hugging Face model ID). + :param device: The device to use for loading the model. + :param token: The API token to download private models from Hugging Face. + :param prefix: String to add at the beginning of each document text. + :param suffix: String to add at the end of each document text. + :param batch_size: Number of documents to embed at once. + :param progress_bar: If True, shows a progress bar when embedding documents. + :param normalize_embeddings: If True, embeddings are L2 normalized. + :param meta_fields_to_embed: List of metadata fields to embed along with the document text. + :param embedding_separator: Separator used to concatenate metadata fields to document text. + :param trust_remote_code: If True, allows custom models and scripts. + :param truncate_dim: Dimension to truncate sentence embeddings to. + :param model_kwargs: Additional keyword arguments for model initialization. + :param tokenizer_kwargs: Additional keyword arguments for tokenizer initialization. + :param config_kwargs: Additional keyword arguments for model configuration. + :param precision: The precision to use for the embeddings. + :param duplicate_policy: The duplicate policy to use when writing documents. + """ + self.document_store = document_store + self.model = model + self.device = device + self.token = token + self.prefix = prefix + self.suffix = suffix + self.batch_size = batch_size + self.progress_bar = progress_bar + self.normalize_embeddings = normalize_embeddings + self.meta_fields_to_embed = meta_fields_to_embed + self.embedding_separator = embedding_separator + self.trust_remote_code = trust_remote_code + self.truncate_dim = truncate_dim + self.model_kwargs = model_kwargs + self.tokenizer_kwargs = tokenizer_kwargs + self.config_kwargs = config_kwargs + self.precision = precision + self.duplicate_policy = duplicate_policy + + pipeline = Pipeline() + + pipeline.add_component( + "embedder", + SentenceTransformersDocumentEmbedder( + model=self.model, + device=self.device, + token=self.token, + prefix=self.prefix, + suffix=self.suffix, + batch_size=self.batch_size, + progress_bar=self.progress_bar, + normalize_embeddings=self.normalize_embeddings, + meta_fields_to_embed=self.meta_fields_to_embed, + embedding_separator=self.embedding_separator, + trust_remote_code=self.trust_remote_code, + truncate_dim=self.truncate_dim, + model_kwargs=self.model_kwargs, + tokenizer_kwargs=self.tokenizer_kwargs, + config_kwargs=self.config_kwargs, + precision=self.precision, + ), + ) + pipeline.add_component( + "writer", + DocumentWriter( + document_store=self.document_store, + policy=self.duplicate_policy, + ), + ) + + pipeline.connect("embedder.documents", "writer.documents") + + super(SentenceTransformersDocumentIndexer, self).__init__( + pipeline=pipeline, + input_mapping={"documents": ["embedder.documents"]}, + output_mapping={"writer.documents_written": "documents_written"}, + ) + + def to_dict(self) -> Dict[str, Any]: + """ + Serialize this instance to a dictionary. + """ + serialization_dict = default_to_dict( + self, + document_store=self.document_store.to_dict(), + model=self.model, + device=self.device.to_dict() if self.device else None, + token=self.token.to_dict() if self.token else None, + prefix=self.prefix, + suffix=self.suffix, + batch_size=self.batch_size, + progress_bar=self.progress_bar, + normalize_embeddings=self.normalize_embeddings, + meta_fields_to_embed=self.meta_fields_to_embed, + embedding_separator=self.embedding_separator, + trust_remote_code=self.trust_remote_code, + truncate_dim=self.truncate_dim, + model_kwargs=self.model_kwargs, + tokenizer_kwargs=self.tokenizer_kwargs, + config_kwargs=self.config_kwargs, + precision=self.precision, + duplicate_policy=self.duplicate_policy.value, + ) + + if serialization_dict["init_parameters"].get("model_kwargs") is not None: + serialize_hf_model_kwargs(serialization_dict["init_parameters"]["model_kwargs"]) + + return serialization_dict + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SentenceTransformersDocumentIndexer": + """ + Load an instance of this component from a dictionary. + """ + deserialize_document_store_in_init_params_inplace(data) + init_params = data.get("init_parameters", {}) + + # Handle device deserialization + if init_params.get("device") is not None: + init_params["device"] = ComponentDevice.from_dict(init_params["device"]) + + # Handle secrets deserialization + deserialize_secrets_inplace(init_params, keys=["token"]) + + + + # Handle model kwargs deserialization + if init_params.get("model_kwargs") is not None: + deserialize_hf_model_kwargs(init_params["model_kwargs"]) + + # Handle duplicate policy deserialization + if policy_value := init_params.get("duplicate_policy"): + init_params["duplicate_policy"] = DuplicatePolicy(policy_value) + + data["init_parameters"] = init_params + return default_from_dict(cls, data) diff --git a/haystack_experimental/super_components/preprocessors/__init__.py b/haystack_experimental/super_components/preprocessors/__init__.py new file mode 100644 index 00000000..2e905683 --- /dev/null +++ b/haystack_experimental/super_components/preprocessors/__init__.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from haystack_experimental.super_components.preprocessors.document_preprocessor import DocumentPreProcessor + +__all__ = [ + "DocumentPreProcessor", +] diff --git a/haystack_experimental/super_components/preprocessors/document_preprocessor.py b/haystack_experimental/super_components/preprocessors/document_preprocessor.py new file mode 100644 index 00000000..a0ba3b62 --- /dev/null +++ b/haystack_experimental/super_components/preprocessors/document_preprocessor.py @@ -0,0 +1,188 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any, Callable, Dict, List, Literal, Optional + +from haystack import Pipeline, component, default_from_dict, default_to_dict +from haystack.components.preprocessors.document_cleaner import DocumentCleaner +from haystack.components.preprocessors.document_splitter import DocumentSplitter, Language +from haystack.utils import deserialize_callable, serialize_callable + +from haystack_experimental.core.super_component import SuperComponent + + +@component +class DocumentPreProcessor(SuperComponent): + """ + A SuperComponent that cleans documents and then splits them. + + This component composes a DocumentCleaner followed by a DocumentSplitter in a single pipeline. + It takes a list of documents as input and returns a processed list of documents. + + Usage: + ```python + from haystack import Document + doc = Document(content="I love pizza!") + preprocessor = DocumentPreProcessor() + results = preprocessor.run(documents=[doc]) + print(result["documents"]) + ``` + """ + + def __init__( # pylint: disable=R0917 + self, + # --- DocumentCleaner arguments --- + remove_empty_lines: bool = True, + remove_extra_whitespaces: bool = True, + remove_repeated_substrings: bool = False, + keep_id: bool = False, + remove_substrings: Optional[List[str]] = None, + remove_regex: Optional[str] = None, + unicode_normalization: Optional[Literal["NFC", "NFKC", "NFD", "NFKD"]] = None, + ascii_only: bool = False, + # --- DocumentSplitter arguments --- + split_by: Literal["function", "page", "passage", "period", "word", "line", "sentence"] = "word", + split_length: int = 250, + split_overlap: int = 0, + split_threshold: int = 0, + splitting_function: Optional[Callable[[str], List[str]]] = None, + respect_sentence_boundary: bool = False, + language: Language = "en", + use_split_rules: bool = True, + extend_abbreviations: bool = True, + ) -> None: + """ + Initialize a DocumentPreProcessor that first cleans documents and then splits them. + + **Cleaner Params**: + :param remove_empty_lines: If `True`, removes empty lines. + :param remove_extra_whitespaces: If `True`, removes extra whitespaces. + :param remove_repeated_substrings: If `True`, remove repeated substrings like headers/footers across pages. + :param keep_id: If `True`, keeps the original document IDs. + :param remove_substrings: A list of strings to remove from the document content. + :param remove_regex: A regex pattern whose matches will be removed from the document content. + :param unicode_normalization: Unicode normalization form to apply to the text, e.g. `"NFC"`. + :param ascii_only: If `True`, convert text to ASCII only. + + **Splitter Params**: + :param split_by: The unit of splitting: "function", "page", "passage", "period", "word", "line", or "sentence". + :param split_length: The maximum number of units (words, lines, pages, etc.) in each split. + :param split_overlap: The number of overlapping units between consecutive splits. + :param split_threshold: The minimum number of units per split. If a split is smaller than this, it's merged + with the previous split. + :param splitting_function: A custom function for splitting if `split_by="function"`. + :param respect_sentence_boundary: If `True`, splits by words but tries not to break inside a sentence. + :param language: Language used by the sentence tokenizer if `split_by="sentence"` or + `respect_sentence_boundary=True`. + :param use_split_rules: Whether to apply additional splitting heuristics for the sentence splitter. + :param extend_abbreviations: Whether to extend the sentence splitter with curated abbreviations for certain + languages. + """ + # Store arguments for serialization + self.remove_empty_lines = remove_empty_lines + self.remove_extra_whitespaces = remove_extra_whitespaces + self.remove_repeated_substrings = remove_repeated_substrings + self.keep_id = keep_id + self.remove_substrings = remove_substrings + self.remove_regex = remove_regex + self.unicode_normalization = unicode_normalization + self.ascii_only = ascii_only + + self.split_by = split_by + self.split_length = split_length + self.split_overlap = split_overlap + self.split_threshold = split_threshold + self.splitting_function = splitting_function + self.respect_sentence_boundary = respect_sentence_boundary + self.language = language + self.use_split_rules = use_split_rules + self.extend_abbreviations = extend_abbreviations + + # Instantiate sub-components + cleaner = DocumentCleaner( + remove_empty_lines=self.remove_empty_lines, + remove_extra_whitespaces=self.remove_extra_whitespaces, + remove_repeated_substrings=self.remove_repeated_substrings, + keep_id=self.keep_id, + remove_substrings=self.remove_substrings, + remove_regex=self.remove_regex, + unicode_normalization=self.unicode_normalization, + ascii_only=self.ascii_only, + ) + + splitter = DocumentSplitter( + split_by=self.split_by, + split_length=self.split_length, + split_overlap=self.split_overlap, + split_threshold=self.split_threshold, + splitting_function=self.splitting_function, + respect_sentence_boundary=self.respect_sentence_boundary, + language=self.language, + use_split_rules=self.use_split_rules, + extend_abbreviations=self.extend_abbreviations, + ) + + # Build the Pipeline + pp = Pipeline() + pp.add_component("cleaner", cleaner) + pp.add_component("splitter", splitter) + + # Connect the cleaner output to splitter + pp.connect("cleaner.documents", "splitter.documents") + + # Define how pipeline inputs/outputs map to sub-component inputs/outputs + input_mapping = { + # The pipeline input "documents" feeds into "cleaner.documents" + "documents": ["cleaner.documents"] + } + # The pipeline output "documents" comes from "splitter.documents" + output_mapping = {"splitter.documents": "documents"} + + # Initialize the SuperComponent + super(DocumentPreProcessor, self).__init__( + pipeline=pp, + input_mapping=input_mapping, + output_mapping=output_mapping + ) + + def to_dict(self) -> Dict[str, Any]: + """ + Serialize this instance to a dictionary. + """ + data = default_to_dict( + self, + remove_empty_lines=self.remove_empty_lines, + remove_extra_whitespaces=self.remove_extra_whitespaces, + remove_repeated_substrings=self.remove_repeated_substrings, + keep_id=self.keep_id, + remove_substrings=self.remove_substrings, + remove_regex=self.remove_regex, + unicode_normalization=self.unicode_normalization, + ascii_only=self.ascii_only, + split_by=self.split_by, + split_length=self.split_length, + split_overlap=self.split_overlap, + split_threshold=self.split_threshold, + respect_sentence_boundary=self.respect_sentence_boundary, + language=self.language, + use_split_rules=self.use_split_rules, + extend_abbreviations=self.extend_abbreviations, + ) + + if self.splitting_function: + data["init_parameters"]["splitting_function"] = serialize_callable(self.splitting_function) + + return data + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DocumentPreProcessor": + """ + Load this instance from a dictionary. + """ + if "splitting_function" in data["init_parameters"]: + data["init_parameters"]["splitting_function"] = deserialize_callable( + data["init_parameters"]["splitting_function"] + ) + + return default_from_dict(cls, data) diff --git a/pyproject.toml b/pyproject.toml index 5f513beb..13e53b83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,15 @@ extra-dependencies = [ "amazon-bedrock-haystack>=1.1.1", "google-vertex-haystack>=2.0.0", # HierachicalSplitter w/ split_by="sentence" - "nltk" + "nltk", + # Tests for MultiFileConverter + "python-docx", + "trafilatura", + "markdown-it-py", + "mdit_plain", + "pypdf", + "python-pptx", + "openpyxl" ] [tool.hatch.envs.test.scripts] diff --git a/test/super_components/__init__.py b/test/super_components/__init__.py new file mode 100644 index 00000000..c1764a6e --- /dev/null +++ b/test/super_components/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/test/super_components/converters/__init__.py b/test/super_components/converters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/super_components/converters/test_multi_file_converter.py b/test/super_components/converters/test_multi_file_converter.py new file mode 100644 index 00000000..0b9dc32f --- /dev/null +++ b/test/super_components/converters/test_multi_file_converter.py @@ -0,0 +1,129 @@ +import pytest +from pathlib import Path +from unittest.mock import Mock, patch + +from haystack import Document +from haystack.dataclasses import ByteStream +from haystack_experimental.core.super_component import SuperComponent +from haystack_experimental.super_components.converters.multi_file_converter import MultiFileConverter + +@pytest.fixture +def converter(): + return MultiFileConverter() + + +class TestMultiFileConverter: + def test_init_default_params(self, converter): + """Test initialization with default parameters""" + assert converter.encoding == "utf-8" + assert converter.json_content_key == "content" + assert isinstance(converter, SuperComponent) + + def test_init_custom_params(self, converter): + """Test initialization with custom parameters""" + converter = MultiFileConverter( + encoding="latin-1", + json_content_key="text" + ) + assert converter.encoding == "latin-1" + assert converter.json_content_key == "text" + + def test_to_dict(self, converter): + """Test serialization to dictionary""" + data = converter.to_dict() + assert data == { + "type": "haystack_experimental.super_components.converters.multi_file_converter.MultiFileConverter", + "init_parameters": { + "encoding": "utf-8", + "json_content_key": "content" + } + } + + def test_from_dict(self): + """Test deserialization from dictionary""" + data = { + "type": "haystack_experimental.super_components.converters.multi_file_converter.MultiFileConverter", + "init_parameters": { + "encoding": "latin-1", + "json_content_key": "text" + } + } + conv = MultiFileConverter.from_dict(data) + assert conv.encoding == "latin-1" + assert conv.json_content_key == "text" + + @pytest.mark.parametrize( + "suffix,file_path", + [ + ("csv", "csv/sample_1.csv"), + ("docx", "docx/sample_docx.docx"), + ("html", "html/what_is_haystack.html"), + ("json", "json/json_conversion_testfile.json"), + ("md", "markdown/sample.md"), + ("pdf", "pdf/sample_pdf_1.pdf"), + ("pptx", "pptx/sample_pptx.pptx"), + ("txt", "txt/doc_1.txt"), + ("xlsx", "xlsx/table_empty_rows_and_columns.xlsx"), + ] + ) + @pytest.mark.integration + def test_run(self, test_files_path, converter, suffix, file_path): + paths = [test_files_path / file_path] + output = converter.run(sources=paths) + docs = output["documents"] + + assert len(docs) == 1 + assert isinstance(docs[0], Document) + assert docs[0].content is not None + assert docs[0].meta["file_path"].endswith(suffix) + + def test_run_with_meta(self, test_files_path, converter): + """Test conversion with metadata""" + paths = [test_files_path / "txt" / "doc_1.txt"] + meta = {"language": "en", "author": "test"} + output = converter.run(sources=paths, meta=meta) + docs = output["documents"] + assert docs[0].meta["language"] == "en" + assert docs[0].meta["author"] == "test" + + def test_run_with_bytestream(self, test_files_path, converter): + """Test converting ByteStream input""" + bytestream = ByteStream( + data=b"test content", + mime_type="text/plain", + meta={"file_path": "test.txt"} + ) + output = converter.run(sources=[bytestream]) + docs = output["documents"] + assert len(docs) == 1 + assert docs[0].content == "test content" + assert docs[0].meta["file_path"] == "test.txt" + + def test_run_error_handling(self, test_files_path, converter, caplog): + """Test error handling for non-existent files""" + paths = [test_files_path / "non_existent.txt"] + with caplog.at_level("WARNING"): + output = converter.run(sources=paths) + assert "Could not read" in caplog.text + assert len(output["documents"]) == 0 + + @pytest.mark.integration + def test_run_all_file_types(self, test_files_path, converter): + """Test converting all supported file types in parallel""" + paths = [ + test_files_path / "csv" / "sample_1.csv", + test_files_path / "docx" / "sample_docx.docx", + test_files_path / "html" / "what_is_haystack.html", + test_files_path / "json" / "json_conversion_testfile.json", + test_files_path / "markdown" / "sample.md", + test_files_path / "txt" / "doc_1.txt", + test_files_path / "pdf" / "sample_pdf_1.pdf", + test_files_path / "pptx" / "sample_pptx.pptx", + test_files_path / "xlsx" / "table_empty_rows_and_columns.xlsx" + ] + output = converter.run(sources=paths) + docs = output["documents"] + + # Verify we got a document for each file + assert len(docs) == len(paths) + assert all(isinstance(doc, Document) for doc in docs) diff --git a/test/super_components/indexers/__init__.py b/test/super_components/indexers/__init__.py new file mode 100644 index 00000000..c1764a6e --- /dev/null +++ b/test/super_components/indexers/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/test/super_components/indexers/test_document_indexer.py b/test/super_components/indexers/test_document_indexer.py new file mode 100644 index 00000000..19442c1d --- /dev/null +++ b/test/super_components/indexers/test_document_indexer.py @@ -0,0 +1,126 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from unittest.mock import Mock, ANY +from uuid import UUID +import pytest + +from haystack_experimental.super_components.indexers import SentenceTransformersDocumentIndexer + +from haystack import Document, Pipeline +from haystack.document_stores.in_memory import InMemoryDocumentStore +from haystack.components.embedders import SentenceTransformersDocumentEmbedder +from haystack.components.writers import DocumentWriter + + +class TestDocumentIndexer: + @pytest.fixture + def indexer(self) -> SentenceTransformersDocumentIndexer: + return SentenceTransformersDocumentIndexer(document_store=InMemoryDocumentStore()) + + @pytest.fixture + def embedding_backend(self, indexer: SentenceTransformersDocumentIndexer, monkeypatch: pytest.MonkeyPatch) -> Mock: + backend = Mock() + backend.embed.return_value = [ + [0.3, 0.4, 0.01, 0.7], + [0.1, 0.9, 0.87, 0.3], + ] + + embedder = indexer.pipeline.get_component("embedder") + monkeypatch.setattr(embedder, "embedding_backend", backend) + + return backend + + def test_init(self, indexer: SentenceTransformersDocumentIndexer) -> None: + assert isinstance(indexer.pipeline, Pipeline) + assert indexer.input_mapping == {"documents": ["embedder.documents"]} + assert indexer.output_mapping == {"writer.documents_written": "documents_written"} + + embedder = indexer.pipeline.get_component("embedder") + assert isinstance(embedder, SentenceTransformersDocumentEmbedder) + + writer = indexer.pipeline.get_component("writer") + assert isinstance(writer, DocumentWriter) + assert isinstance(writer.document_store, InMemoryDocumentStore) + assert writer.document_store.bm25_tokenization_regex == r"(?u)\b\w\w+\b" + assert writer.document_store.bm25_algorithm == "BM25L" + assert writer.document_store.bm25_parameters == {} + assert writer.document_store.embedding_similarity_function == "dot_product" + assert UUID(writer.document_store.index, version=4) + + def test_from_dict(self) -> None: + indexer = SentenceTransformersDocumentIndexer.from_dict( + { + "init_parameters": { + 'document_store': { + 'init_parameters': { + 'bm25_algorithm': 'BM25L', + 'bm25_parameters': {}, + 'bm25_tokenization_regex': '(?u)\\b\\w\\w+\\b', + 'embedding_similarity_function': 'dot_product', + 'index': '28f84766-11b7-4eac-bb75-3ee4e8d56958' + }, + 'type': 'haystack.document_stores.in_memory.document_store.InMemoryDocumentStore' + }, + "prefix": "", + "suffix": "", + "batch_size": 32, + "embedding_separator": "\n", + "meta_fields_to_embed": None, + "duplicate_policy": "overwrite", + }, + "type": "haystack_experimental.super_components.indexers.sentence_transformers_document_indexer.SentenceTransformersDocumentIndexer", + } + ) + assert isinstance(indexer, SentenceTransformersDocumentIndexer) + + def test_to_dict(self, indexer: SentenceTransformersDocumentIndexer) -> None: + expected = {'init_parameters': {'batch_size': 32, + 'config_kwargs': None, + 'device': None, + 'document_store': {'init_parameters': {'bm25_algorithm': 'BM25L', + 'bm25_parameters': {}, + 'bm25_tokenization_regex': '(?u)\\b\\w\\w+\\b', + 'embedding_similarity_function': 'dot_product', + 'index': ANY}, + 'type': 'haystack.document_stores.in_memory.document_store.InMemoryDocumentStore'}, + 'duplicate_policy': 'overwrite', + 'embedding_separator': '\n', + 'meta_fields_to_embed': None, + 'model': 'sentence-transformers/all-mpnet-base-v2', + 'model_kwargs': None, + 'normalize_embeddings': False, + 'precision': 'float32', + 'prefix': '', + 'progress_bar': True, + 'suffix': '', + 'token': {'env_vars': ['HF_API_TOKEN', 'HF_TOKEN'], + 'strict': False, + 'type': 'env_var'}, + 'tokenizer_kwargs': None, + 'truncate_dim': None, + 'trust_remote_code': False}, + 'type': 'haystack_experimental.super_components.indexers.sentence_transformers_document_indexer.SentenceTransformersDocumentIndexer' + } + assert indexer.to_dict() == expected + + def test_warm_up(self, indexer: SentenceTransformersDocumentIndexer, monkeypatch: pytest.MonkeyPatch) -> None: + with monkeypatch.context() as m: + m.setattr(indexer.pipeline, "warm_up", Mock()) + + indexer.warm_up() + + indexer.pipeline.warm_up.assert_called_once() + + def test_run(self, indexer: SentenceTransformersDocumentIndexer, embedding_backend: Mock) -> None: + documents = [ + Document(content="Test document"), + Document(content="Another test document"), + ] + + indexer.warm_up() + result = indexer.run(documents=documents) + + embedding_backend.embed.assert_called_once + assert result == {"documents_written": len(documents)} diff --git a/test/test_files/csv/sample_1.csv b/test/test_files/csv/sample_1.csv new file mode 100644 index 00000000..7cf88f29 --- /dev/null +++ b/test/test_files/csv/sample_1.csv @@ -0,0 +1,4 @@ +Name,Age +John Doe,27 +Jane Smith,37 +Mike Johnson,47 diff --git a/test/test_files/docx/sample_docx.docx b/test/test_files/docx/sample_docx.docx new file mode 100644 index 00000000..3a740ac9 Binary files /dev/null and b/test/test_files/docx/sample_docx.docx differ diff --git a/test/test_files/html/what_is_haystack.html b/test/test_files/html/what_is_haystack.html new file mode 100644 index 00000000..2d62b206 --- /dev/null +++ b/test/test_files/html/what_is_haystack.html @@ -0,0 +1,1634 @@ + + + + + + + + + + What is Haystack? | Haystack + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 🎃 We're participating in Hacktoberfest 2023! + + + + + +
+
+ + + +
+ +
+
+
+ + + + + +
+

What is Haystack?

+

Haystack is the open source Python framework by deepset for building custom apps with large language models (LLMs). It lets you quickly try out the latest models in natural language processing (NLP) while being flexible and easy to use. Our inspiring community of users and builders has helped shape Haystack into what it is today: a complete framework for building production-ready NLP apps.

+

Building with Haystack

+

Haystack offers comprehensive tooling for developing state-of-the-art NLP systems that use LLMs (such as GPT-4, Falcon and similar) and Transformer models . With Haystack, you can effortlessly experiment with various models hosted on platforms like Hugging Face, OpenAI, Cohere, or even models deployed on SageMaker and your local models to find the perfect fit for your use case.

+ + + + + + + + + + + + + + + + + + + + + Model Providers + + +

Some examples of what you can build include:

+
    +
  • Semantic search on a large collection of documents in any language
  • +
  • Generative question answering on a knowledge base containing mixed types of information: images, text, and tables.
  • +
  • Natural language chatbots powered by cutting-edge generative models like GPT-4
  • +
  • An LLM-based Haystack Agent capable of resolving complex queries
  • +
  • Information extraction from documents to populate your database or build a knowledge graph
  • +
+

This is just a small subset of the kinds of systems that can be created in Haystack.

+

Functionality for all stages of an NLP project

+

A successful NLP project requires more than just the language models. As an end-to-end framework, Haystack assists you in building your system every step of the way, offering tooling for each stage of the NLP project life cycle:

+ +

But that’s not all: +metadata filtering, +model distillation, or the prompt hub, whatever your NLP heart desires, you’re likely to find it in Haystack. And if not? We’ll build it together.

+ + + + + + + + + + + + + + + + + + + + + + + Rest API + + +

Building blocks

+

Haystack uses a few simple but effective concepts to help you build fully functional and customized end-to-end NLP systems.

+

Components

+

At the core of Haystack are its components—fundamental building blocks that can perform tasks like document retrieval, text generation, or summarization. A single component is already quite powerful. It can manage local language models or communicate with a hosted model through an API.

+

While Haystack offers a bunch of components you can use out of the box, it also lets you create your own custom components. Explore the +collection of integrations that includes custom components developed by our community, which you can freely use.

+

You can chain components together to build pipelines, which are the foundation of the NLP app architecture in Haystack.

+

Pipelines

+

Pipelines are powerful structures made up of components, such as a Retriever and Reader, connected to infrastructure building blocks, such as a DocumentStore (for example, Elasticsearch or Weaviate) to form complex systems.

+

Haystack offers ready-made pipelines for most common tasks, such as question answering, document retrieval, or summarization. But it’s just as easy to design and create a custom pipeline for NLP scenarios that are way more complex than question answering.

+

Agents

+

The Haystack Agent makes use of a large language model to resolve complex tasks. When initializing the Agent, you give it a set of tools, which can be pipeline components or whole pipelines. The Agent can use to those tools iteratively to arrive at an answer. When given a query, the Agent determines which tools are useful to answer this query and calls them in a loop until it gets the answer. This way, it can achieve much more than extractive or generative question answering pipelines.

+ + + + + + + + + + + + + + + + + + + + + Agent Tools + + +

Who’s it for?

+

Haystack is for everyone looking to build natural language apps—NLP enthusiasts and newbies alike. You don’t need to understand how the models work under the hood. With Haystack’s modular and flexible components, pipelines, and agents, all you need is some basic knowledge of Python to dive right in.

+

Our community

+

At the heart of Haystack is the vibrant open source community that thrives on the diverse backgrounds and skill sets of its members. We value collaboration greatly and encourage our users to shape Haystack actively through GitHub contributions. Our Discord channel is a space where community members can connect, seek help, and learn from each other.

+

We also organize live online and in-person events, webinars, and office hours, which are an opportunity to learn and grow.

+ + + + + + + + +
+ + + +
+ Join Discord +
+ + + +
+
+ +

Enter the Haystack universe

+ + + + +
+ + + +
+ +
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+ +
+ + + +
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test_files/json/json_conversion_testfile.json b/test/test_files/json/json_conversion_testfile.json new file mode 100644 index 00000000..04620d3a --- /dev/null +++ b/test/test_files/json/json_conversion_testfile.json @@ -0,0 +1 @@ +{"content": "Content from a json file"} \ No newline at end of file diff --git a/test/test_files/markdown/sample.md b/test/test_files/markdown/sample.md new file mode 100644 index 00000000..e02535b7 --- /dev/null +++ b/test/test_files/markdown/sample.md @@ -0,0 +1,2 @@ + +## A test document for markdown diff --git a/test/test_files/pdf/sample_pdf_1.pdf b/test/test_files/pdf/sample_pdf_1.pdf new file mode 100644 index 00000000..87259b89 Binary files /dev/null and b/test/test_files/pdf/sample_pdf_1.pdf differ diff --git a/test/test_files/pptx/sample_pptx.pptx b/test/test_files/pptx/sample_pptx.pptx new file mode 100644 index 00000000..1f6e3791 Binary files /dev/null and b/test/test_files/pptx/sample_pptx.pptx differ diff --git a/test/test_files/txt/doc_1.txt b/test/test_files/txt/doc_1.txt new file mode 100644 index 00000000..41218908 --- /dev/null +++ b/test/test_files/txt/doc_1.txt @@ -0,0 +1,2 @@ +Some text for testing. +Two lines in here. diff --git a/test/test_files/xlsx/table_empty_rows_and_columns.xlsx b/test/test_files/xlsx/table_empty_rows_and_columns.xlsx new file mode 100644 index 00000000..f599b8c3 Binary files /dev/null and b/test/test_files/xlsx/table_empty_rows_and_columns.xlsx differ