feat(regulatory): 增加适用条件确认暂停恢复

This commit is contained in:
2026-06-07 09:19:31 +08:00
parent bd805203f1
commit bbd2d3532a
12 changed files with 535 additions and 1 deletions

View File

@@ -0,0 +1,81 @@
from __future__ import annotations
from review_agent.models import FileSummaryBatch
OPTION_FIELDS = {
"product_category": ["体外诊断试剂", "医疗器械", "其他"],
"registration_type": ["首次注册", "变更注册", "延续注册"],
"clinical_evaluation_path": ["临床试验", "免临床", "同品种比对", "待确认"],
}
def detect_regulatory_condition_candidates(summary_batch: FileSummaryBatch) -> dict[str, dict[str, object]]:
"""Infers review-scope conditions from the summary batch and file names."""
corpus_parts = [summary_batch.product_name or ""]
for item in summary_batch.items.order_by("file_index"):
corpus_parts.extend([item.directory_level, item.file_name, item.relative_path])
corpus = "\n".join(part for part in corpus_parts if part)
return {
"product_category": {
"label": "产品类别",
"input_type": "select",
"options": OPTION_FIELDS["product_category"],
"suggested": _detect_product_category(corpus),
},
"registration_type": {
"label": "注册类型",
"input_type": "select",
"options": OPTION_FIELDS["registration_type"],
"suggested": _detect_registration_type(corpus),
},
"clinical_evaluation_path": {
"label": "临床评价路径",
"input_type": "select",
"options": OPTION_FIELDS["clinical_evaluation_path"],
"suggested": _detect_clinical_path(corpus),
},
"product_name": {
"label": "产品名称",
"input_type": "text",
"suggested": summary_batch.product_name or "",
},
"model_spec": {
"label": "型号规格",
"input_type": "text",
"suggested": "",
},
"intended_use": {
"label": "预期用途",
"input_type": "text",
"suggested": "",
},
}
def _detect_product_category(corpus: str) -> str:
if any(keyword in corpus for keyword in ["体外诊断", "检测试剂", "试剂盒", "IVD"]):
return "体外诊断试剂"
if "医疗器械" in corpus:
return "医疗器械"
return "其他"
def _detect_registration_type(corpus: str) -> str:
if "延续" in corpus:
return "延续注册"
if "变更" in corpus:
return "变更注册"
return "首次注册"
def _detect_clinical_path(corpus: str) -> str:
if "免临床" in corpus or "免于临床" in corpus:
return "免临床"
if "同品种" in corpus or "同类" in corpus:
return "同品种比对"
if "临床试验" in corpus:
return "临床试验"
return "待确认"

View File

@@ -1,10 +1,15 @@
from __future__ import annotations
import json
from django.conf import settings
from django.http import Http404, JsonResponse
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required
from review_agent.models import RegulatoryReviewBatch, WorkflowNodeRun
from review_agent.regulatory_review.events import record_event
from review_agent.regulatory_review.workflow import start_regulatory_review_workflow
@require_http_methods(["GET"])
@@ -43,6 +48,59 @@ def batch_status(request, batch_id: int):
)
@require_http_methods(["POST"])
@login_required
def confirm_conditions(request, batch_id: int):
batch = RegulatoryReviewBatch.objects.filter(pk=batch_id, user=request.user).first()
if not batch:
raise Http404("批次不存在。")
try:
payload = json.loads(request.body.decode("utf-8") or "{}")
except json.JSONDecodeError:
return JsonResponse({"error": "请求体不是有效 JSON。"}, status=400)
conditions = payload.get("conditions")
if not isinstance(conditions, dict):
return JsonResponse({"error": "conditions 必须是对象。"}, status=400)
batch.condition_json = {
**(batch.condition_json or {}),
"confirmed": True,
"confirmed_conditions": _normalize_conditions(conditions),
}
batch.status = RegulatoryReviewBatch.Status.RUNNING
batch.save(update_fields=["condition_json", "status"])
WorkflowNodeRun.objects.filter(
workflow_type="regulatory_review",
workflow_batch_id=batch.pk,
node_code="condition_confirm",
).update(
status=WorkflowNodeRun.Status.SUCCESS,
progress=100,
message="适用条件已确认",
)
record_event(
batch,
"condition_confirmed",
{"conditions": batch.condition_json["confirmed_conditions"], "resume_from": "rule_scope"},
)
start_regulatory_review_workflow(
batch,
async_run=getattr(settings, "REGULATORY_REVIEW_ASYNC", True),
)
batch.refresh_from_db()
return JsonResponse(
{
"batch": {
"id": batch.pk,
"workflow_type": "regulatory_review",
"batch_no": batch.batch_no,
"status": batch.status,
"condition_json": batch.condition_json,
}
}
)
def _format_risk_summary(risk_summary: dict) -> str:
labels = [
("blocking", "阻断项"),
@@ -56,3 +114,15 @@ def _format_risk_summary(risk_summary: dict) -> str:
for key, label in labels
if int(risk_summary.get(key) or 0)
)
def _normalize_conditions(conditions: dict) -> dict[str, str]:
allowed = [
"product_category",
"registration_type",
"clinical_evaluation_path",
"product_name",
"model_spec",
"intended_use",
]
return {key: str(conditions.get(key) or "").strip() for key in allowed}

View File

@@ -19,6 +19,7 @@ from review_agent.models import (
from review_agent.regulatory_review.services.completeness_check import run_completeness_check
from review_agent.regulatory_review.services.consistency_check import run_consistency_check
from review_agent.regulatory_review.services.export import build_assistant_summary, export_review_results
from review_agent.regulatory_review.services.info_extract import detect_regulatory_condition_candidates
from review_agent.regulatory_review.services.risk_assess import persist_findings
from review_agent.regulatory_review.services.rule_loader import load_rule_file
from review_agent.regulatory_review.services.structure_check import run_structure_check
@@ -29,6 +30,7 @@ from .events import record_event
NODE_DEFINITIONS = [
("prepare", "准备", "prepare"),
("condition_confirm", "适用条件确认", "condition_confirm"),
("rule_scope", "规则范围", "rule_scope"),
("completeness_check", "完整性核查", "completeness_check"),
("text_extract", "文本抽取", "text_extract"),
@@ -43,6 +45,10 @@ NODE_DEFINITIONS = [
logger = logging.getLogger("review_agent.regulatory_review.workflow")
class WorkflowPausedForUser(Exception):
pass
def build_batch_no() -> str:
return f"RR-{timezone.localtime().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:6]}"
@@ -108,7 +114,11 @@ class RegulatoryWorkflowExecutor:
try:
for node in self._nodes():
if node.status == WorkflowNodeRun.Status.SUCCESS:
continue
self._run_node(node)
except WorkflowPausedForUser:
return
except Exception as exc:
logger.exception("Regulatory workflow failed", extra={"batch_id": self.batch.pk})
self.batch.status = RegulatoryReviewBatch.Status.FAILED
@@ -155,6 +165,9 @@ class RegulatoryWorkflowExecutor:
)
def _execute_node(self, node_code: str) -> None:
if node_code == "condition_confirm":
self._pause_for_condition_confirmation()
return
if node_code == "rule_scope":
self.rule_set = load_rule_file()
return
@@ -181,6 +194,34 @@ class RegulatoryWorkflowExecutor:
content=build_assistant_summary(self.batch, exports),
)
def _pause_for_condition_confirmation(self) -> None:
if self.batch.condition_json.get("confirmed"):
return
candidates = detect_regulatory_condition_candidates(self.batch.source_summary_batch)
self.batch.condition_json = {
**(self.batch.condition_json or {}),
"confirmed": False,
"resume_from": "rule_scope",
"candidates": candidates,
}
self.batch.status = RegulatoryReviewBatch.Status.WAITING_USER
self.batch.save(update_fields=["status", "condition_json"])
node = WorkflowNodeRun.objects.get(
workflow_type="regulatory_review",
workflow_batch_id=self.batch.pk,
node_code="condition_confirm",
)
node.status = WorkflowNodeRun.Status.WAITING_USER
node.progress = 50
node.message = "请确认产品类别、注册类型、临床评价路径等适用条件"
node.save(update_fields=["status", "progress", "message"])
record_event(
self.batch,
"waiting_user",
{"node_code": "condition_confirm", "candidates": candidates, "resume_from": "rule_scope"},
)
raise WorkflowPausedForUser()
def _rules(self) -> dict:
if self.rule_set is None:
self.rule_set = load_rule_file()