feat: 重构资料包模型与会话绑定主链路

This commit is contained in:
2026-06-04 00:43:13 +08:00
parent ddf5e7d15c
commit d0841e533f
18 changed files with 1000 additions and 263 deletions

View File

@@ -2,24 +2,67 @@ from django.urls import reverse
from agent_core.results import AgentResult
from apps.audit.models import AgentAuditLog
from apps.documents.models import UploadedDocument
from apps.chat.models import Conversation
from apps.documents.models import SubmissionBatch, UploadedDocument
def test_chat_post_returns_agent_result_and_audit_log(client, db):
def _create_conversation_with_batch():
batch = SubmissionBatch.objects.create(
batch_id="SUB-20260604-001",
product_name="新型冠状病毒 2019-nCoV 核酸检测试剂盒",
workflow_type="registration",
conversation_id="conv-001",
file_count=2,
page_count=12,
import_status="completed",
)
conversation = Conversation.objects.create(
conversation_id="conv-001",
title="新型冠状病毒 2019-nCoV 核酸检测试剂盒",
product_name=batch.product_name,
batch_id=batch.batch_id,
task_status="processing",
node_results=[
{"label": "资料包导入", "status": "已完成"},
{"label": "目录汇总", "status": "处理中"},
],
)
return batch, conversation
def test_chat_post_returns_agent_result_and_audit_log(client, db, monkeypatch):
batch, conversation = _create_conversation_with_batch()
UploadedDocument.objects.create(
batch=batch,
scenario_id="document_review",
original_name="说明书.md",
file_type="md",
size=1,
status=UploadedDocument.STATUS_INDEXED,
)
monkeypatch.setattr(
"apps.chat.views.run_agent",
lambda *args, **kwargs: AgentResult(answer="模拟回答", status="success"),
)
response = client.post(
reverse("chat:index", args=["knowledge_qa"]),
reverse("chat:detail", args=[conversation.conversation_id]),
{"message": "如何处理异常?"},
)
assert response.status_code == 200
content = response.content.decode("utf-8")
assert "mock-model" in content
assert "审核智能体" in content
assert "模拟回答" in content
assert AgentAuditLog.objects.count() == 1
assert AgentAuditLog.objects.get().batch_id == batch.batch_id
def test_chat_rejects_empty_message(client, db):
response = client.post(reverse("chat:index", args=["knowledge_qa"]), {"message": ""})
_batch, conversation = _create_conversation_with_batch()
response = client.post(reverse("chat:detail", args=[conversation.conversation_id]), {"message": ""})
assert response.status_code == 200
assert AgentAuditLog.objects.count() == 0
@@ -27,15 +70,18 @@ def test_chat_rejects_empty_message(client, db):
def test_chat_passes_selected_document_ids_to_agent_core(client, db, monkeypatch):
batch, conversation = _create_conversation_with_batch()
selected = UploadedDocument.objects.create(
scenario_id="knowledge_qa",
batch=batch,
scenario_id="document_review",
original_name="selected.md",
file_type="md",
size=1,
status=UploadedDocument.STATUS_INDEXED,
)
other = UploadedDocument.objects.create(
scenario_id="knowledge_qa",
UploadedDocument.objects.create(
batch=batch,
scenario_id="document_review",
original_name="other.md",
file_type="md",
size=1,
@@ -45,63 +91,38 @@ def test_chat_passes_selected_document_ids_to_agent_core(client, db, monkeypatch
def fake_run_agent(scenario_config, user_input, options=None):
captured["options"] = options or {}
from agent_core.results import AgentResult
return AgentResult(answer="ok", status="success")
monkeypatch.setattr("apps.chat.views.run_agent", fake_run_agent)
response = client.post(
reverse("chat:index", args=["knowledge_qa"]),
reverse("chat:detail", args=[conversation.conversation_id]),
{"message": "只查选中文档", "document_ids": [str(selected.id)]},
)
assert response.status_code == 200
assert captured["options"]["document_ids"] == [selected.id]
assert other.id not in captured["options"]["document_ids"]
assert captured["options"]["conversation_id"] == conversation.conversation_id
assert captured["options"]["batch_id"] == batch.batch_id
def test_chat_renders_structured_output_references_and_tool_calls(client, db, monkeypatch):
def fake_run_agent(scenario_config, user_input, options=None):
return AgentResult(
answer="建议先隔离现场。",
structured_output={
"output_type": "quality_report",
"summary": "发现异常批次需要立即处置。",
"risk_level": "high",
"suggested_actions": ["隔离现场", "通知负责人"],
},
references=[
{
"source": "sop.md",
"content": "异常处理 SOP先隔离现场再通知负责人。",
}
],
tool_calls=[
{
"tool_name": "query_demo_records",
"success": True,
"result": {"records": [{"title": "A线缺陷"}]},
"error": "",
}
],
model_name="mock-model",
status="success",
)
monkeypatch.setattr("apps.chat.views.run_agent", fake_run_agent)
response = client.post(
reverse("chat:index", args=["quality_analysis"]),
{"message": "分析 A 线异常"},
def test_chat_renders_three_column_workspace_and_node_results(client, db):
batch, conversation = _create_conversation_with_batch()
UploadedDocument.objects.create(
batch=batch,
scenario_id="document_review",
original_name="说明书.md",
file_type="md",
size=1,
status=UploadedDocument.STATUS_INDEXED,
)
response = client.get(reverse("chat:detail", args=[conversation.conversation_id]))
content = response.content.decode("utf-8")
assert response.status_code == 200
assert "结构化结果" in content
assert "发现异常批次需要立即处置" in content
assert "引用片段" in content
assert "sop.md" in content
assert "工具调用" in content
assert "query_demo_records" in content
assert "查看本次审计日志" in content
assert "会话历史" in content
assert "对话区与节点导航" in content
assert "上传区" in content
assert "资料包导入 / 已完成" in content
assert "目录汇总 / 处理中" in content

View File

@@ -2,8 +2,9 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from apps.documents.forms import DocumentUploadForm
from apps.documents.models import UploadedDocument
from apps.documents.services import extract_text, index_document
from apps.documents.models import SubmissionBatch, UploadedDocument
from apps.documents.services import extract_text, import_submission_batch, index_document
from apps.chat.models import Conversation
def test_upload_txt_document_creates_uploaded_record(client, db):
@@ -31,7 +32,7 @@ def test_upload_redirect_shows_success_message(client, db):
)
assert response.status_code == 200
assert "文件已上传,可继续执行入库" in response.content.decode("utf-8")
assert "资料包已导入,已绑定会话" in response.content.decode("utf-8")
def test_upload_accepts_pdf_and_docx_documents(client, db):
@@ -145,3 +146,74 @@ def test_index_document_marks_failed_when_extracted_text_is_empty(db, monkeypatc
assert updated_document.status == UploadedDocument.STATUS_FAILED
assert "文档内容为空" in updated_document.error_message
def test_upload_creates_submission_batch_and_bound_conversation(client, db):
file = SimpleUploadedFile(
"目标产品说明书.txt",
"产品名称:新型冠状病毒 2019-nCoV 核酸检测试剂盒".encode("utf-8"),
content_type="text/plain",
)
response = client.post(
reverse("documents:upload"),
{"scenario_id": "document_review", "file": file},
)
assert response.status_code == 302
batch = SubmissionBatch.objects.get()
conversation = Conversation.objects.get()
assert batch.product_name == "新型冠状病毒 2019-nCoV 核酸检测试剂盒"
assert batch.conversation_id == conversation.conversation_id
assert conversation.title == "新型冠状病毒 2019-nCoV 核酸检测试剂盒"
assert batch.file_count == 1
def test_document_list_supports_product_name_search(client, db):
SubmissionBatch.objects.create(
batch_id="SUB-20260604-001",
product_name="新型冠状病毒 2019-nCoV 核酸检测试剂盒",
workflow_type="registration",
conversation_id="conv-001",
file_count=2,
page_count=12,
import_status="completed",
)
SubmissionBatch.objects.create(
batch_id="SUB-20260604-002",
product_name="呼吸道合胞病毒核酸检测试剂盒",
workflow_type="registration",
conversation_id="conv-002",
file_count=3,
page_count=20,
import_status="completed",
)
response = client.get(reverse("documents:list"), {"keyword": "新型冠状病毒"})
content = response.content.decode("utf-8")
assert response.status_code == 200
assert "新型冠状病毒 2019-nCoV 核酸检测试剂盒" in content
assert "呼吸道合胞病毒核酸检测试剂盒" not in content
def test_import_submission_batch_marks_manual_review_when_product_names_conflict(db):
files = [
SimpleUploadedFile(
"注册申请表.txt",
"产品名称产品A".encode("utf-8"),
content_type="text/plain",
),
SimpleUploadedFile(
"目标产品说明书.txt",
"产品名称产品B".encode("utf-8"),
content_type="text/plain",
),
]
result = import_submission_batch("document_review", files)
batch = SubmissionBatch.objects.get(batch_id=result["batch_id"])
assert batch.import_status == "review_required"
assert result["registration_overview_report"]["warnings"]
assert "产品名称来源冲突" in result["registration_overview_report"]["warnings"][0]