feat(regulatory): 增加法规规则版本检查
This commit is contained in:
1
review_agent/regulatory_review/__init__.py
Normal file
1
review_agent/regulatory_review/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""NMPA regulatory review workflow package."""
|
||||
@@ -0,0 +1,58 @@
|
||||
code: nmpa_ivd_registration_v1
|
||||
name: NMPA IVD 注册资料 Demo 规则
|
||||
rag_collection: nmpa_ivd_registration_v1
|
||||
source_material_dir: docs/0.原始材料/关于公布体外诊断试剂注册申报资料要求和批准证明文件格式的公告
|
||||
requirements:
|
||||
- code: product_technical_requirements
|
||||
title: 产品技术要求
|
||||
type: required
|
||||
severity: blocking
|
||||
category: completeness
|
||||
file_keywords:
|
||||
- 产品技术要求
|
||||
suggestion: 请补充产品技术要求并确认版本与注册申请资料一致。
|
||||
citation_query: 体外诊断试剂 产品技术要求 注册申报资料
|
||||
- code: instructions_for_use
|
||||
title: 说明书
|
||||
type: required
|
||||
severity: high
|
||||
category: completeness
|
||||
file_keywords:
|
||||
- 说明书
|
||||
- 使用说明
|
||||
required_sections:
|
||||
- 储存条件
|
||||
- 有效期
|
||||
- 样本要求
|
||||
suggestion: 请补充说明书并核对储存条件、有效期和样本要求章节。
|
||||
citation_query: 体外诊断试剂 说明书 储存条件 有效期 样本要求
|
||||
- code: registration_test_report
|
||||
title: 注册检验报告
|
||||
type: required
|
||||
severity: blocking
|
||||
category: completeness
|
||||
file_keywords:
|
||||
- 注册检验报告
|
||||
- 检验报告
|
||||
suggestion: 请补充注册检验报告并复核报告覆盖的产品型号。
|
||||
citation_query: 体外诊断试剂 注册检验报告 注册申报资料
|
||||
- code: clinical_evaluation
|
||||
title: 临床评价资料
|
||||
type: conditional
|
||||
severity: high
|
||||
category: completeness
|
||||
file_keywords:
|
||||
- 临床评价
|
||||
- 临床试验
|
||||
suggestion: 请根据适用情形补充临床评价资料或说明豁免依据。
|
||||
citation_query: 体外诊断试剂 临床评价资料 注册申报
|
||||
- code: essential_principles_checklist
|
||||
title: 安全和性能基本原则清单
|
||||
type: recommended
|
||||
severity: medium
|
||||
category: completeness
|
||||
file_keywords:
|
||||
- 安全和性能基本原则
|
||||
- 基本原则清单
|
||||
suggestion: 建议补充安全和性能基本原则清单,便于审评追溯。
|
||||
citation_query: 体外诊断试剂 安全和性能基本原则清单
|
||||
1
review_agent/regulatory_review/services/__init__.py
Normal file
1
review_agent/regulatory_review/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Services for NMPA regulatory review."""
|
||||
106
review_agent/regulatory_review/services/rule_loader.py
Normal file
106
review_agent/regulatory_review/services/rule_loader.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from django.conf import settings
|
||||
|
||||
from review_agent.models import RegulatoryRuleVersion
|
||||
|
||||
|
||||
DEFAULT_RULE_CODE = "nmpa_ivd_registration_v1"
|
||||
DEFAULT_RULE_PATH = (
|
||||
Path(settings.BASE_DIR)
|
||||
/ "review_agent"
|
||||
/ "regulatory_review"
|
||||
/ "rules"
|
||||
/ "nmpa_ivd_registration_v1.yaml"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuleVersionCheck:
|
||||
status: str
|
||||
code: str
|
||||
path: Path
|
||||
current_hash: str
|
||||
database_hash: str = ""
|
||||
record: RegulatoryRuleVersion | None = None
|
||||
|
||||
|
||||
def compute_file_sha256(path: str | Path) -> str:
|
||||
file_path = Path(path)
|
||||
digest = hashlib.sha256()
|
||||
with file_path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def load_rule_file(path: str | Path | None = None) -> dict:
|
||||
rule_path = Path(path) if path else DEFAULT_RULE_PATH
|
||||
with rule_path.open("r", encoding="utf-8") as handle:
|
||||
payload = yaml.safe_load(handle) or {}
|
||||
if payload.get("code") != DEFAULT_RULE_CODE:
|
||||
raise ValueError(f"规则 code 必须为 {DEFAULT_RULE_CODE}")
|
||||
if not isinstance(payload.get("requirements"), list) or not payload["requirements"]:
|
||||
raise ValueError("规则文件必须包含 requirements 列表。")
|
||||
return payload
|
||||
|
||||
|
||||
def check_rule_version(
|
||||
*,
|
||||
path: str | Path | None = None,
|
||||
update_missing: bool = True,
|
||||
) -> RuleVersionCheck:
|
||||
rule_path = Path(path) if path else DEFAULT_RULE_PATH
|
||||
rule_set = load_rule_file(rule_path)
|
||||
current_hash = compute_file_sha256(rule_path)
|
||||
record = RegulatoryRuleVersion.objects.filter(code=rule_set["code"]).first()
|
||||
yaml_path = str(rule_path.relative_to(settings.BASE_DIR))
|
||||
|
||||
if record is None:
|
||||
if not update_missing:
|
||||
return RuleVersionCheck(
|
||||
status="missing",
|
||||
code=rule_set["code"],
|
||||
path=rule_path,
|
||||
current_hash=current_hash,
|
||||
)
|
||||
record = RegulatoryRuleVersion.objects.create(
|
||||
code=rule_set["code"],
|
||||
name=rule_set.get("name") or rule_set["code"],
|
||||
yaml_path=yaml_path,
|
||||
yaml_hash=current_hash,
|
||||
rag_collection=rule_set.get("rag_collection", ""),
|
||||
status=RegulatoryRuleVersion.Status.ACTIVE,
|
||||
)
|
||||
return RuleVersionCheck(
|
||||
status="created",
|
||||
code=record.code,
|
||||
path=rule_path,
|
||||
current_hash=current_hash,
|
||||
database_hash=record.yaml_hash,
|
||||
record=record,
|
||||
)
|
||||
|
||||
if record.yaml_hash != current_hash:
|
||||
return RuleVersionCheck(
|
||||
status="mismatch",
|
||||
code=record.code,
|
||||
path=rule_path,
|
||||
current_hash=current_hash,
|
||||
database_hash=record.yaml_hash,
|
||||
record=record,
|
||||
)
|
||||
|
||||
return RuleVersionCheck(
|
||||
status="ok",
|
||||
code=record.code,
|
||||
path=rule_path,
|
||||
current_hash=current_hash,
|
||||
database_hash=record.yaml_hash,
|
||||
record=record,
|
||||
)
|
||||
Reference in New Issue
Block a user