extraction_accuracy.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. from __future__ import annotations
  2. import json
  3. import re
  4. import unicodedata
  5. from dataclasses import dataclass
  6. from typing import Any
  7. from app.agents.runtime import get_agent_runtime
  8. @dataclass
  9. class EditJudgement:
  10. is_extraction_error: bool
  11. reason: str
  12. confidence: float
  13. source: str
  14. def _parse_json_object(text: str) -> dict[str, Any] | None:
  15. cleaned = text.strip()
  16. if cleaned.startswith("```"):
  17. cleaned = re.sub(r"^```(?:json)?\s*", "", cleaned)
  18. cleaned = re.sub(r"\s*```$", "", cleaned)
  19. start = cleaned.find("{")
  20. end = cleaned.rfind("}")
  21. if start < 0 or end <= start:
  22. return None
  23. try:
  24. value = json.loads(cleaned[start : end + 1])
  25. except json.JSONDecodeError:
  26. return None
  27. return value if isinstance(value, dict) else None
  28. def _normalize_text(value: str) -> str:
  29. normalized = unicodedata.normalize("NFKC", str(value or "")).lower()
  30. return re.sub(r"[\s,。;、,.;::!!??\"'“”‘’()()\[\]【】]+", "", normalized)
  31. def _token_set(value: str) -> set[str]:
  32. return {
  33. token
  34. for token in re.split(r"[\s,。;、,.;::|/]+", str(value or "").strip())
  35. if token
  36. }
  37. def _fallback_judgement(
  38. original_value: str,
  39. edited_value: str,
  40. ) -> EditJudgement:
  41. original_normalized = _normalize_text(original_value)
  42. edited_normalized = _normalize_text(edited_value)
  43. if original_normalized == edited_normalized:
  44. return EditJudgement(
  45. is_extraction_error=False,
  46. reason="修改仅涉及空格、标点或显示格式,不判定为抽取错误。",
  47. confidence=0.95,
  48. source="fallback",
  49. )
  50. if (
  51. original_normalized
  52. and edited_normalized
  53. and (
  54. original_normalized in edited_normalized
  55. or edited_normalized in original_normalized
  56. )
  57. ):
  58. return EditJudgement(
  59. is_extraction_error=False,
  60. reason="修改主要属于内容截断、补充说明或显示长度调整,不判定为抽取错误。",
  61. confidence=0.8,
  62. source="fallback",
  63. )
  64. original_tokens = _token_set(original_value)
  65. edited_tokens = _token_set(edited_value)
  66. if original_tokens and original_tokens == edited_tokens:
  67. return EditJudgement(
  68. is_extraction_error=False,
  69. reason="字段内容仅调整顺序或分隔方式,不判定为抽取错误。",
  70. confidence=0.85,
  71. source="fallback",
  72. )
  73. return EditJudgement(
  74. is_extraction_error=True,
  75. reason="字段值发生了实质变化,暂按原抽取结果错误记录。",
  76. confidence=0.55,
  77. source="fallback",
  78. )
  79. def _normalize_model_confidence(value: Any) -> float:
  80. try:
  81. number = float(value)
  82. except (TypeError, ValueError):
  83. return 0.7
  84. if number > 1:
  85. number /= 100
  86. return max(0.0, min(number, 1.0))
  87. async def judge_extraction_edit(
  88. *,
  89. field_name: str,
  90. original_value: str,
  91. edited_value: str,
  92. evidence_text: str = "",
  93. document_title: str = "",
  94. extraction_id: str = "",
  95. ) -> EditJudgement:
  96. fallback = _fallback_judgement(original_value, edited_value)
  97. runtime = get_agent_runtime()
  98. if not runtime.enabled:
  99. return fallback
  100. prompt = f"""
  101. 你是煤矿业务资料字段抽取质量判定器。用户修改了系统抽取出的字段值,
  102. 请判断这次修改是否说明“原抽取结果存在抽取错误”。
  103. 判定规则:
  104. 1. 如果用户只是删除多余内容、截断过长文本、调整换行或空格、统一标点、调整顺序、
  105. 简化表达、补充说明或改变显示格式,则应判定为“非抽取错误”。
  106. 2. 如果用户修正了实质性的数值、日期、单位、名称、地点、结论或字段含义,
  107. 则应判定为“抽取错误”。
  108. 3. 只判断修改原因是否属于抽取错误,不要评价修改后的文字是否更好。
  109. 资料标题:{document_title or "未提供"}
  110. 字段名称:{field_name}
  111. 原抽取值:{original_value[:1200]}
  112. 用户修改后:{edited_value[:1200]}
  113. 原文证据:{evidence_text[:1200] or "未提供"}
  114. 只输出 JSON:
  115. {{"is_extraction_error": true, "reason": "简短中文理由", "confidence": 0.9}}
  116. """.strip()
  117. result = await runtime.invoke(
  118. message=prompt,
  119. thread_id=f"extraction-edit-judgement-{extraction_id or field_name}",
  120. context={"purpose": "field_edit_accuracy"},
  121. )
  122. if result.status != "success":
  123. return fallback
  124. parsed = _parse_json_object(result.output)
  125. if parsed is None or "is_extraction_error" not in parsed:
  126. return fallback
  127. raw_flag = parsed.get("is_extraction_error")
  128. if isinstance(raw_flag, str):
  129. is_extraction_error = raw_flag.strip().lower() in {"true", "yes", "1", "是"}
  130. else:
  131. is_extraction_error = bool(raw_flag)
  132. return EditJudgement(
  133. is_extraction_error=is_extraction_error,
  134. reason=str(parsed.get("reason") or ("属于抽取错误" if is_extraction_error else "不属于抽取错误")),
  135. confidence=_normalize_model_confidence(parsed.get("confidence")),
  136. source="llm",
  137. )