| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 | 
							- # Copyright (c) 2024 Microsoft Corporation.
 - # Licensed under the MIT License
 - """
 - Reference:
 -  - [graphrag](https://github.com/microsoft/graphrag)
 - """
 - 
 - import logging
 - import numbers
 - import re
 - import traceback
 - from dataclasses import dataclass
 - from typing import Any, Mapping, Callable
 - import tiktoken
 - from graphrag.graph_prompt import GRAPH_EXTRACTION_PROMPT, CONTINUE_PROMPT, LOOP_PROMPT
 - from graphrag.utils import ErrorHandlerFn, perform_variable_replacements, clean_str
 - from rag.llm.chat_model import Base as CompletionLLM
 - import networkx as nx
 - from rag.utils import num_tokens_from_string
 - from timeit import default_timer as timer
 - 
 - DEFAULT_TUPLE_DELIMITER = "<|>"
 - DEFAULT_RECORD_DELIMITER = "##"
 - DEFAULT_COMPLETION_DELIMITER = "<|COMPLETE|>"
 - DEFAULT_ENTITY_TYPES = ["organization", "person", "location", "event", "time"]
 - ENTITY_EXTRACTION_MAX_GLEANINGS = 1
 - 
 - 
 - @dataclass
 - class GraphExtractionResult:
 -     """Unipartite graph extraction result class definition."""
 - 
 -     output: nx.Graph
 -     source_docs: dict[Any, Any]
 - 
 - 
 - class GraphExtractor:
 -     """Unipartite graph extractor class definition."""
 - 
 -     _llm: CompletionLLM
 -     _join_descriptions: bool
 -     _tuple_delimiter_key: str
 -     _record_delimiter_key: str
 -     _entity_types_key: str
 -     _input_text_key: str
 -     _completion_delimiter_key: str
 -     _entity_name_key: str
 -     _input_descriptions_key: str
 -     _extraction_prompt: str
 -     _summarization_prompt: str
 -     _loop_args: dict[str, Any]
 -     _max_gleanings: int
 -     _on_error: ErrorHandlerFn
 - 
 -     def __init__(
 -         self,
 -         llm_invoker: CompletionLLM,
 -         prompt: str | None = None,
 -         tuple_delimiter_key: str | None = None,
 -         record_delimiter_key: str | None = None,
 -         input_text_key: str | None = None,
 -         entity_types_key: str | None = None,
 -         completion_delimiter_key: str | None = None,
 -         join_descriptions=True,
 -         encoding_model: str | None = None,
 -         max_gleanings: int | None = None,
 -         on_error: ErrorHandlerFn | None = None,
 -     ):
 -         """Init method definition."""
 -         # TODO: streamline construction
 -         self._llm = llm_invoker
 -         self._join_descriptions = join_descriptions
 -         self._input_text_key = input_text_key or "input_text"
 -         self._tuple_delimiter_key = tuple_delimiter_key or "tuple_delimiter"
 -         self._record_delimiter_key = record_delimiter_key or "record_delimiter"
 -         self._completion_delimiter_key = (
 -             completion_delimiter_key or "completion_delimiter"
 -         )
 -         self._entity_types_key = entity_types_key or "entity_types"
 -         self._extraction_prompt = prompt or GRAPH_EXTRACTION_PROMPT
 -         self._max_gleanings = (
 -             max_gleanings
 -             if max_gleanings is not None
 -             else ENTITY_EXTRACTION_MAX_GLEANINGS
 -         )
 -         self._on_error = on_error or (lambda _e, _s, _d: None)
 -         self.prompt_token_count = num_tokens_from_string(self._extraction_prompt)
 - 
 -         # Construct the looping arguments
 -         encoding = tiktoken.get_encoding(encoding_model or "cl100k_base")
 -         yes = encoding.encode("YES")
 -         no = encoding.encode("NO")
 -         self._loop_args = {"logit_bias": {yes[0]: 100, no[0]: 100}, "max_tokens": 1}
 - 
 -     def __call__(
 -         self, texts: list[str],
 -             prompt_variables: dict[str, Any] | None = None,
 -             callback: Callable | None = None
 -     ) -> GraphExtractionResult:
 -         """Call method definition."""
 -         if prompt_variables is None:
 -             prompt_variables = {}
 -         all_records: dict[int, str] = {}
 -         source_doc_map: dict[int, str] = {}
 - 
 -         # Wire defaults into the prompt variables
 -         prompt_variables = {
 -             **prompt_variables,
 -             self._tuple_delimiter_key: prompt_variables.get(self._tuple_delimiter_key)
 -             or DEFAULT_TUPLE_DELIMITER,
 -             self._record_delimiter_key: prompt_variables.get(self._record_delimiter_key)
 -             or DEFAULT_RECORD_DELIMITER,
 -             self._completion_delimiter_key: prompt_variables.get(
 -                 self._completion_delimiter_key
 -             )
 -             or DEFAULT_COMPLETION_DELIMITER,
 -             self._entity_types_key: ",".join(
 -                 prompt_variables.get(self._entity_types_key) or DEFAULT_ENTITY_TYPES
 -             ),
 -         }
 - 
 -         st = timer()
 -         total = len(texts)
 -         total_token_count = 0
 -         for doc_index, text in enumerate(texts):
 -             try:
 -                 # Invoke the entity extraction
 -                 result, token_count = self._process_document(text, prompt_variables)
 -                 source_doc_map[doc_index] = text
 -                 all_records[doc_index] = result
 -                 total_token_count += token_count
 -                 if callback: callback(msg=f"{doc_index+1}/{total}, elapsed: {timer() - st}s, used tokens: {total_token_count}")
 -             except Exception as e:
 -                 if callback: callback(msg="Knowledge graph extraction error:{}".format(str(e)))
 -                 logging.exception("error extracting graph")
 -                 self._on_error(
 -                     e,
 -                     traceback.format_exc(),
 -                     {
 -                         "doc_index": doc_index,
 -                         "text": text,
 -                     },
 -                 )
 - 
 -         output = self._process_results(
 -             all_records,
 -             prompt_variables.get(self._tuple_delimiter_key, DEFAULT_TUPLE_DELIMITER),
 -             prompt_variables.get(self._record_delimiter_key, DEFAULT_RECORD_DELIMITER),
 -         )
 - 
 -         return GraphExtractionResult(
 -             output=output,
 -             source_docs=source_doc_map,
 -         )
 - 
 -     def _process_document(
 -         self, text: str, prompt_variables: dict[str, str]
 -     ) -> str:
 -         variables = {
 -             **prompt_variables,
 -             self._input_text_key: text,
 -         }
 -         token_count = 0
 -         text = perform_variable_replacements(self._extraction_prompt, variables=variables)
 -         gen_conf = {"temperature": 0.3}
 -         response = self._llm.chat(text, [{"role": "user", "content": "Output:"}], gen_conf)
 -         if response.find("**ERROR**") >= 0: raise Exception(response)
 -         token_count = num_tokens_from_string(text + response)
 - 
 -         results = response or ""
 -         history = [{"role": "system", "content": text}, {"role": "assistant", "content": response}]
 - 
 -         # Repeat to ensure we maximize entity count
 -         for i in range(self._max_gleanings):
 -             text = perform_variable_replacements(CONTINUE_PROMPT, history=history, variables=variables)
 -             history.append({"role": "user", "content": text})
 -             response = self._llm.chat("", history, gen_conf)
 -             if response.find("**ERROR**") >=0: raise Exception(response)
 -             results += response or ""
 - 
 -             # if this is the final glean, don't bother updating the continuation flag
 -             if i >= self._max_gleanings - 1:
 -                 break
 -             history.append({"role": "assistant", "content": response})
 -             history.append({"role": "user", "content": LOOP_PROMPT})
 -             continuation = self._llm.chat("", history, self._loop_args)
 -             if continuation != "YES":
 -                 break
 - 
 -         return results, token_count
 - 
 -     def _process_results(
 -         self,
 -         results: dict[int, str],
 -         tuple_delimiter: str,
 -         record_delimiter: str,
 -     ) -> nx.Graph:
 -         """Parse the result string to create an undirected unipartite graph.
 - 
 -         Args:
 -             - results - dict of results from the extraction chain
 -             - tuple_delimiter - delimiter between tuples in an output record, default is '<|>'
 -             - record_delimiter - delimiter between records, default is '##'
 -         Returns:
 -             - output - unipartite graph in graphML format
 -         """
 -         graph = nx.Graph()
 -         for source_doc_id, extracted_data in results.items():
 -             records = [r.strip() for r in extracted_data.split(record_delimiter)]
 - 
 -             for record in records:
 -                 record = re.sub(r"^\(|\)$", "", record.strip())
 -                 record_attributes = record.split(tuple_delimiter)
 - 
 -                 if record_attributes[0] == '"entity"' and len(record_attributes) >= 4:
 -                     # add this record as a node in the G
 -                     entity_name = clean_str(record_attributes[1].upper())
 -                     entity_type = clean_str(record_attributes[2].upper())
 -                     entity_description = clean_str(record_attributes[3])
 - 
 -                     if entity_name in graph.nodes():
 -                         node = graph.nodes[entity_name]
 -                         if self._join_descriptions:
 -                             node["description"] = "\n".join(
 -                                 list({
 -                                     *_unpack_descriptions(node),
 -                                     entity_description,
 -                                 })
 -                             )
 -                         else:
 -                             if len(entity_description) > len(node["description"]):
 -                                 node["description"] = entity_description
 -                         node["source_id"] = ", ".join(
 -                             list({
 -                                 *_unpack_source_ids(node),
 -                                 str(source_doc_id),
 -                             })
 -                         )
 -                         node["entity_type"] = (
 -                             entity_type if entity_type != "" else node["entity_type"]
 -                         )
 -                     else:
 -                         graph.add_node(
 -                             entity_name,
 -                             entity_type=entity_type,
 -                             description=entity_description,
 -                             source_id=str(source_doc_id),
 -                             weight=1
 -                         )
 - 
 -                 if (
 -                     record_attributes[0] == '"relationship"'
 -                     and len(record_attributes) >= 5
 -                 ):
 -                     # add this record as edge
 -                     source = clean_str(record_attributes[1].upper())
 -                     target = clean_str(record_attributes[2].upper())
 -                     edge_description = clean_str(record_attributes[3])
 -                     edge_source_id = clean_str(str(source_doc_id))
 -                     weight = (
 -                         float(record_attributes[-1])
 -                         if isinstance(record_attributes[-1], numbers.Number)
 -                         else 1.0
 -                     )
 -                     if source not in graph.nodes():
 -                         graph.add_node(
 -                             source,
 -                             entity_type="",
 -                             description="",
 -                             source_id=edge_source_id,
 -                             weight=1
 -                         )
 -                     if target not in graph.nodes():
 -                         graph.add_node(
 -                             target,
 -                             entity_type="",
 -                             description="",
 -                             source_id=edge_source_id,
 -                             weight=1
 -                         )
 -                     if graph.has_edge(source, target):
 -                         edge_data = graph.get_edge_data(source, target)
 -                         if edge_data is not None:
 -                             weight += edge_data["weight"]
 -                             if self._join_descriptions:
 -                                 edge_description = "\n".join(
 -                                     list({
 -                                         *_unpack_descriptions(edge_data),
 -                                         edge_description,
 -                                     })
 -                                 )
 -                             edge_source_id = ", ".join(
 -                                 list({
 -                                     *_unpack_source_ids(edge_data),
 -                                     str(source_doc_id),
 -                                 })
 -                             )
 -                     graph.add_edge(
 -                         source,
 -                         target,
 -                         weight=weight,
 -                         description=edge_description,
 -                         source_id=edge_source_id,
 -                     )
 - 
 -         for node_degree in graph.degree:
 -             graph.nodes[str(node_degree[0])]["rank"] = int(node_degree[1])
 -         return graph
 - 
 - 
 - def _unpack_descriptions(data: Mapping) -> list[str]:
 -     value = data.get("description", None)
 -     return [] if value is None else value.split("\n")
 - 
 - 
 - def _unpack_source_ids(data: Mapping) -> list[str]:
 -     value = data.get("source_id", None)
 -     return [] if value is None else value.split(", ")
 - 
 - 
 
 
  |