416 lines
16 KiB
Python
416 lines
16 KiB
Python
import logging
|
||
|
||
import pytest
|
||
|
||
from review_agent.models import (
|
||
Conversation,
|
||
ExportedSummaryFile,
|
||
FileAttachment,
|
||
FileSummaryBatch,
|
||
FileSummaryItem,
|
||
Message,
|
||
RegulatoryIssue,
|
||
RegulatoryArtifact,
|
||
RegulatoryReviewBatch,
|
||
WorkflowEvent,
|
||
WorkflowNodeRun,
|
||
)
|
||
from review_agent.regulatory_review.workflow import (
|
||
NODE_DEFINITIONS,
|
||
create_regulatory_review_batch,
|
||
find_latest_successful_summary_batch,
|
||
start_regulatory_review_workflow,
|
||
)
|
||
from review_agent.services import stream_message
|
||
from review_agent.skill_router import SkillRoute, route_message_intent
|
||
|
||
|
||
pytestmark = pytest.mark.django_db
|
||
|
||
|
||
def test_rule_router_starts_regulatory_review_for_nmpa_keywords(monkeypatch, django_user_model):
|
||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||
conversation = Conversation.objects.create(user=user, title="会话")
|
||
monkeypatch.setattr(
|
||
"review_agent.skill_router._route_with_llm",
|
||
lambda conversation, content, attachments: (_ for _ in ()).throw(ValueError("fallback")),
|
||
)
|
||
|
||
route = route_message_intent(conversation, "请做NMPA核查和风险预警")
|
||
|
||
assert route.action == "regulatory_review"
|
||
assert route.workflow_type == "regulatory_review"
|
||
assert route.starts_regulatory_review
|
||
|
||
|
||
def test_find_latest_successful_summary_batch_ignores_failed_batches(django_user_model):
|
||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||
conversation = Conversation.objects.create(user=user, title="会话")
|
||
success = FileSummaryBatch.objects.create(
|
||
conversation=conversation,
|
||
user=user,
|
||
batch_no="FS-OK",
|
||
status=FileSummaryBatch.Status.SUCCESS,
|
||
)
|
||
FileSummaryBatch.objects.create(
|
||
conversation=conversation,
|
||
user=user,
|
||
batch_no="FS-FAILED",
|
||
status=FileSummaryBatch.Status.FAILED,
|
||
)
|
||
|
||
assert find_latest_successful_summary_batch(conversation) == success
|
||
|
||
|
||
def test_create_regulatory_review_batch_initializes_nodes(django_user_model):
|
||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||
conversation = Conversation.objects.create(user=user, title="会话")
|
||
message = Message.objects.create(conversation=conversation, role=Message.Role.USER, content="法规核查")
|
||
summary = FileSummaryBatch.objects.create(
|
||
conversation=conversation,
|
||
user=user,
|
||
batch_no="FS-OK",
|
||
status=FileSummaryBatch.Status.SUCCESS,
|
||
)
|
||
|
||
batch = create_regulatory_review_batch(
|
||
conversation=conversation,
|
||
user=user,
|
||
trigger_message=message,
|
||
source_summary_batch=summary,
|
||
)
|
||
|
||
assert batch.status == RegulatoryReviewBatch.Status.PENDING
|
||
assert WorkflowNodeRun.objects.filter(
|
||
workflow_type="regulatory_review",
|
||
workflow_batch_id=batch.pk,
|
||
).count() == len(NODE_DEFINITIONS)
|
||
assert WorkflowEvent.objects.filter(
|
||
workflow_type="regulatory_review",
|
||
workflow_batch_id=batch.pk,
|
||
event_type="workflow_created",
|
||
).exists()
|
||
|
||
|
||
def test_start_regulatory_review_workflow_runs_synchronously(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-OK",
|
||
status=FileSummaryBatch.Status.SUCCESS,
|
||
)
|
||
batch = create_regulatory_review_batch(
|
||
conversation=conversation,
|
||
user=user,
|
||
source_summary_batch=summary,
|
||
)
|
||
batch.condition_json = {"confirmed": True, "confirmed_conditions": {"product_category": "体外诊断试剂"}}
|
||
batch.save(update_fields=["condition_json"])
|
||
|
||
start_regulatory_review_workflow(batch, async_run=False)
|
||
|
||
batch.refresh_from_db()
|
||
assert batch.status == RegulatoryReviewBatch.Status.SUCCESS
|
||
assert WorkflowEvent.objects.filter(
|
||
workflow_type="regulatory_review",
|
||
workflow_batch_id=batch.pk,
|
||
event_type="workflow_completed",
|
||
).exists()
|
||
|
||
|
||
def test_workflow_continues_when_llm_review_times_out(monkeypatch, settings, django_user_model):
|
||
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-OK",
|
||
status=FileSummaryBatch.Status.SUCCESS,
|
||
)
|
||
batch = create_regulatory_review_batch(
|
||
conversation=conversation,
|
||
user=user,
|
||
source_summary_batch=summary,
|
||
)
|
||
batch.condition_json = {"confirmed": True, "confirmed_conditions": {"product_category": "体外诊断试剂"}}
|
||
batch.save(update_fields=["condition_json"])
|
||
monkeypatch.setattr(
|
||
"review_agent.regulatory_review.services.llm_review.generate_completion",
|
||
lambda messages, temperature=0.0: (_ for _ in ()).throw(TimeoutError("The read operation timed out")),
|
||
)
|
||
|
||
start_regulatory_review_workflow(batch, async_run=False)
|
||
|
||
batch.refresh_from_db()
|
||
assert batch.status == RegulatoryReviewBatch.Status.SUCCESS
|
||
assert batch.error_message == ""
|
||
|
||
|
||
def test_regulatory_workflow_logs_node_and_method_details(caplog, 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-OK",
|
||
status=FileSummaryBatch.Status.SUCCESS,
|
||
)
|
||
batch = create_regulatory_review_batch(
|
||
conversation=conversation,
|
||
user=user,
|
||
source_summary_batch=summary,
|
||
)
|
||
batch.condition_json = {"confirmed": True, "confirmed_conditions": {"product_category": "体外诊断试剂"}}
|
||
batch.save(update_fields=["condition_json"])
|
||
|
||
with caplog.at_level(logging.INFO, logger="review_agent.regulatory_review.workflow"):
|
||
start_regulatory_review_workflow(batch, async_run=False)
|
||
|
||
messages = [record.getMessage() for record in caplog.records]
|
||
assert any("法规核查工作流开始" in message and batch.batch_no in message for message in messages)
|
||
assert any("节点开始" in message and "完整性核查" in message for message in messages)
|
||
assert any("方法执行" in message and "run_completeness_check" in message for message in messages)
|
||
assert any("节点完成" in message and "完整性核查" in message for message in messages)
|
||
|
||
|
||
def test_stream_message_prompts_for_summary_when_missing(monkeypatch, django_user_model):
|
||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||
conversation = Conversation.objects.create(user=user, title="会话")
|
||
monkeypatch.setattr(
|
||
"review_agent.services.route_message_intent",
|
||
lambda conversation, content: SkillRoute(
|
||
action="regulatory_review",
|
||
workflow_type="regulatory_review",
|
||
confidence=0.9,
|
||
),
|
||
)
|
||
|
||
frames = list(stream_message(conversation, "请做法规核查"))
|
||
|
||
joined = "".join(frames)
|
||
assert "请先在当前对话右侧上传需要核查的文件或压缩包" in joined
|
||
assert "我会先自动汇总再继续法规核查" in joined
|
||
assert not RegulatoryReviewBatch.objects.exists()
|
||
|
||
|
||
def test_stream_message_auto_runs_summary_before_regulatory_review(
|
||
monkeypatch, 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="会话")
|
||
attachment_path = tmp_path / "application.txt"
|
||
attachment_path.write_text("产品名称:甲胎蛋白检测试剂盒", encoding="utf-8")
|
||
FileAttachment.objects.create(
|
||
conversation=conversation,
|
||
user=user,
|
||
original_name="application.txt",
|
||
storage_path=str(attachment_path),
|
||
file_size=attachment_path.stat().st_size,
|
||
is_active=True,
|
||
)
|
||
monkeypatch.setattr(
|
||
"review_agent.services.route_message_intent",
|
||
lambda conversation, content: SkillRoute(
|
||
action="regulatory_review",
|
||
workflow_type="regulatory_review",
|
||
confidence=0.9,
|
||
),
|
||
)
|
||
|
||
def finish_summary(batch, async_run=True):
|
||
batch.status = FileSummaryBatch.Status.SUCCESS
|
||
batch.save(update_fields=["status"])
|
||
|
||
monkeypatch.setattr("review_agent.services.start_file_summary_workflow", finish_summary)
|
||
|
||
frames = list(stream_message(conversation, "进行第一章NMPA 法规核查"))
|
||
joined = "".join(frames)
|
||
|
||
assert "\"workflow_type\": \"file_summary\"" in joined
|
||
assert "\"workflow_type\": \"regulatory_review\"" in joined
|
||
assert "已先启动文件目录与页数自动汇总工作流" in joined
|
||
assert FileSummaryBatch.objects.filter(conversation=conversation, status=FileSummaryBatch.Status.SUCCESS).exists()
|
||
regulatory = RegulatoryReviewBatch.objects.get(conversation=conversation)
|
||
assert regulatory.condition_json["rule_scope"]["attachment4_chapter"] == "1"
|
||
|
||
|
||
def test_stream_message_starts_regulatory_workflow(monkeypatch, settings, django_user_model):
|
||
settings.REGULATORY_REVIEW_ASYNC = False
|
||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||
conversation = Conversation.objects.create(user=user, title="会话")
|
||
FileSummaryBatch.objects.create(
|
||
conversation=conversation,
|
||
user=user,
|
||
batch_no="FS-OK",
|
||
status=FileSummaryBatch.Status.SUCCESS,
|
||
)
|
||
monkeypatch.setattr(
|
||
"review_agent.services.route_message_intent",
|
||
lambda conversation, content: SkillRoute(
|
||
action="regulatory_review",
|
||
workflow_type="regulatory_review",
|
||
confidence=0.9,
|
||
),
|
||
)
|
||
|
||
frames = list(stream_message(conversation, "请做法规核查"))
|
||
|
||
joined = "".join(frames)
|
||
assert "workflow_started" in joined
|
||
assert "\"workflow_type\": \"regulatory_review\"" in joined
|
||
assert RegulatoryReviewBatch.objects.filter(conversation=conversation).exists()
|
||
|
||
|
||
def test_stream_message_records_attachment4_chapter_scope(monkeypatch, settings, django_user_model):
|
||
settings.REGULATORY_REVIEW_ASYNC = False
|
||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||
conversation = Conversation.objects.create(user=user, title="会话")
|
||
FileSummaryBatch.objects.create(
|
||
conversation=conversation,
|
||
user=user,
|
||
batch_no="FS-OK",
|
||
status=FileSummaryBatch.Status.SUCCESS,
|
||
)
|
||
monkeypatch.setattr(
|
||
"review_agent.services.route_message_intent",
|
||
lambda conversation, content: SkillRoute(
|
||
action="regulatory_review",
|
||
workflow_type="regulatory_review",
|
||
confidence=0.9,
|
||
),
|
||
)
|
||
|
||
list(stream_message(conversation, "请做第一章 NMPA 法规核查"))
|
||
|
||
batch = RegulatoryReviewBatch.objects.get(conversation=conversation)
|
||
assert batch.condition_json["rule_scope"]["attachment4_chapter"] == "1"
|
||
assert batch.condition_json["rule_scope"]["label"] == "第1章 监管信息"
|
||
|
||
|
||
def test_workflow_chapter_scope_only_checks_selected_attachment4_chapter(settings, tmp_path, django_user_model):
|
||
settings.MEDIA_ROOT = tmp_path
|
||
settings.REGULATORY_REVIEW_ASYNC = False
|
||
settings.REGULATORY_RAG_CHROMA_PATH = tmp_path / "missing-rag"
|
||
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-OK",
|
||
status=FileSummaryBatch.Status.SUCCESS,
|
||
)
|
||
batch = create_regulatory_review_batch(
|
||
conversation=conversation,
|
||
user=user,
|
||
source_summary_batch=summary,
|
||
)
|
||
batch.condition_json = {
|
||
"confirmed": True,
|
||
"confirmed_conditions": {"product_category": "体外诊断试剂"},
|
||
"rule_scope": {"attachment4_chapter": "1", "label": "第1章 监管信息"},
|
||
}
|
||
batch.save(update_fields=["condition_json"])
|
||
|
||
start_regulatory_review_workflow(batch, async_run=False)
|
||
|
||
issue_codes = list(RegulatoryIssue.objects.filter(batch=batch).values_list("rule_code", flat=True))
|
||
assert issue_codes
|
||
assert all(code.startswith("attachment4_1") for code in issue_codes)
|
||
assert not any(code.startswith("attachment4_2") for code in issue_codes)
|
||
|
||
|
||
def test_workflow_generates_issues_exports_and_assistant_summary(settings, tmp_path, django_user_model):
|
||
settings.MEDIA_ROOT = tmp_path
|
||
settings.REGULATORY_REVIEW_ASYNC = False
|
||
settings.REGULATORY_RAG_CHROMA_PATH = tmp_path / "missing-rag"
|
||
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-OK",
|
||
status=FileSummaryBatch.Status.SUCCESS,
|
||
)
|
||
ifu_path = tmp_path / "ifu.txt"
|
||
ifu_path.write_text("产品名称:甲胎蛋白检测试剂盒\n样本要求:血清\n有效期:12个月", encoding="utf-8")
|
||
FileSummaryItem.objects.create(
|
||
batch=summary,
|
||
file_index=1,
|
||
file_name="说明书.txt",
|
||
file_type="txt",
|
||
relative_path="说明书.txt",
|
||
storage_path=str(ifu_path),
|
||
)
|
||
batch = create_regulatory_review_batch(
|
||
conversation=conversation,
|
||
user=user,
|
||
source_summary_batch=summary,
|
||
)
|
||
batch.condition_json = {"confirmed": True, "confirmed_conditions": {"product_category": "体外诊断试剂"}}
|
||
batch.save(update_fields=["condition_json"])
|
||
|
||
start_regulatory_review_workflow(batch, async_run=False)
|
||
|
||
batch.refresh_from_db()
|
||
assert batch.status == RegulatoryReviewBatch.Status.SUCCESS
|
||
assert RegulatoryIssue.objects.filter(batch=batch, severity="blocking").exists()
|
||
assert ExportedSummaryFile.objects.filter(
|
||
workflow_type="regulatory_review",
|
||
workflow_batch_id=batch.pk,
|
||
).count() == 3
|
||
assert RegulatoryArtifact.objects.filter(batch=batch, name="text_extract_status.json").exists()
|
||
assert RegulatoryArtifact.objects.filter(batch=batch, name="rag_result_json.json").exists()
|
||
assert conversation.messages.filter(role=Message.Role.ASSISTANT, content__contains="已完成 NMPA").exists()
|
||
|
||
|
||
def test_workflow_records_llm_review_artifacts_for_review_nodes(
|
||
monkeypatch, settings, tmp_path, django_user_model
|
||
):
|
||
settings.MEDIA_ROOT = tmp_path
|
||
settings.REGULATORY_REVIEW_ASYNC = False
|
||
settings.REGULATORY_RAG_CHROMA_PATH = tmp_path / "missing-rag"
|
||
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-OK",
|
||
status=FileSummaryBatch.Status.SUCCESS,
|
||
)
|
||
ifu_path = tmp_path / "ifu.txt"
|
||
ifu_path.write_text("产品名称:甲胎蛋白检测试剂盒\n型号规格:20人份/盒", encoding="utf-8")
|
||
FileSummaryItem.objects.create(
|
||
batch=summary,
|
||
file_index=1,
|
||
file_name="说明书.txt",
|
||
file_type="txt",
|
||
relative_path="说明书.txt",
|
||
storage_path=str(ifu_path),
|
||
)
|
||
batch = create_regulatory_review_batch(
|
||
conversation=conversation,
|
||
user=user,
|
||
source_summary_batch=summary,
|
||
)
|
||
batch.condition_json = {"confirmed": True, "confirmed_conditions": {"product_category": "体外诊断试剂"}}
|
||
batch.save(update_fields=["condition_json"])
|
||
|
||
monkeypatch.setattr(
|
||
"review_agent.regulatory_review.workflow.review_workflow_payload",
|
||
lambda stage, payload: {"status": "success", "stage": stage, "result": {"reviewed": True}, "error_message": ""},
|
||
)
|
||
|
||
start_regulatory_review_workflow(batch, async_run=False)
|
||
|
||
artifact_names = set(RegulatoryArtifact.objects.filter(batch=batch).values_list("name", flat=True))
|
||
assert "llm_review_completeness_check.json" in artifact_names
|
||
assert "llm_review_text_extract.json" in artifact_names
|
||
assert "llm_review_structure_check.json" in artifact_names
|
||
assert "llm_review_consistency_check.json" in artifact_names
|
||
assert "llm_review_risk_assess.json" in artifact_names
|