CoolFace
Apppublic

jdwh08s/Autodoc-Lifter

sourceHugging Faceagpl-3.0updated 2y agoView on Hugging Face
0likes
full_doc.py337 linesDownload Raw Back to root
1#####################################################2### DOCUMENT PROCESSOR [FULLDOC]3#####################################################4### Jonathan Wang5 6# ABOUT:7# This creates an app to chat with PDFs.8 9# This is the FULLDOC10# which is a class that associates documents11# with their critical information12# and their tools. (keywords, summary, queryengine, etc.)13#####################################################14### TODO Board:15# Automatically determine which reader to use for each document based on the file type.16 17#####################################################18### PROGRAM SETTINGS19 20#####################################################21### PROGRAM IMPORTS22from __future__ import annotations23 24import asyncio25from pathlib import Path26from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, TypeVar27from uuid import UUID, uuid428 29from llama_index.core import StorageContext, VectorStoreIndex30from llama_index.core.query_engine import SubQuestionQueryEngine31from llama_index.core.schema import BaseNode, TransformComponent32from llama_index.core.settings import Settings33from llama_index.core.tools import QueryEngineTool, ToolMetadata34from streamlit import session_state as ss35 36if TYPE_CHECKING:37    from llama_index.core.base.base_query_engine import BaseQueryEngine38    from llama_index.core.callbacks import CallbackManager39    from llama_index.core.node_parser import NodeParser40    from llama_index.core.readers.base import BaseReader41    from llama_index.core.response_synthesizers import BaseSynthesizer42    from llama_index.core.retrievers import BaseRetriever43 44# Own Modules45from engine import get_engine46from keywords import KeywordMetadataAdder47from retriever import get_retriever48from storage import get_docstore, get_vector_store49from summary import DEFAULT_ONELINE_SUMMARY_TEMPLATE, DEFAULT_TREE_SUMMARY_TEMPLATE50 51#####################################################52### SCRIPT53 54GenericNode = TypeVar("GenericNode", bound=BaseNode)55 56class FullDocument:57    """Bundles all the information about a document together.58 59    Args:60        name (str): The name of the document.61        file_path (Path): The path to the document.62        summary (str): The summary of the document.63        keywords (List[str]): The keywords of the document.64        entities (List[str]): The entities of the document.65        vector_store (BaseDocumentStore): The vector store of the document.66    """67 68    # Identifiers69    id: UUID70    name: str71    file_path: Path72    file_name: str73 74    # Basic Contents75    summary: str76    summary_oneline: str  # A one line summary of the document.77    keywords: set[str]  # List of keywords in document.78    # entities: Set[str]  # list of entities in document  ## TODO: Add entities79    metadata: dict[str, Any] | None80    # NOTE: other metdata that might be useful:81        # Document Creation / Last Date (e.g., recency important for legal/medical questions)82        # Document Source and Trustworthiness83        # Document Access Level (though this isn't important for us here.)84        # Document Citations?85        # Document Format? (text/spreadsheet/presentation/image/etc.)86 87    # RAG Components88    nodes: list[BaseNode]89    storage_context: StorageContext  # NOTE: current setup has single storage context per document.90    vector_store_index: VectorStoreIndex91    retriever: BaseRetriever  # TODO(Jonathan Wang): Consider multiple retrievers for keywords vs semantic.92    engine: BaseQueryEngine  # TODO(Jonathan Wang): Consider mulitple engines. 93    subquestion_engine: SubQuestionQueryEngine94 95    def __init__(96        self,97        name: str,98        file_path: Path | str,99        metadata: dict[str, Any] | None = None100    ) -> None:101        self.id = uuid4()102        self.name = name103 104        if (isinstance(file_path, str)):105            file_path = Path(file_path)106        self.file_path = file_path107        self.file_name = file_path.name108 109        self.metadata = metadata110 111 112    @classmethod113    def class_name(cls) -> str:114        return "FullDocument"115 116    def add_name_to_nodes(self, nodes: list[GenericNode]) -> list[GenericNode]:117        """Add the name of the document to the nodes.118 119        Args:120            nodes (List[GenericNode]): The nodes to add the name to.121 122        Returns:123            List[GenericNode]: The nodes with the name added.124        """125        for node in nodes:126            node.metadata["name"] = self.name127        return nodes128 129    def file_to_nodes(130        self,131        reader: BaseReader,132        postreaders: list[Callable[[list[GenericNode]], list[GenericNode]] | TransformComponent] | None=None,  # NOTE: these should be used in order. and probably all TransformComponent instead.133        node_parser: NodeParser | None=None,134        postparsers: list[Callable[[list[GenericNode]], list[GenericNode]] | TransformComponent] | None=None,  # Stuff like chunking, adding Embeddings, etc.135    ) -> None:136        """Read in the file path and get the nodes.137 138        Args:139            file_path (Optional[Path], optional): The path to the file. Defaults to file_path from init.140            reader (Optional[BaseReader], optional): The reader to use. Defaults to reader from init.141        """142        # Use the provided reader to read in the file.143        print("NEWPDF: Reading input file...")144        nodes = reader.load_data(file_path=self.file_path)145 146        # Use node postreaders to post process the nodes.147        if (postreaders is not None):148            for node_postreader in postreaders:149                nodes = node_postreader(nodes)  # type: ignore  (TransformComponent allows a list of nodes)150 151        # Use node parser to parse the nodes.152        if (node_parser is None):153            node_parser = Settings.node_parser154            nodes = node_parser(nodes)  # type: ignore  (Document is a child of BaseNode)155 156        # Use node postreaders to post process the nodes. (also add the common name to the nodes)157        if (postparsers is None):158            postparsers = [self.add_name_to_nodes]159        else:160            postparsers.append(self.add_name_to_nodes)161 162        for node_postparser in postparsers:163            nodes = node_postparser(nodes)  # type: ignore  (TransformComponent allows a list of nodes)164 165        # Save nodes166        self.nodes = nodes  # type: ignore167 168    def nodes_to_summary(169        self,170        summarizer: BaseSynthesizer,  # NOTE: this is typically going to be a TreeSummarizer / SimpleSummarize for our use case171        query_str: str = DEFAULT_TREE_SUMMARY_TEMPLATE,172    ) -> None:173        """Summarize the nodes.174 175        Args:176            summarizer (BaseSynthesizer): The summarizer to use. Takes in nodes and returns summary.177        """178        if (not hasattr(self, "nodes")):179            msg = "Nodes must be extracted from document using `file_to_nodes` before calling `nodes_to_summary`."180            raise ValueError(msg)181 182        text_chunks = [getattr(node, "text", "") for node in self.nodes if hasattr(node, "text")]183        summary_responses = summarizer.aget_response(query_str=query_str, text_chunks=text_chunks)184 185        loop = asyncio.get_event_loop()186        summary = loop.run_until_complete(summary_responses)187 188        if (not isinstance(summary, str)):189            # TODO(Jonathan Wang): ... this should always give us a string, right? we're not doing anything fancy with TokenGen/TokenAsyncGen/Pydantic BaseModel...190            msg = f"Summarizer must return a string summary. Actual type: {type(summary)}, with value {summary}."191            raise TypeError(msg)192 193        self.summary = summary194 195    def summary_to_oneline(196        self,197        summarizer: BaseSynthesizer,  # NOTE: this is typically going to be a SimpleSummarize / TreeSummarizer for our use case198        query_str: str = DEFAULT_ONELINE_SUMMARY_TEMPLATE,199    ) -> None:200 201        if (not hasattr(self, "summary")):202            msg = "Summary must be extracted from document using `nodes_to_summary` before calling `summary_to_oneline`."203            raise ValueError(msg)204 205        oneline = summarizer.get_response(query_str=query_str, text_chunks=[self.summary])  # There's only one chunk.206        self.summary_oneline = oneline  # type: ignore | shouldn't have fancy TokenGenerators / TokenAsyncGenerators / Pydantic BaseModels207 208    def nodes_to_document_keywords(self, keyword_extractor: Optional[KeywordMetadataAdder] = None) -> None:209        """Save the keywords from the nodes into the document.210 211        Args:212            keyword_extractor (Optional[BaseKeywordExtractor], optional): The keyword extractor to use. Defaults to None.213        """214        if (not hasattr(self, "nodes")):215            msg = "Nodes must be extracted from document using `file_to_nodes` before calling `nodes_to_keywords`."216            raise ValueError(msg)217 218        if (keyword_extractor is None):219            keyword_extractor = KeywordMetadataAdder()220 221        # Add keywords to nodes using KeywordMetadataAdder222        keyword_extractor.process_nodes(self.nodes)223 224        # Save keywords225        keywords: list[str] = []226        for node in self.nodes:227            node_keywords = node.metadata.get("keyword_metadata", "").split(", ")  # NOTE: KeywordMetadataAdder concatinates b/c required string output228            keywords = keywords + node_keywords229 230        # TODO(Jonathan Wang): handle dedupling keywords which are similar to each other (fuzzy?)231        self.keywords = set(keywords)232 233    def nodes_to_storage(self, create_new_storage: bool = True) -> None:234        """Save the nodes to storage."""235        if (not hasattr(self, "nodes")):236            msg = "Nodes must be extracted from document using `file_to_nodes` before calling `nodes_to_storage`."237            raise ValueError(msg)238 239        if (create_new_storage):240            docstore = get_docstore(documents=self.nodes)241            self.docstore = docstore242 243            vector_store = get_vector_store()244 245            storage_context = StorageContext.from_defaults(246                docstore=docstore,247                vector_store=vector_store248            )249            self.storage_context = storage_context250 251            vector_store_index = VectorStoreIndex(252                self.nodes, storage_context=storage_context253            )254            self.vector_store_index = vector_store_index255 256        else:257            ### TODO(Jonathan Wang): use an existing storage instead of creating a new one.258            msg = "Currently creates new storage for every document."259            raise NotImplementedError(msg)260 261    # TODO(Jonathan Wang): Create multiple different retrievers based on the question type(?)262    # E.g., if the question is focused on specific keywords or phrases, use a retriever oriented towards sparse scores.263    def storage_to_retriever(264        self,265        semantic_nodes: int = 6,266        sparse_nodes: int = 3,267        fusion_nodes: int = 3,268        semantic_weight: float = 0.6,269        merge_up_thresh: float = 0.5,270        callback_manager: CallbackManager | None=None271    ) -> None:272        """Create retriever from storage."""273        if (not hasattr(self, "vector_store_index")):274            msg = "Vector store must be extracted from document using `nodes_to_storage` before calling `storage_to_retriever`."275            raise ValueError(msg)276 277        retriever = get_retriever(278            _vector_store_index=self.vector_store_index,279            semantic_top_k=semantic_nodes,280            sparse_top_k=sparse_nodes,281            fusion_similarity_top_k=fusion_nodes,282            semantic_weight_fraction=semantic_weight,283            merge_up_thresh=merge_up_thresh,284            verbose=True,285            _callback_manager=callback_manager or ss.callback_manager286        )287        self.retriever = retriever288 289    def retriever_to_engine(290        self,291        response_synthesizer: BaseSynthesizer,292        callback_manager: CallbackManager | None=None293    ) -> None:294        """Create query engine from retriever."""295        if (not hasattr(self, "retriever")):296            msg = "Retriever must be extracted from document using `storage_to_retriever` before calling `retriver_to_engine`."297            raise ValueError(msg)298 299        engine = get_engine(300            retriever=self.retriever,301            response_synthesizer=response_synthesizer,302            callback_manager=callback_manager or ss.callback_manager303        )304        self.engine = engine305 306    # TODO(Jonathan Wang): Create Summarization Index and Engine.307    def engine_to_sub_question_engine(self) -> None:308        """Convert a basic query engine into a sub-question query engine for handling complex, multi-step questions.309 310        Args:311            query_engine (BaseQueryEngine): The Base Query Engine to convert.312        """313        if (not hasattr(self, "summary_oneline")):314            msg = "One Line Summary must be created for the document before calling `engine_to_sub_query_engine`"315            raise ValueError(msg)316        elif (not hasattr(self, "engine")):317            msg = "Basic Query Engine must be created before calling `engine_to_sub_query_engine`"318            raise ValueError(msg)319 320        sqe_tools = [321            QueryEngineTool(322                query_engine=self.engine,  # TODO(Jonathan Wang): handle mulitple engines?323                metadata=ToolMetadata(324                    name=(self.name + "simple query answerer"),325                    description=f"""A tool that answers simple questions about the following document: {self.summary_oneline}"""326                )327            )328            # TODO(Jonathan Wang): add more tools329        ]330 331        subquestion_engine = SubQuestionQueryEngine.from_defaults(332            query_engine_tools=sqe_tools, 333            verbose=True,334            use_async=True335        )336        self.subquestion_engine = subquestion_engine337