您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

graph_extractor.py 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. # Copyright (c) 2024 Microsoft Corporation.
  2. # Licensed under the MIT License
  3. """
  4. Reference:
  5. - [graphrag](https://github.com/microsoft/graphrag)
  6. """
  7. import re
  8. from typing import Any, Callable
  9. from dataclasses import dataclass
  10. import tiktoken
  11. import trio
  12. from graphrag.general.extractor import Extractor, ENTITY_EXTRACTION_MAX_GLEANINGS, DEFAULT_ENTITY_TYPES
  13. from graphrag.general.graph_prompt import GRAPH_EXTRACTION_PROMPT, CONTINUE_PROMPT, LOOP_PROMPT
  14. from graphrag.utils import ErrorHandlerFn, perform_variable_replacements, chat_limiter
  15. from rag.llm.chat_model import Base as CompletionLLM
  16. import networkx as nx
  17. from rag.utils import num_tokens_from_string
  18. DEFAULT_TUPLE_DELIMITER = "<|>"
  19. DEFAULT_RECORD_DELIMITER = "##"
  20. DEFAULT_COMPLETION_DELIMITER = "<|COMPLETE|>"
  21. @dataclass
  22. class GraphExtractionResult:
  23. """Unipartite graph extraction result class definition."""
  24. output: nx.Graph
  25. source_docs: dict[Any, Any]
  26. class GraphExtractor(Extractor):
  27. """Unipartite graph extractor class definition."""
  28. _join_descriptions: bool
  29. _tuple_delimiter_key: str
  30. _record_delimiter_key: str
  31. _entity_types_key: str
  32. _input_text_key: str
  33. _completion_delimiter_key: str
  34. _entity_name_key: str
  35. _input_descriptions_key: str
  36. _extraction_prompt: str
  37. _summarization_prompt: str
  38. _loop_args: dict[str, Any]
  39. _max_gleanings: int
  40. _on_error: ErrorHandlerFn
  41. def __init__(
  42. self,
  43. llm_invoker: CompletionLLM,
  44. language: str | None = "English",
  45. entity_types: list[str] | None = None,
  46. get_entity: Callable | None = None,
  47. set_entity: Callable | None = None,
  48. get_relation: Callable | None = None,
  49. set_relation: Callable | None = None,
  50. tuple_delimiter_key: str | None = None,
  51. record_delimiter_key: str | None = None,
  52. input_text_key: str | None = None,
  53. entity_types_key: str | None = None,
  54. completion_delimiter_key: str | None = None,
  55. join_descriptions=True,
  56. max_gleanings: int | None = None,
  57. on_error: ErrorHandlerFn | None = None,
  58. ):
  59. super().__init__(llm_invoker, language, entity_types, get_entity, set_entity, get_relation, set_relation)
  60. """Init method definition."""
  61. # TODO: streamline construction
  62. self._llm = llm_invoker
  63. self._join_descriptions = join_descriptions
  64. self._input_text_key = input_text_key or "input_text"
  65. self._tuple_delimiter_key = tuple_delimiter_key or "tuple_delimiter"
  66. self._record_delimiter_key = record_delimiter_key or "record_delimiter"
  67. self._completion_delimiter_key = (
  68. completion_delimiter_key or "completion_delimiter"
  69. )
  70. self._entity_types_key = entity_types_key or "entity_types"
  71. self._extraction_prompt = GRAPH_EXTRACTION_PROMPT
  72. self._max_gleanings = (
  73. max_gleanings
  74. if max_gleanings is not None
  75. else ENTITY_EXTRACTION_MAX_GLEANINGS
  76. )
  77. self._on_error = on_error or (lambda _e, _s, _d: None)
  78. self.prompt_token_count = num_tokens_from_string(self._extraction_prompt)
  79. # Construct the looping arguments
  80. encoding = tiktoken.get_encoding("cl100k_base")
  81. yes = encoding.encode("YES")
  82. no = encoding.encode("NO")
  83. self._loop_args = {"logit_bias": {yes[0]: 100, no[0]: 100}, "max_tokens": 1}
  84. # Wire defaults into the prompt variables
  85. self._prompt_variables = {
  86. "entity_types": entity_types,
  87. self._tuple_delimiter_key: DEFAULT_TUPLE_DELIMITER,
  88. self._record_delimiter_key: DEFAULT_RECORD_DELIMITER,
  89. self._completion_delimiter_key: DEFAULT_COMPLETION_DELIMITER,
  90. self._entity_types_key: ",".join(DEFAULT_ENTITY_TYPES),
  91. }
  92. async def _process_single_content(self, chunk_key_dp: tuple[str, str], chunk_seq: int, num_chunks: int, out_results):
  93. token_count = 0
  94. chunk_key = chunk_key_dp[0]
  95. content = chunk_key_dp[1]
  96. variables = {
  97. **self._prompt_variables,
  98. self._input_text_key: content,
  99. }
  100. gen_conf = {"temperature": 0.3}
  101. hint_prompt = perform_variable_replacements(self._extraction_prompt, variables=variables)
  102. async with chat_limiter:
  103. response = await trio.to_thread.run_sync(lambda: self._chat(hint_prompt, [{"role": "user", "content": "Output:"}], gen_conf))
  104. token_count += num_tokens_from_string(hint_prompt + response)
  105. results = response or ""
  106. history = [{"role": "system", "content": hint_prompt}, {"role": "user", "content": response}]
  107. # Repeat to ensure we maximize entity count
  108. for i in range(self._max_gleanings):
  109. text = perform_variable_replacements(CONTINUE_PROMPT, history=history, variables=variables)
  110. history.append({"role": "user", "content": text})
  111. async with chat_limiter:
  112. response = await trio.to_thread.run_sync(lambda: self._chat("", history, gen_conf))
  113. token_count += num_tokens_from_string("\n".join([m["content"] for m in history]) + response)
  114. results += response or ""
  115. # if this is the final glean, don't bother updating the continuation flag
  116. if i >= self._max_gleanings - 1:
  117. break
  118. history.append({"role": "assistant", "content": response})
  119. history.append({"role": "user", "content": LOOP_PROMPT})
  120. async with chat_limiter:
  121. continuation = await trio.to_thread.run_sync(lambda: self._chat("", history, {"temperature": 0.8}))
  122. token_count += num_tokens_from_string("\n".join([m["content"] for m in history]) + response)
  123. if continuation != "YES":
  124. break
  125. record_delimiter = variables.get(self._record_delimiter_key, DEFAULT_RECORD_DELIMITER)
  126. tuple_delimiter = variables.get(self._tuple_delimiter_key, DEFAULT_TUPLE_DELIMITER)
  127. records = [re.sub(r"^\(|\)$", "", r.strip()) for r in results.split(record_delimiter)]
  128. records = [r for r in records if r.strip()]
  129. maybe_nodes, maybe_edges = self._entities_and_relations(chunk_key, records, tuple_delimiter)
  130. out_results.append((maybe_nodes, maybe_edges, token_count))
  131. if self.callback:
  132. self.callback(0.5+0.1*len(out_results)/num_chunks, msg = f"Entities extraction of chunk {chunk_seq} {len(out_results)}/{num_chunks} done, {len(maybe_nodes)} nodes, {len(maybe_edges)} edges, {token_count} tokens.")