hybrid_extractor.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. """
  2. Hybrid extractor with mode switching.
  3. Supports: rules, bert, ollama, hybrid (rules + BERT + Ollama with field-type routing).
  4. """
  5. from __future__ import annotations
  6. from typing import Any
  7. import requests
  8. from app.config import settings
  9. from app.extractors.rule_extractor import RuleBasedExtractor
  10. class HybridExtractor:
  11. """
  12. Multi-mode extractor for labor arbitration case elements.
  13. Modes:
  14. - "rules": Rule-based regex extraction only (fast, no GPU needed)
  15. - "bert": BERT multi-task model via nlp-service (deep learning)
  16. - "ollama": Qwen 2.5 via Ollama (LLM zero-shot)
  17. - "hybrid": Rules for structured fields + BERT for entities + Ollama for free text
  18. """
  19. def __init__(self, mode: str | None = None):
  20. self._mode = mode or settings.extractor_mode or "rules"
  21. self._rule = RuleBasedExtractor()
  22. self._ollama = None
  23. # Init Ollama if needed
  24. if self._mode in ("ollama", "hybrid") and settings.use_ollama:
  25. try:
  26. from app.anj import OllamaClaimsExtractor
  27. self._ollama = OllamaClaimsExtractor(
  28. settings.ollama_base_url,
  29. settings.ollama_model_name,
  30. )
  31. except Exception:
  32. self._ollama = None
  33. def extract(self, text: str) -> dict[str, Any]:
  34. if self._mode == "rules":
  35. return self._extract_rules(text)
  36. elif self._mode == "bert":
  37. return self._extract_bert(text)
  38. elif self._mode == "ollama":
  39. return self._extract_ollama(text)
  40. elif self._mode == "hybrid":
  41. return self._extract_hybrid(text)
  42. else:
  43. return self._extract_rules(text)
  44. def _extract_rules(self, text: str) -> dict[str, Any]:
  45. return self._rule.extract_all(text)
  46. def _extract_bert(self, text: str) -> dict[str, Any]:
  47. """Call the nlp-service BERT model for extraction."""
  48. try:
  49. resp = requests.post(
  50. settings.bert_model_service_url,
  51. json={"text": text},
  52. timeout=60,
  53. )
  54. resp.raise_for_status()
  55. raw = resp.json()
  56. # Flatten NLP nested response to backend flat format
  57. flat = {}
  58. parties = raw.get("parties", {})
  59. for k, v in parties.items():
  60. if v is not None:
  61. flat[k] = v
  62. facts = raw.get("facts", {})
  63. for k, v in facts.items():
  64. if v is not None:
  65. flat[k] = v
  66. if raw.get("case_cause", {}).get("type"):
  67. flat["case_cause"] = raw["case_cause"]["type"]
  68. if raw.get("tmpl_primary_cause"):
  69. flat["primary_cause_type"] = raw.get("tmpl_primary_cause")
  70. if raw.get("claims", {}).get("amount_total") is not None:
  71. flat["claims"] = raw["claims"]
  72. if raw.get("contract_type"):
  73. flat["contract_type"] = raw.get("contract_type")
  74. if raw.get("law_refs"):
  75. flat["law_refs"] = raw["law_refs"]
  76. return flat
  77. except Exception:
  78. # Fallback to rules
  79. return self._extract_rules(text)
  80. def _extract_ollama(self, text: str) -> dict[str, Any]:
  81. """Use Ollama LLM for extraction, with rule fallback for structured fields."""
  82. base = self._rule.extract_all(text)
  83. if self._ollama is None:
  84. return base
  85. try:
  86. # Enhance claims with Ollama
  87. base["claims"] = self._ollama.extract_claims(text or "")
  88. except Exception:
  89. pass
  90. try:
  91. # Enhance template fields with Ollama
  92. template_fields = self._ollama.extract_dispute_template_fields(text or "")
  93. for k, v in template_fields.items():
  94. if v is not None and v != "":
  95. base[k] = v
  96. except Exception:
  97. pass
  98. return base
  99. def _extract_hybrid(self, text: str) -> dict[str, Any]:
  100. """
  101. Hybrid extraction with field-type routing:
  102. - Structured fields (case_number, dates, amounts): rules
  103. - Entity fields (names, positions): BERT model
  104. - Free text fields (facts, claims): Ollama LLM
  105. """
  106. # Start with rules for structured fields
  107. base = self._rule.extract_all(text)
  108. # Try BERT for entity fields
  109. try:
  110. resp = requests.post(
  111. settings.bert_model_service_url,
  112. json={"text": text},
  113. timeout=60,
  114. )
  115. if resp.status_code == 200:
  116. bert_result = resp.json()
  117. # Merge entity fields from BERT
  118. entity_fields = [
  119. "applicant_name", "respondent_name", "worker_position",
  120. "entry_date", "leave_date", "filing_date",
  121. "termination_reason", "arbitration_org",
  122. ]
  123. bert_parties = bert_result.get("parties", {})
  124. for field in entity_fields:
  125. if field in bert_parties and bert_parties[field]:
  126. base[field] = bert_parties[field]
  127. bert_facts = bert_result.get("facts", {})
  128. for field in entity_fields:
  129. if field in bert_facts and bert_facts[field]:
  130. base[field] = bert_facts[field]
  131. # Use BERT case cause if confident
  132. bert_cause = bert_result.get("case_cause", {}).get("type")
  133. if bert_cause and bert_cause != "劳动争议":
  134. base["case_cause"] = bert_cause
  135. except Exception:
  136. pass
  137. # Try Ollama for free text fields
  138. if self._ollama is not None:
  139. try:
  140. base["claims"] = self._ollama.extract_claims(text or "")
  141. except Exception:
  142. pass
  143. try:
  144. template_fields = self._ollama.extract_dispute_template_fields(text or "")
  145. for k, v in template_fields.items():
  146. if v is not None and v != "":
  147. base[k] = v
  148. except Exception:
  149. pass
  150. return base
  151. @property
  152. def mode(self) -> str:
  153. return self._mode
  154. @mode.setter
  155. def mode(self, value: str):
  156. self._mode = value
  157. if value in ("ollama", "hybrid") and self._ollama is None and settings.use_ollama:
  158. try:
  159. from app.anj import OllamaClaimsExtractor
  160. self._ollama = OllamaClaimsExtractor(
  161. settings.ollama_base_url,
  162. settings.ollama_model_name,
  163. )
  164. except Exception:
  165. pass