feat(regulatory): 支持按附件4章节核查
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user