feat(regulatory): 支持按附件4章节核查

This commit is contained in:
2026-06-07 11:17:57 +08:00
parent f46d9c5be6
commit b8d711729d
2 changed files with 121 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import json
import logging
import re
from pathlib import Path
from threading import Thread
from uuid import uuid4
@@ -48,6 +49,16 @@ NODE_DEFINITIONS = [
logger = logging.getLogger("review_agent.regulatory_review.workflow")
ATTACHMENT4_CHAPTER_LABELS = {
"1": "第1章 监管信息",
"2": "第2章 综述资料",
"3": "第3章 非临床资料",
"4": "第4章 临床评价资料",
"5": "第5章 产品说明书和标签样稿",
"6": "第6章 质量管理体系文件",
}
class WorkflowPausedForUser(Exception):
pass
@@ -89,6 +100,7 @@ def create_regulatory_review_batch(
source_summary_batch=source_summary_batch,
batch_no=batch_no,
work_dir=str(work_dir),
condition_json=_initial_condition_json(trigger_message),
)
for code, name, group in NODE_DEFINITIONS:
WorkflowNodeRun.objects.create(
@@ -173,7 +185,7 @@ class RegulatoryWorkflowExecutor:
self._pause_for_condition_confirmation()
return
if node_code == "rule_scope":
self.rule_set = load_rule_file()
self.rule_set = apply_rule_scope(load_rule_file(), self.batch.condition_json.get("rule_scope") or {})
return
if node_code == "completeness_check":
self.findings.extend(run_completeness_check(self.batch.source_summary_batch, self._rules()))
@@ -258,7 +270,7 @@ class RegulatoryWorkflowExecutor:
def _rules(self) -> dict:
if self.rule_set is None:
self.rule_set = load_rule_file()
self.rule_set = apply_rule_scope(load_rule_file(), self.batch.condition_json.get("rule_scope") or {})
return self.rule_set
def _extract_source_texts(self) -> dict[str, str]:
@@ -298,3 +310,52 @@ def start_regulatory_review_workflow(batch: RegulatoryReviewBatch, *, async_run:
executor.run()
return
Thread(target=executor.run, daemon=True).start()
def _initial_condition_json(trigger_message: Message | None) -> dict:
scope = detect_attachment4_chapter_scope(trigger_message.content if trigger_message else "")
return {"rule_scope": scope} if scope else {}
def detect_attachment4_chapter_scope(content: str) -> dict[str, str] | None:
normalized = (content or "").strip()
if not normalized:
return None
chapter = _extract_chapter_number(normalized)
if chapter not in ATTACHMENT4_CHAPTER_LABELS:
return None
return {"attachment4_chapter": chapter, "label": ATTACHMENT4_CHAPTER_LABELS[chapter]}
def apply_rule_scope(rule_set: dict, rule_scope: dict) -> dict:
chapter = str(rule_scope.get("attachment4_chapter") or "")
if chapter not in ATTACHMENT4_CHAPTER_LABELS:
return rule_set
scoped = {**rule_set}
scoped["requirements"] = [
requirement
for requirement in rule_set.get("requirements", [])
if _requirement_in_chapter(requirement, chapter)
]
scoped["active_rule_scope"] = rule_scope
return scoped
def _requirement_in_chapter(requirement: dict, chapter: str) -> bool:
attachment4_code = str(requirement.get("attachment4_code") or "")
return attachment4_code == chapter or attachment4_code.startswith(f"{chapter}.")
def _extract_chapter_number(content: str) -> str:
match = re.search(r"\s*([一二三四五六1-6])\s*[章节张]", content)
if match:
return _normalize_chapter_number(match.group(1))
match = re.search(r"(^|[^\d])([1-6])\s*[章节张]", content)
if match:
return match.group(2)
return ""
def _normalize_chapter_number(value: str) -> str:
chinese = {"": "1", "": "2", "": "3", "": "4", "": "5", "": "6"}
return chinese.get(value, value)