Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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 logging
  8. import argparse
  9. import json
  10. import re
  11. import traceback
  12. from dataclasses import dataclass
  13. from typing import Any
  14. import tiktoken
  15. from graphrag.claim_prompt import CLAIM_EXTRACTION_PROMPT, CONTINUE_PROMPT, LOOP_PROMPT
  16. from graphrag.extractor import Extractor
  17. from rag.llm.chat_model import Base as CompletionLLM
  18. from graphrag.utils import ErrorHandlerFn, perform_variable_replacements
  19. DEFAULT_TUPLE_DELIMITER = "<|>"
  20. DEFAULT_RECORD_DELIMITER = "##"
  21. DEFAULT_COMPLETION_DELIMITER = "<|COMPLETE|>"
  22. CLAIM_MAX_GLEANINGS = 1
  23. @dataclass
  24. class ClaimExtractorResult:
  25. """Claim extractor result class definition."""
  26. output: list[dict]
  27. source_docs: dict[str, Any]
  28. class ClaimExtractor(Extractor):
  29. """Claim extractor class definition."""
  30. _extraction_prompt: str
  31. _summary_prompt: str
  32. _output_formatter_prompt: str
  33. _input_text_key: str
  34. _input_entity_spec_key: str
  35. _input_claim_description_key: str
  36. _tuple_delimiter_key: str
  37. _record_delimiter_key: str
  38. _completion_delimiter_key: str
  39. _max_gleanings: int
  40. _on_error: ErrorHandlerFn
  41. def __init__(
  42. self,
  43. llm_invoker: CompletionLLM,
  44. extraction_prompt: str | None = None,
  45. input_text_key: str | None = None,
  46. input_entity_spec_key: str | None = None,
  47. input_claim_description_key: str | None = None,
  48. input_resolved_entities_key: str | None = None,
  49. tuple_delimiter_key: str | None = None,
  50. record_delimiter_key: str | None = None,
  51. completion_delimiter_key: str | None = None,
  52. encoding_model: str | None = None,
  53. max_gleanings: int | None = None,
  54. on_error: ErrorHandlerFn | None = None,
  55. ):
  56. """Init method definition."""
  57. self._llm = llm_invoker
  58. self._extraction_prompt = extraction_prompt or CLAIM_EXTRACTION_PROMPT
  59. self._input_text_key = input_text_key or "input_text"
  60. self._input_entity_spec_key = input_entity_spec_key or "entity_specs"
  61. self._tuple_delimiter_key = tuple_delimiter_key or "tuple_delimiter"
  62. self._record_delimiter_key = record_delimiter_key or "record_delimiter"
  63. self._completion_delimiter_key = (
  64. completion_delimiter_key or "completion_delimiter"
  65. )
  66. self._input_claim_description_key = (
  67. input_claim_description_key or "claim_description"
  68. )
  69. self._input_resolved_entities_key = (
  70. input_resolved_entities_key or "resolved_entities"
  71. )
  72. self._max_gleanings = (
  73. max_gleanings if max_gleanings is not None else CLAIM_MAX_GLEANINGS
  74. )
  75. self._on_error = on_error or (lambda _e, _s, _d: None)
  76. # Construct the looping arguments
  77. encoding = tiktoken.get_encoding(encoding_model or "cl100k_base")
  78. yes = encoding.encode("YES")
  79. no = encoding.encode("NO")
  80. self._loop_args = {"logit_bias": {yes[0]: 100, no[0]: 100}, "max_tokens": 1}
  81. def __call__(
  82. self, inputs: dict[str, Any], prompt_variables: dict | None = None
  83. ) -> ClaimExtractorResult:
  84. """Call method definition."""
  85. if prompt_variables is None:
  86. prompt_variables = {}
  87. texts = inputs[self._input_text_key]
  88. entity_spec = str(inputs[self._input_entity_spec_key])
  89. claim_description = inputs[self._input_claim_description_key]
  90. resolved_entities = inputs.get(self._input_resolved_entities_key, {})
  91. source_doc_map = {}
  92. prompt_args = {
  93. self._input_entity_spec_key: entity_spec,
  94. self._input_claim_description_key: claim_description,
  95. self._tuple_delimiter_key: prompt_variables.get(self._tuple_delimiter_key)
  96. or DEFAULT_TUPLE_DELIMITER,
  97. self._record_delimiter_key: prompt_variables.get(self._record_delimiter_key)
  98. or DEFAULT_RECORD_DELIMITER,
  99. self._completion_delimiter_key: prompt_variables.get(
  100. self._completion_delimiter_key
  101. )
  102. or DEFAULT_COMPLETION_DELIMITER,
  103. }
  104. all_claims: list[dict] = []
  105. for doc_index, text in enumerate(texts):
  106. document_id = f"d{doc_index}"
  107. try:
  108. claims = self._process_document(prompt_args, text, doc_index)
  109. all_claims += [
  110. self._clean_claim(c, document_id, resolved_entities) for c in claims
  111. ]
  112. source_doc_map[document_id] = text
  113. except Exception as e:
  114. logging.exception("error extracting claim")
  115. self._on_error(
  116. e,
  117. traceback.format_exc(),
  118. {"doc_index": doc_index, "text": text},
  119. )
  120. continue
  121. return ClaimExtractorResult(
  122. output=all_claims,
  123. source_docs=source_doc_map,
  124. )
  125. def _clean_claim(
  126. self, claim: dict, document_id: str, resolved_entities: dict
  127. ) -> dict:
  128. # clean the parsed claims to remove any claims with status = False
  129. obj = claim.get("object_id", claim.get("object"))
  130. subject = claim.get("subject_id", claim.get("subject"))
  131. # If subject or object in resolved entities, then replace with resolved entity
  132. obj = resolved_entities.get(obj, obj)
  133. subject = resolved_entities.get(subject, subject)
  134. claim["object_id"] = obj
  135. claim["subject_id"] = subject
  136. claim["doc_id"] = document_id
  137. return claim
  138. def _process_document(
  139. self, prompt_args: dict, doc, doc_index: int
  140. ) -> list[dict]:
  141. record_delimiter = prompt_args.get(
  142. self._record_delimiter_key, DEFAULT_RECORD_DELIMITER
  143. )
  144. completion_delimiter = prompt_args.get(
  145. self._completion_delimiter_key, DEFAULT_COMPLETION_DELIMITER
  146. )
  147. variables = {
  148. self._input_text_key: doc,
  149. **prompt_args,
  150. }
  151. text = perform_variable_replacements(self._extraction_prompt, variables=variables)
  152. gen_conf = {"temperature": 0.5}
  153. results = self._chat(text, [{"role": "user", "content": "Output:"}], gen_conf)
  154. claims = results.strip().removesuffix(completion_delimiter)
  155. history = [{"role": "system", "content": text}, {"role": "assistant", "content": results}]
  156. # Repeat to ensure we maximize entity count
  157. for i in range(self._max_gleanings):
  158. text = perform_variable_replacements(CONTINUE_PROMPT, history=history, variables=variables)
  159. history.append({"role": "user", "content": text})
  160. extension = self._chat("", history, gen_conf)
  161. claims += record_delimiter + extension.strip().removesuffix(
  162. completion_delimiter
  163. )
  164. # If this isn't the last loop, check to see if we should continue
  165. if i >= self._max_gleanings - 1:
  166. break
  167. history.append({"role": "assistant", "content": extension})
  168. history.append({"role": "user", "content": LOOP_PROMPT})
  169. continuation = self._chat("", history, self._loop_args)
  170. if continuation != "YES":
  171. break
  172. result = self._parse_claim_tuples(claims, prompt_args)
  173. for r in result:
  174. r["doc_id"] = f"{doc_index}"
  175. return result
  176. def _parse_claim_tuples(
  177. self, claims: str, prompt_variables: dict
  178. ) -> list[dict[str, Any]]:
  179. """Parse claim tuples."""
  180. record_delimiter = prompt_variables.get(
  181. self._record_delimiter_key, DEFAULT_RECORD_DELIMITER
  182. )
  183. completion_delimiter = prompt_variables.get(
  184. self._completion_delimiter_key, DEFAULT_COMPLETION_DELIMITER
  185. )
  186. tuple_delimiter = prompt_variables.get(
  187. self._tuple_delimiter_key, DEFAULT_TUPLE_DELIMITER
  188. )
  189. def pull_field(index: int, fields: list[str]) -> str | None:
  190. return fields[index].strip() if len(fields) > index else None
  191. result: list[dict[str, Any]] = []
  192. claims_values = (
  193. claims.strip().removesuffix(completion_delimiter).split(record_delimiter)
  194. )
  195. for claim in claims_values:
  196. claim = claim.strip().removeprefix("(").removesuffix(")")
  197. claim = re.sub(r".*Output:", "", claim)
  198. # Ignore the completion delimiter
  199. if claim == completion_delimiter:
  200. continue
  201. claim_fields = claim.split(tuple_delimiter)
  202. o = {
  203. "subject_id": pull_field(0, claim_fields),
  204. "object_id": pull_field(1, claim_fields),
  205. "type": pull_field(2, claim_fields),
  206. "status": pull_field(3, claim_fields),
  207. "start_date": pull_field(4, claim_fields),
  208. "end_date": pull_field(5, claim_fields),
  209. "description": pull_field(6, claim_fields),
  210. "source_text": pull_field(7, claim_fields),
  211. "doc_id": pull_field(8, claim_fields),
  212. }
  213. if any([not o["subject_id"], not o["object_id"], o["subject_id"].lower() == "none", o["object_id"] == "none"]):
  214. continue
  215. result.append(o)
  216. return result
  217. if __name__ == "__main__":
  218. parser = argparse.ArgumentParser()
  219. parser.add_argument('-t', '--tenant_id', default=False, help="Tenant ID", action='store', required=True)
  220. parser.add_argument('-d', '--doc_id', default=False, help="Document ID", action='store', required=True)
  221. args = parser.parse_args()
  222. from api.db import LLMType
  223. from api.db.services.llm_service import LLMBundle
  224. from api import settings
  225. from api.db.services.knowledgebase_service import KnowledgebaseService
  226. kb_ids = KnowledgebaseService.get_kb_ids(args.tenant_id)
  227. ex = ClaimExtractor(LLMBundle(args.tenant_id, LLMType.CHAT))
  228. docs = [d["content_with_weight"] for d in settings.retrievaler.chunk_list(args.doc_id, args.tenant_id, kb_ids, max_count=12, fields=["content_with_weight"])]
  229. info = {
  230. "input_text": docs,
  231. "entity_specs": "organization, person",
  232. "claim_description": ""
  233. }
  234. claim = ex(info)
  235. logging.info(json.dumps(claim.output, ensure_ascii=False, indent=2))