Refine AI review false-positive rules
This commit is contained in:
@@ -2346,8 +2346,11 @@ def build_ai_prompt(record: dict[str, Any], pdf_context: dict[str, Any], privacy
|
||||
7. 手术操作名称可能因为换行被结构化解析截断;如果 PDF定位文本或局部截图中显示完整多行名称,请把完整名称放入 suggested_updates。
|
||||
8. 门急诊诊断编码只能用于 outpatient_diagnosis_code;主要诊断请修正 discharge_diagnoses 中“诊断类别=主要诊断”的行,不要把门急诊诊断编码复制到 discharge_diagnoses[].疾病编码,除非出院诊断表格对应“疾病编码”单元格本身清楚显示该编码。
|
||||
9. PDF局部截图可能因遮挡、截屏边界或缩放只显示编码/文字前半段;如果 PDF 显示值是结构化字段值的前缀,且没有相反证据,应判为 match/ok,不要写“需确认完整编码”,也不要建议把结构化字段截短。
|
||||
10. remaining_issues 只写当前文档复核人应该特别注意的内容;不要写如何修改,不要重复 suggested_updates,不要写“无需补录/无需处理/首页原貌”这类已判定无问题的说明。
|
||||
11. 不要编造 PDF 中看不见的内容,不要输出置信度。
|
||||
10. 入院途径、离院方式、入院病情等代码字段只核对代码本身;例如 PDF 显示“2(门诊)”而结构化字段为“2”就是一致,不要要求复核代码与中文标签关系。
|
||||
11. 手术操作日期只有在日历日期确实早于入院日期时才算问题;同日或晚于入院日期均为正常,不要推测月份应改为其他月份。
|
||||
12. 手术及操作编码允许带 x 和 001/002/005/006 等扩展后缀;如果原始内容显示“54.5100x ... 005”这类拆开的后缀,应建议写入完整编码“54.5100x005”,不要要求人工确认扩展码是否有效。
|
||||
13. remaining_issues 只写当前文档复核人应该特别注意的内容;不要写如何修改,不要重复 suggested_updates,不要写“无需补录/无需处理/首页原貌”这类已判定无问题的说明。
|
||||
14. 不要编造 PDF 中看不见的内容,不要输出置信度。
|
||||
|
||||
必须返回这个 JSON 结构:
|
||||
{{
|
||||
@@ -2560,6 +2563,128 @@ def ai_pdf_prefix_truncation_issue(value: Any) -> bool:
|
||||
return ai_text_pdf_prefix_issue(value)
|
||||
|
||||
|
||||
def ai_extract_leading_code(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
text = text.replace("(", "(").replace(")", ")").replace(":", ":")
|
||||
match = re.match(r"^\s*([A-Za-z]?\d+(?:[.\-xX]\d+)*)", text)
|
||||
return ai_compact_compare_value(match.group(1)) if match else ""
|
||||
|
||||
|
||||
def ai_code_label_text(value: Any) -> bool:
|
||||
text = str(value or "")
|
||||
return bool(re.search(r"(代码|编码|入院途径|离院方式|入院病情|[A-Za-z_][A-Za-z0-9_]*_code)", text, flags=re.IGNORECASE))
|
||||
|
||||
|
||||
def ai_code_label_values_match(pdf_value: Any, structured_value: Any) -> bool:
|
||||
pdf_code = ai_extract_leading_code(pdf_value)
|
||||
structured_code = ai_extract_leading_code(structured_value)
|
||||
if not pdf_code or not structured_code or pdf_code != structured_code:
|
||||
return False
|
||||
pdf_text = str(pdf_value or "")
|
||||
structured_text = str(structured_value or "")
|
||||
return pdf_text != structured_text and (
|
||||
bool(re.search(r"[((][^))]{1,12}[))]", pdf_text)) or ai_code_label_text(pdf_text + structured_text)
|
||||
)
|
||||
|
||||
|
||||
def ai_text_code_label_consistent_issue(text: Any) -> bool:
|
||||
raw = str(text or "")
|
||||
if not raw or "PDF" not in raw or not ai_code_label_text(raw):
|
||||
return False
|
||||
if re.search(r"(缺失|为空|未填|未填写|漏填)", raw):
|
||||
return False
|
||||
pdf_match = re.search(
|
||||
r"PDF[^,,;;。]*?(?:显示|勾选|可见|值)?[^,,;;。]*?(?:为|是|=)?\s*[\"'“”‘’]?([A-Za-z]?\d+(?:[.\-xX]\d+)*)(?:[))\"'“”‘’]|[((][^))]{1,12}[))])?",
|
||||
raw,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
structured_match = re.search(
|
||||
r"(?:结构化字段|结构化值|结构化|[A-Za-z_][A-Za-z0-9_]*_code|[A-Za-z_][A-Za-z0-9_]*字段值|字段值)[^,,;;。]*?(?:是否为|为|是|=)?\s*[\"'“”‘’]?([A-Za-z]?\d+(?:[.\-xX]\d+)*)",
|
||||
raw,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if structured_match and not pdf_match and re.search(r"(与PDF一致|与PDF相符)", raw):
|
||||
return True
|
||||
if pdf_match and not structured_match and re.search(r"(与结构化一致|与PDF一致|是否与结构化一致|是否匹配|是否正确)", raw):
|
||||
return True
|
||||
if not pdf_match or not structured_match:
|
||||
return False
|
||||
return ai_compact_compare_value(pdf_match.group(1)) == ai_compact_compare_value(structured_match.group(1))
|
||||
|
||||
|
||||
def ai_code_label_consistent_issue(value: Any) -> bool:
|
||||
if isinstance(value, dict):
|
||||
pdf_value = value.get("pdf_value") or value.get("pdf") or value.get("PDF值") or value.get("图片值")
|
||||
structured_value = (
|
||||
value.get("structured_value")
|
||||
or value.get("structured")
|
||||
or value.get("field_value")
|
||||
or value.get("结构化值")
|
||||
or value.get("结构化字段")
|
||||
)
|
||||
if ai_code_label_values_match(pdf_value, structured_value) and ai_code_label_text(ai_join_text(value)):
|
||||
return True
|
||||
return ai_text_code_label_consistent_issue(ai_join_text(value))
|
||||
return ai_text_code_label_consistent_issue(value)
|
||||
|
||||
|
||||
def ai_parse_date_token(value: str) -> date | None:
|
||||
match = re.match(r"(\d{4})[-/年](\d{1,2})[-/月](\d{1,2})", value.strip())
|
||||
if not match:
|
||||
return None
|
||||
try:
|
||||
return date(int(match.group(1)), int(match.group(2)), int(match.group(3)))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def ai_dates_in_text(text: str) -> list[tuple[date, int]]:
|
||||
dates: list[tuple[date, int]] = []
|
||||
for match in re.finditer(r"\d{4}[-/年]\d{1,2}[-/月]\d{1,2}", text):
|
||||
parsed = ai_parse_date_token(match.group(0))
|
||||
if parsed:
|
||||
dates.append((parsed, match.start()))
|
||||
return dates
|
||||
|
||||
|
||||
def ai_operation_date_not_early_issue(value: Any) -> bool:
|
||||
text = ai_join_text(value)
|
||||
if not text or not re.search(r"(手术操作日期|手术日期|手术时间)", text):
|
||||
return False
|
||||
if not re.search(r"(早于入院|入院前|同一天|是否早于)", text):
|
||||
return False
|
||||
if re.search(r"PDF.{0,24}\d{4}[-/年]\d{1,2}[-/月]\d{1,2}.{0,24}结构化(?:字段|值)", text):
|
||||
return False
|
||||
admission_match = re.search(r"入院(?:时间|日期)?[^0-9]{0,12}(\d{4}[-/年]\d{1,2}[-/月]\d{1,2})", text)
|
||||
if not admission_match:
|
||||
return False
|
||||
admission_date = ai_parse_date_token(admission_match.group(1))
|
||||
if not admission_date:
|
||||
return False
|
||||
operation_dates = [
|
||||
item_date
|
||||
for item_date, position in ai_dates_in_text(text)
|
||||
if position < admission_match.start()
|
||||
]
|
||||
if not operation_dates:
|
||||
operation_dates = [
|
||||
item_date
|
||||
for item_date, position in ai_dates_in_text(text)
|
||||
if position != admission_match.start(1)
|
||||
]
|
||||
if not operation_dates:
|
||||
return False
|
||||
return all(item_date >= admission_date for item_date in operation_dates)
|
||||
|
||||
|
||||
def ai_false_positive_issue(value: Any) -> bool:
|
||||
return (
|
||||
ai_pdf_prefix_truncation_issue(value)
|
||||
or ai_code_label_consistent_issue(value)
|
||||
or ai_operation_date_not_early_issue(value)
|
||||
)
|
||||
|
||||
|
||||
def ai_has_nonblank_suggested_updates(parsed: dict[str, Any]) -> bool:
|
||||
suggested_updates = parsed.get("suggested_updates")
|
||||
if not isinstance(suggested_updates, list):
|
||||
@@ -2579,16 +2704,20 @@ def ai_problem_evidence(parsed: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
]
|
||||
|
||||
|
||||
def ai_only_pdf_prefix_truncation(parsed: dict[str, Any]) -> bool:
|
||||
def ai_only_false_positive(parsed: dict[str, Any]) -> bool:
|
||||
if ai_has_nonblank_suggested_updates(parsed):
|
||||
return False
|
||||
issues = ai_remaining_issues(parsed)
|
||||
if issues:
|
||||
return all(ai_pdf_prefix_truncation_issue(issue) for issue in issues)
|
||||
return all(ai_false_positive_issue(issue) for issue in issues)
|
||||
evidence = ai_problem_evidence(parsed)
|
||||
if evidence:
|
||||
return all(ai_pdf_prefix_truncation_issue(item) for item in evidence)
|
||||
return ai_pdf_prefix_truncation_issue(parsed.get("summary")) or ai_pdf_prefix_truncation_issue(parsed.get("decision"))
|
||||
return all(ai_false_positive_issue(item) for item in evidence)
|
||||
return ai_false_positive_issue(parsed.get("summary")) or ai_false_positive_issue(parsed.get("decision"))
|
||||
|
||||
|
||||
def ai_only_pdf_prefix_truncation(parsed: dict[str, Any]) -> bool:
|
||||
return ai_only_false_positive(parsed)
|
||||
|
||||
|
||||
def normalize_ai_parsed(parsed: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -2596,9 +2725,9 @@ def normalize_ai_parsed(parsed: dict[str, Any]) -> dict[str, Any]:
|
||||
remaining = normalized.get("remaining_issues")
|
||||
if isinstance(remaining, list):
|
||||
normalized["remaining_issues"] = [
|
||||
item for item in remaining if ai_needs_review_text(item) and not ai_pdf_prefix_truncation_issue(item)
|
||||
item for item in remaining if ai_needs_review_text(item) and not ai_false_positive_issue(item)
|
||||
]
|
||||
elif remaining and ai_needs_review_text(remaining) and not ai_pdf_prefix_truncation_issue(remaining):
|
||||
elif remaining and ai_needs_review_text(remaining) and not ai_false_positive_issue(remaining):
|
||||
normalized["remaining_issues"] = [remaining]
|
||||
else:
|
||||
normalized["remaining_issues"] = []
|
||||
@@ -2622,7 +2751,7 @@ def ai_has_confirmed_problem(parsed: dict[str, Any]) -> bool:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
result = str(item.get("result") or "").strip().lower()
|
||||
if result in {"mismatch", "uncertain", "missing", "problem"} and not ai_pdf_prefix_truncation_issue(item):
|
||||
if result in {"mismatch", "uncertain", "missing", "problem"} and not ai_false_positive_issue(item):
|
||||
return True
|
||||
|
||||
if resolution in {"false_positive", "ok", "no_issue", "误报", "无问题"}:
|
||||
@@ -2651,7 +2780,7 @@ def ai_remaining_issues(parsed: dict[str, Any]) -> list[Any]:
|
||||
|
||||
|
||||
def ai_has_unresolved_problem(parsed: dict[str, Any]) -> bool:
|
||||
if ai_only_pdf_prefix_truncation(parsed):
|
||||
if ai_only_false_positive(parsed):
|
||||
return False
|
||||
if ai_remaining_issues(parsed):
|
||||
return True
|
||||
@@ -2687,20 +2816,26 @@ def ai_classification(parsed: dict[str, Any]) -> str:
|
||||
resolution = str(parsed.get("issue_resolution") or "").strip().lower()
|
||||
ok_values = {"ok", "pass", "passed", "no_issue", "no issue", "无问题", "通过", "已通过"}
|
||||
problem_values = {"problem", "not_ok", "not ok", "confirm", "不ok", "不通过", "需确认", "待确认", "需复核"}
|
||||
if ai_only_pdf_prefix_truncation(parsed):
|
||||
if ai_only_false_positive(parsed):
|
||||
return AI_OK_STATUS
|
||||
if classification in ok_values or decision in ok_values or resolution in {"false_positive", "ok", "no_issue", "误报", "无问题", "通过"}:
|
||||
return AI_OK_STATUS
|
||||
if classification in problem_values or decision in problem_values or resolution in {"confirmed_problem", "uncertain", "problem", "待确认", "已证实"}:
|
||||
if not ai_remaining_issues(parsed) and ai_has_nonblank_suggested_updates(parsed):
|
||||
return AI_OK_STATUS
|
||||
if (
|
||||
not ai_remaining_issues(parsed)
|
||||
and not any(not ai_false_positive_issue(item) for item in ai_problem_evidence(parsed))
|
||||
and not ai_has_unresolved_problem(parsed)
|
||||
):
|
||||
return AI_OK_STATUS
|
||||
return AI_PROBLEM_STATUS
|
||||
if ai_remaining_issues(parsed) or ai_has_unresolved_problem(parsed):
|
||||
return AI_PROBLEM_STATUS
|
||||
suggested_updates = parsed.get("suggested_updates")
|
||||
if isinstance(suggested_updates, list) and suggested_updates:
|
||||
return AI_OK_STATUS
|
||||
if any(not ai_pdf_prefix_truncation_issue(item) for item in ai_problem_evidence(parsed)):
|
||||
if any(not ai_false_positive_issue(item) for item in ai_problem_evidence(parsed)):
|
||||
return AI_PROBLEM_STATUS
|
||||
return AI_OK_STATUS
|
||||
|
||||
@@ -2724,6 +2859,15 @@ def blank_ai_value(value: Any) -> bool:
|
||||
return any(marker in text for marker in ("空白", "未填写", "无内容", "可见但为空", "编码栏可见但为空", "null"))
|
||||
|
||||
|
||||
def normalize_operation_code(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
match = re.match(r"^([A-Za-z]?\d{1,3}\.\d{1,4}x?)\s+(\d{3})$", text, flags=re.IGNORECASE)
|
||||
if match:
|
||||
return f"{match.group(1)}{match.group(2)}"
|
||||
return text
|
||||
|
||||
|
||||
def suggested_update_value(item: dict[str, Any]) -> Any:
|
||||
for key in ("value", "new", "new_value", "pdf_value", "suggested_value"):
|
||||
if key in item:
|
||||
@@ -2829,6 +2973,8 @@ def ai_suggested_updates(result: dict[str, Any], before: dict[str, Any]) -> tupl
|
||||
compare_old = rows[index].get(key)
|
||||
if ai_outpatient_code_leak(path, value, before, compare_old):
|
||||
continue
|
||||
if field == "operations" and key == "手术操作编码":
|
||||
value = normalize_operation_code(value)
|
||||
if ai_pdf_value_is_structured_prefix(value, compare_old):
|
||||
continue
|
||||
if comparable(compare_old) == comparable(value):
|
||||
|
||||
Reference in New Issue
Block a user