218 lines
7.6 KiB
Python
218 lines
7.6 KiB
Python
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 FileSummaryBatch, RegulatoryReviewBatch, WorkflowNodeRun
|
|
from review_agent.regulatory_review.events import record_event
|
|
from review_agent.regulatory_review.services.rectification_review import review_missing_issues
|
|
from review_agent.regulatory_review.workflow import create_regulatory_review_batch, start_regulatory_review_workflow
|
|
|
|
|
|
@require_http_methods(["GET"])
|
|
@login_required
|
|
def batch_status(request, batch_id: int):
|
|
batch = RegulatoryReviewBatch.objects.filter(pk=batch_id, user=request.user).first()
|
|
if not batch:
|
|
raise Http404("批次不存在。")
|
|
nodes = WorkflowNodeRun.objects.filter(
|
|
workflow_type="regulatory_review",
|
|
workflow_batch_id=batch.pk,
|
|
).order_by("id")
|
|
return JsonResponse(
|
|
{
|
|
"batch": {
|
|
"id": batch.pk,
|
|
"workflow_type": "regulatory_review",
|
|
"batch_no": batch.batch_no,
|
|
"status": batch.status,
|
|
"source_summary_batch_id": batch.source_summary_batch_id,
|
|
"risk_summary": batch.risk_summary,
|
|
"risk_summary_text": _format_risk_summary(batch.risk_summary or {}),
|
|
"error_message": batch.error_message,
|
|
},
|
|
"nodes": [
|
|
{
|
|
"node_code": node.node_code,
|
|
"node_name": node.node_name,
|
|
"status": node.status,
|
|
"progress": node.progress,
|
|
"message": node.message,
|
|
}
|
|
for node in nodes
|
|
],
|
|
}
|
|
)
|
|
|
|
|
|
@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,
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
@login_required
|
|
def start_full_review(request, batch_id: int):
|
|
source_batch = RegulatoryReviewBatch.objects.filter(pk=batch_id, user=request.user).first()
|
|
if not source_batch:
|
|
raise Http404("批次不存在。")
|
|
payload, error_response = _json_payload(request)
|
|
if error_response:
|
|
return error_response
|
|
summary_batch = FileSummaryBatch.objects.filter(
|
|
pk=payload.get("file_summary_batch_id"),
|
|
conversation=source_batch.conversation,
|
|
user=request.user,
|
|
status=FileSummaryBatch.Status.SUCCESS,
|
|
).first()
|
|
if not summary_batch:
|
|
return JsonResponse({"error": "file_summary_batch_id 不存在或未成功。"}, status=400)
|
|
new_batch = create_regulatory_review_batch(
|
|
conversation=source_batch.conversation,
|
|
user=request.user,
|
|
source_summary_batch=summary_batch,
|
|
)
|
|
new_batch.condition_json = {
|
|
"source_review_batch_id": source_batch.pk,
|
|
"regenerated_from": {
|
|
"batch_id": source_batch.pk,
|
|
"batch_no": source_batch.batch_no,
|
|
"file_summary_batch_id": source_batch.source_summary_batch_id,
|
|
"file_summary_batch_no": source_batch.source_summary_batch.batch_no,
|
|
},
|
|
"confirmed": True,
|
|
"confirmed_conditions": source_batch.condition_json.get("confirmed_conditions", {}),
|
|
}
|
|
new_batch.save(update_fields=["condition_json"])
|
|
record_event(
|
|
new_batch,
|
|
"full_package_review_started",
|
|
{"source_review_batch_id": source_batch.pk, "source_review_batch_no": source_batch.batch_no},
|
|
)
|
|
start_regulatory_review_workflow(
|
|
new_batch,
|
|
async_run=getattr(settings, "REGULATORY_REVIEW_ASYNC", True),
|
|
)
|
|
new_batch.refresh_from_db()
|
|
return JsonResponse(
|
|
{
|
|
"batch": {
|
|
"id": new_batch.pk,
|
|
"workflow_type": "regulatory_review",
|
|
"batch_no": new_batch.batch_no,
|
|
"status": new_batch.status,
|
|
"source_review_batch_id": source_batch.pk,
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
@require_http_methods(["POST"])
|
|
@login_required
|
|
def review_issues(request, batch_id: int):
|
|
batch = RegulatoryReviewBatch.objects.filter(pk=batch_id, user=request.user).first()
|
|
if not batch:
|
|
raise Http404("批次不存在。")
|
|
payload, error_response = _json_payload(request)
|
|
if error_response:
|
|
return error_response
|
|
issue_ids = payload.get("issue_ids")
|
|
if not isinstance(issue_ids, list):
|
|
return JsonResponse({"error": "issue_ids 必须是列表。"}, status=400)
|
|
summary_batch = FileSummaryBatch.objects.filter(
|
|
pk=payload.get("file_summary_batch_id"),
|
|
conversation=batch.conversation,
|
|
user=request.user,
|
|
status=FileSummaryBatch.Status.SUCCESS,
|
|
).first()
|
|
if not summary_batch:
|
|
return JsonResponse({"error": "file_summary_batch_id 不存在或未成功。"}, status=400)
|
|
record = review_missing_issues(batch=batch, issue_ids=[int(item) for item in issue_ids], file_summary_batch=summary_batch)
|
|
return JsonResponse({"review_record": record})
|
|
|
|
|
|
def _format_risk_summary(risk_summary: dict) -> str:
|
|
labels = [
|
|
("blocking", "阻断项"),
|
|
("high", "高风险"),
|
|
("medium", "中风险"),
|
|
("low", "低风险"),
|
|
("info", "提示"),
|
|
]
|
|
return " · ".join(
|
|
f"{label} {int(risk_summary.get(key) or 0)}"
|
|
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}
|
|
|
|
|
|
def _json_payload(request):
|
|
try:
|
|
return json.loads(request.body.decode("utf-8") or "{}"), None
|
|
except json.JSONDecodeError:
|
|
return {}, JsonResponse({"error": "请求体不是有效 JSON。"}, status=400)
|