import json import pytest from django.urls import reverse from review_agent.models import ( Conversation, FileSummaryBatch, FileSummaryItem, RegulatoryReviewBatch, WorkflowEvent, WorkflowNodeRun, ) from review_agent.regulatory_review.services.info_extract import detect_regulatory_condition_candidates from review_agent.regulatory_review.workflow import ( create_regulatory_review_batch, start_regulatory_review_workflow, ) pytestmark = pytest.mark.django_db def test_detect_regulatory_condition_candidates_from_summary_items(django_user_model): user = django_user_model.objects.create_user(username="owner", password="pass") conversation = Conversation.objects.create(user=user, title="会话") summary = FileSummaryBatch.objects.create( conversation=conversation, user=user, batch_no="FS-COND", status=FileSummaryBatch.Status.SUCCESS, product_name="甲胎蛋白检测试剂盒", ) FileSummaryItem.objects.create( batch=summary, file_index=1, directory_level="临床评价资料", file_name="免临床评价资料.docx", file_type="docx", relative_path="4.临床评价资料/免临床评价资料.docx", storage_path="missing.docx", ) candidates = detect_regulatory_condition_candidates(summary) assert candidates["product_category"]["suggested"] == "体外诊断试剂" assert candidates["registration_type"]["suggested"] == "首次注册" assert candidates["clinical_evaluation_path"]["suggested"] == "免临床" assert candidates["product_name"]["suggested"] == "甲胎蛋白检测试剂盒" def test_detect_regulatory_condition_prefers_attachment_fields_over_chapter_title(settings, tmp_path, django_user_model): settings.MEDIA_ROOT = tmp_path user = django_user_model.objects.create_user(username="owner", password="pass") conversation = Conversation.objects.create(user=user, title="会话") summary = FileSummaryBatch.objects.create( conversation=conversation, user=user, batch_no="FS-COND", status=FileSummaryBatch.Status.SUCCESS, product_name="第1章 监管信息", ) application = tmp_path / "application.txt" application.write_text( "产品名称:甲胎蛋白检测试剂盒\n型号规格:20人份/盒\n预期用途:用于人血清中甲胎蛋白检测\n注册类型:首次注册\n", encoding="utf-8", ) FileSummaryItem.objects.create( batch=summary, file_index=1, directory_level="1. 监管信息 / 1.2 申请表", file_name="申请表.txt", file_type="txt", relative_path="1.监管信息/申请表.txt", storage_path=str(application), ) candidates = detect_regulatory_condition_candidates(summary) assert candidates["product_name"]["suggested"] == "甲胎蛋白检测试剂盒" assert candidates["model_spec"]["suggested"] == "20人份/盒" assert candidates["intended_use"]["suggested"] == "用于人血清中甲胎蛋白检测" def test_detect_regulatory_condition_keeps_wrapped_product_name(settings, tmp_path, django_user_model): settings.MEDIA_ROOT = tmp_path user = django_user_model.objects.create_user(username="owner", password="pass") conversation = Conversation.objects.create(user=user, title="会话") summary = FileSummaryBatch.objects.create( conversation=conversation, user=user, batch_no="FS-COND", status=FileSummaryBatch.Status.SUCCESS, product_name="第1章 监管信息", ) application = tmp_path / "application.txt" application.write_text( "产品名称:呼吸道合胞病毒、肺炎支原体核酸检测试剂盒\n" "(荧光PCR法)\n" "型号规格:24人份/盒\n" "预期用途:用于呼吸道合胞病毒、肺炎支原体核酸检测\n", encoding="utf-8", ) FileSummaryItem.objects.create( batch=summary, file_index=1, directory_level="1. 监管信息 / 1.2 申请表", file_name="申请表.txt", file_type="txt", relative_path="1.监管信息/申请表.txt", storage_path=str(application), ) candidates = detect_regulatory_condition_candidates(summary) assert candidates["product_name"]["suggested"] == "呼吸道合胞病毒、肺炎支原体核酸检测试剂盒 (荧光PCR法)" assert candidates["model_spec"]["suggested"] == "24人份/盒" def test_detect_regulatory_condition_uses_llm_review_for_better_product_name( monkeypatch, settings, tmp_path, django_user_model ): settings.MEDIA_ROOT = tmp_path settings.REGULATORY_LLM_REVIEW_ALLOW_TEST_CALLS = True user = django_user_model.objects.create_user(username="owner", password="pass") conversation = Conversation.objects.create(user=user, title="会话") summary = FileSummaryBatch.objects.create( conversation=conversation, user=user, batch_no="FS-COND", status=FileSummaryBatch.Status.SUCCESS, product_name="第1章 监管信息", ) application = tmp_path / "application.txt" application.write_text( "产品名称:呼吸道合胞病毒、肺炎支原体核酸检测试剂盒\n" "型号规格:24人份/盒\n", encoding="utf-8", ) FileSummaryItem.objects.create( batch=summary, file_index=1, directory_level="1. 监管信息 / 1.2 申请表", file_name="申请表.txt", file_type="txt", relative_path="1.监管信息/申请表.txt", storage_path=str(application), ) monkeypatch.setattr( "review_agent.regulatory_review.services.llm_review.generate_completion", lambda messages, temperature=0.0: json.dumps( {"fields": {"产品名称": "呼吸道合胞病毒、肺炎支原体核酸检测试剂盒 (荧光PCR法)"}}, ensure_ascii=False, ), ) candidates = detect_regulatory_condition_candidates(summary) assert candidates["product_name"]["suggested"] == "呼吸道合胞病毒、肺炎支原体核酸检测试剂盒 (荧光PCR法)" assert candidates["product_name"]["source"] == "llm" def test_detect_regulatory_condition_infers_fields_from_unlabeled_attachment_text( settings, tmp_path, django_user_model ): settings.MEDIA_ROOT = tmp_path user = django_user_model.objects.create_user(username="owner", password="pass") conversation = Conversation.objects.create(user=user, title="会话") summary = FileSummaryBatch.objects.create( conversation=conversation, user=user, batch_no="FS-COND", status=FileSummaryBatch.Status.SUCCESS, product_name="第1章 监管信息", ) standard_list = tmp_path / "standard_list.txt" standard_list.write_text( "国家药品监督管理局:\n" "卡尤迪生物科技宜兴有限公司申请境内第三类体外诊断试剂" "呼吸道合胞病毒、肺炎支原体核酸检测试剂盒(荧光PCR法)产品注册。\n", encoding="utf-8", ) product_list = tmp_path / "product_list.txt" product_list.write_text( "呼吸道合胞病毒、肺炎支原体核酸检测试剂盒\n" "(荧光PCR法)\n" "产品的包装规格\n" "24人份/盒、48人份/盒\n", encoding="utf-8", ) FileSummaryItem.objects.create( batch=summary, file_index=1, directory_level="第1章 监管信息", file_name="符合标准的清单.txt", file_type="txt", relative_path="第1章 监管信息/符合标准的清单.txt", storage_path=str(standard_list), ) FileSummaryItem.objects.create( batch=summary, file_index=2, directory_level="第1章 监管信息", file_name="产品列表.txt", file_type="txt", relative_path="第1章 监管信息/产品列表.txt", storage_path=str(product_list), ) candidates = detect_regulatory_condition_candidates(summary) assert candidates["product_category"]["suggested"] == "体外诊断试剂" assert candidates["product_name"]["suggested"] == "呼吸道合胞病毒、肺炎支原体核酸检测试剂盒(荧光PCR法)" assert candidates["product_name"]["source"] == "inferred" assert candidates["model_spec"]["suggested"] == "24人份/盒、48人份/盒" def test_workflow_pauses_before_rule_scope_until_conditions_confirmed(settings, tmp_path, django_user_model): settings.MEDIA_ROOT = tmp_path user = django_user_model.objects.create_user(username="owner", password="pass") conversation = Conversation.objects.create(user=user, title="会话") summary = FileSummaryBatch.objects.create( conversation=conversation, user=user, batch_no="FS-COND", status=FileSummaryBatch.Status.SUCCESS, product_name="甲胎蛋白检测试剂盒", ) batch = create_regulatory_review_batch( conversation=conversation, user=user, source_summary_batch=summary, ) start_regulatory_review_workflow(batch, async_run=False) batch.refresh_from_db() condition_node = WorkflowNodeRun.objects.get( workflow_type="regulatory_review", workflow_batch_id=batch.pk, node_code="condition_confirm", ) rule_scope_node = WorkflowNodeRun.objects.get( workflow_type="regulatory_review", workflow_batch_id=batch.pk, node_code="rule_scope", ) assert batch.status == RegulatoryReviewBatch.Status.WAITING_USER assert condition_node.status == WorkflowNodeRun.Status.WAITING_USER assert rule_scope_node.status == WorkflowNodeRun.Status.PENDING assert batch.condition_json["candidates"]["product_category"]["suggested"] == "体外诊断试剂" assert WorkflowEvent.objects.filter( workflow_type="regulatory_review", workflow_batch_id=batch.pk, event_type="waiting_user", ).exists() def test_confirm_conditions_endpoint_resumes_workflow(client, settings, tmp_path, django_user_model): settings.MEDIA_ROOT = tmp_path settings.REGULATORY_REVIEW_ASYNC = False user = django_user_model.objects.create_user(username="owner", password="pass") conversation = Conversation.objects.create(user=user, title="会话") summary = FileSummaryBatch.objects.create( conversation=conversation, user=user, batch_no="FS-COND", status=FileSummaryBatch.Status.SUCCESS, product_name="甲胎蛋白检测试剂盒", ) batch = create_regulatory_review_batch( conversation=conversation, user=user, source_summary_batch=summary, ) start_regulatory_review_workflow(batch, async_run=False) client.force_login(user) response = client.post( reverse("regulatory_review_confirm_conditions", args=[batch.pk]), data=json.dumps( { "conditions": { "product_category": "体外诊断试剂", "registration_type": "首次注册", "clinical_evaluation_path": "免临床", "product_name": "甲胎蛋白检测试剂盒", "model_spec": "卡型", "intended_use": "用于甲胎蛋白检测", } } ), content_type="application/json", ) batch.refresh_from_db() assert response.status_code == 200 assert response.json()["batch"]["status"] == RegulatoryReviewBatch.Status.SUCCESS assert batch.condition_json["confirmed"] is True assert batch.condition_json["confirmed_conditions"]["model_spec"] == "卡型" assert WorkflowNodeRun.objects.get( workflow_type="regulatory_review", workflow_batch_id=batch.pk, node_code="condition_confirm", ).status == WorkflowNodeRun.Status.SUCCESS