import glob import textwrap from .utils.warnings_config import install_warning_filters install_warning_filters() from .utils.terminal_interface import interface from .extraction.text_to_sentences import extract_txt_sentences # from .extraction.pdf_to_sentences import extract_pdf_sentences from .extraction.pdf_to_chunks import extract_pdf_chunks from .extraction.sentence_to_qualities import build_sentence_decomposer from .extraction.decompose import decompose, DecomposeMode from .extraction.triplet_extraction import extract_triplets # from chunk_decompose import build_chunk_decomposer # (we'll add similarly later) # from Requirement_Extraction.chunk_decompose import extract as chunk_decompose # from .graph.triplet_extraction import extract_triplets from .graph import get_graph from .graph.utils import map_doc_extracted_triplets_to_graph_relations from tqdm import tqdm from kbdebugger.compat.langchain import Document import rich graph = get_graph() # ENVIRONMENT VARIABLES DATA_SOURCE = "DSA" DATA_FILE = " ./data/SDS/20241015_MISSION_KI_Glossar_v1.0 en.pdf" EXTRACT_TYPE = "Sentences" RETRIEVING_APPROACH = "Sparse Retrieval" BULK_UPLOAD = True def system_message(txt: str) -> None: print("\n##########>> SYSTEM <<##########") print(txt) print("################################\n") def bulk_upload(documents): relation_update = 0 for doc in tqdm(documents, desc="Decompose the Documents", unit="item"): # decompose_list = sentence_decompose(doc.page_content) doc_decomposed = decompose( text=doc.page_content, mode=DecomposeMode.SENTENCES, ) try: for sentence in doc_decomposed: triplets = extract_triplets(sentence) for relation in map_doc_extracted_triplets_to_graph_relations(triplets, doc): print(relation) graph.upsert_relation(relation) relation_update += 1 except: print(f"Not able to upload: {doc.page_content}") continue system_message(f"We add or update {relation_update} relations in your Graph Knowledge") def get_similar( documents: list[Document] ) -> None: """ Get similar documents from the existing knowledge graph and add new relations based on user approval. Interactively asks the user for input on which retrieving approach to use and whether to upload new relations. Args: documents (list): List of LangChain `Document` objects (produced earlier by create_sentences or create_chunks etc.). to be used for retrieval and relation extraction. Assume documents is a list of Document objects, each document can be as simple as one sentence or chunk. So we iterate over each document (sentence), use this sentence as a query and query the KB (Knowledge Graph) to get similar sentences that are already in the graph. Now say the KB result set returned 2 sentences. For each of these sentences, we now query our NEW DATA SOURCE (e.g., DSA knowledge.txt) using the retrieving approach (Dense, Sparse, Hybrid) to get relevant information. """ BULK_UPLOAD = False # TODO: remove this line as it is not used global RETRIEVING_APPROACH # build Retriever match interface( message="Which retrieving approach would you like to use?", options=["Dense Retrieval", "Sparse Retrieval", "Hybrid Retrieval"] ): case "Sparse Retrieval": from .retrieval.BM25Retriever import build_retriever Retriever = build_retriever(documents) case "Dense Retrieval": from .retrieval.SemanticRetriever import build_retriever Retriever = build_retriever(documents) RETRIEVING_APPROACH = "Dense Retrieval" case "Hybrid Retrieval": from .retrieval.HybridRetriever import build_retriever Retriever = build_retriever(documents) RETRIEVING_APPROACH = "Hybrid Retrieval" # Old: # Cypher_query = """ # MATCH (n)-[r:is]->(req:Node{label:"requirement"}) # RETURN DISTINCT r.sentence as sentence # """ # New # cypher_query = """ # MATCH (n)-[r:REL]->(req:Node{label:"requirement"}) # RETURN DISTINCT r.sentence as sentence # """ cypher_query = textwrap.dedent(""" MATCH ()-[r:REL]->(n:Node{label:"requirement"}) WHERE r.sentence IS NOT NULL RETURN DISTINCT r.sentence AS sentence """).strip() # Query our Knowledge Base (Graph) graph_result = graph.query(cypher_query) for source in graph_result: print("################################################################") source = source["sentence"] #e.g., "fairness is requirement" system_message("Graph Information to expand:") print(source) relevant_docs = Retriever.invoke(source) # type: ignore # retrieve relevant docs containing: "fairness is requirement" """ # Example: relevant_docs = [ Document( page_content="Equality is subclass of Fairness", metadata={"source": "DSA_knowledge.txt", "page_number": 3} ), Document( page_content="Human agency and oversight is a requirement", metadata={"source": "DSA_knowledge.txt", "page_number": 5} ), ] """ # remove the source from the relevant_docs relevant_docs = [doc for doc in relevant_docs if doc.page_content != source] print("---------------------") system_message(f"Relevant Information based on the '{RETRIEVING_APPROACH}' retrieving approach:") for i,sen in enumerate(relevant_docs): print(i," : ", sen.page_content) print(sen.metadata) print("---------------------\n") for doc in relevant_docs: if not isinstance(doc, Document): continue print("===========================================================") system_message("Decompose the current document:") print(doc.page_content) # decomposed_list = decompose(doc.page_content) # i.e., chunk the page_content doc_decomposed = decompose( text=doc.page_content, mode=DecomposeMode.SENTENCES, ) """ e.g., doc_decomposed = [ "Human agency and oversight is a requirement.", "Equality is subclass of Fairness.", ] """ # decompose_list is a list of chunks or sentences based on EXTRACT_TYPE print("===========================================================") system_message("Atomic decomposed sentences:") for i,sen in enumerate(doc_decomposed): print(i,": ", sen) print("===========================================================") try: for sentence in doc_decomposed: print("&&&&&&&&&&&&&&&&&&&&&&&&") print("Sentence: ",sentence) extracted_triplets = extract_triplets(sentence) """ e.g. extracted_triplets = { 'sentence': "Human agency and oversight is a requirement.", 'edges': [ ('Human agency', 'requirement', 'is'), ('oversight', 'requirement', 'is') ] } """ print(f"Extraction Result: {extracted_triplets}") graph_relations = map_doc_extracted_triplets_to_graph_relations(extracted_triplets, doc) for i, relation in enumerate(graph_relations): # relation_sentence = relation['edge']['properties']['sentence'] rich.print(f"👉️ Proposed triplet: [bold yellow]{extracted_triplets['triplets'][i]}[/bold yellow]") match interface( f"Would you like to upload the above relation to the knowledge graph?", ["YES", "NO"] ): case "YES": graph.upsert_relation(relation) print("✅ [UPSERT] relation upserted to knowledge graph\n\n") case "NO": print("😢 [INFO] relation neglected") print("&&&&&&&&&&&&&&&&&&&&&&&&") except: print(f"Not able to upload: {doc.page_content}") continue print("################################################################") def main(): # module-level variables global DATA_SOURCE global DATA_FILE global EXTRACT_TYPE global BULK_UPLOAD print("Welcome to TrustiFA Knowledge Graph Extraction Tool") documents: list | None = None match interface( "Which data source do you want to use?", ["DSA", "SDS"] ): case "DSA": documents = extract_txt_sentences("data/DSA/DSA_knowledge.txt") case "SDS": DATA_SOURCE = "SDS" files_list = glob.glob("data/SDS/**/*.pdf", recursive=True) file = interface( "Which file do you want to use?", files_list ) if file is None: raise ValueError("No file selected. Exiting.") DATA_FILE = file match interface( "Do you want to extract sentences or chunks?", ["Sentences", "Chunks"] ): # case "Sentences": # documents = extract_pdf_sentences(file) case "Chunks": EXTRACT_TYPE = "Chunks" documents = extract_pdf_chunks(file) if documents is None: raise ValueError("No data loaded. Exiting.") system_message(f"Loaded {len(documents)} documents from {DATA_SOURCE} source using {EXTRACT_TYPE} extraction.") if DATA_SOURCE == "DSA": match interface( "Do you want the bulk upload to Graph Knowledge?", ["Yes", "No"] ): case "Yes": bulk_upload(documents) case "No": get_similar(documents) else: get_similar(documents) if __name__ == "__main__": main()