feat(regulatory): 新增法规核查模型与工作流通用字段

This commit is contained in:
2026-06-07 00:23:58 +08:00
parent 665403735a
commit f52dcc197d
8 changed files with 878 additions and 7 deletions

View File

@@ -4,7 +4,14 @@ from review_agent.models import FileSummaryBatch, WorkflowEvent
def record_event(batch: FileSummaryBatch, event_type: str, payload: dict | None = None) -> WorkflowEvent:
return WorkflowEvent.objects.create(batch=batch, event_type=event_type, payload=payload or {})
return WorkflowEvent.objects.create(
batch=batch,
workflow_type="file_summary",
workflow_batch_id=batch.pk,
conversation=batch.conversation,
event_type=event_type,
payload=payload or {},
)
def serialize_event(event: WorkflowEvent) -> dict[str, object]:

View File

@@ -54,6 +54,9 @@ def generate_excel_export(batch: FileSummaryBatch) -> ExportedSummaryFile:
workbook.save(path)
exported = ExportedSummaryFile.objects.create(
batch=batch,
workflow_type="file_summary",
workflow_batch_id=batch.pk,
export_category="summary",
export_type=ExportedSummaryFile.ExportType.EXCEL,
file_name=path.name,
storage_path=str(path),

View File

@@ -65,6 +65,9 @@ def generate_markdown_report(batch: FileSummaryBatch) -> tuple[ExportedSummaryFi
path.write_text(content, encoding="utf-8")
exported = ExportedSummaryFile.objects.create(
batch=batch,
workflow_type="file_summary",
workflow_batch_id=batch.pk,
export_category="summary",
export_type=ExportedSummaryFile.ExportType.MARKDOWN,
file_name=path.name,
storage_path=str(path),

View File

@@ -283,11 +283,12 @@ def export_download(request, export_id: int):
extra={"export_id": exported.pk, "storage_path": exported.storage_path},
)
return JsonResponse({"error": "文件不存在。"}, status=404)
content_type = (
"text/markdown; charset=utf-8"
if exported.export_type == ExportedSummaryFile.ExportType.MARKDOWN
else "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
content_types = {
ExportedSummaryFile.ExportType.MARKDOWN: "text/markdown; charset=utf-8",
ExportedSummaryFile.ExportType.EXCEL: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
ExportedSummaryFile.ExportType.JSON: "application/json; charset=utf-8",
}
content_type = content_types.get(exported.export_type, "application/octet-stream")
logger.info(
"Export download started",
extra={

View File

@@ -112,7 +112,14 @@ def create_file_summary_batch(
attachment.save(update_fields=["upload_status"])
for code, name, _skill_name in NODE_DEFINITIONS:
WorkflowNodeRun.objects.create(batch=batch, node_code=code, node_name=name)
WorkflowNodeRun.objects.create(
batch=batch,
workflow_type="file_summary",
workflow_batch_id=batch.pk,
node_group="file_summary",
node_code=code,
node_name=name,
)
record_event(batch, "workflow_created", {"batch_id": batch.pk, "batch_no": batch.batch_no})
logger.info(

View File

@@ -0,0 +1,479 @@
# Generated by Django 5.2.14 on 2026-06-06 16:22
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"review_agent",
"0002_fileattachment_filesummarybatch_exportedsummaryfile_and_more",
),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="RegulatoryArtifact",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"artifact_type",
models.CharField(
choices=[
("markdown", "Markdown"),
("excel", "Excel"),
("json", "JSON"),
("text", "文本"),
],
max_length=20,
),
),
("name", models.CharField(max_length=160)),
("storage_path", models.CharField(max_length=500)),
(
"content_hash",
models.CharField(blank=True, default="", max_length=128),
),
("metadata", models.JSONField(blank=True, default=dict)),
("created_at", models.DateTimeField(auto_now_add=True)),
],
options={
"db_table": "ra_regulatory_artifact",
"ordering": ["-created_at", "-id"],
},
),
migrations.CreateModel(
name="RegulatoryIssue",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("rule_code", models.CharField(blank=True, default="", max_length=120)),
(
"category",
models.CharField(
choices=[
("completeness", "完整性"),
("structure", "章节"),
("consistency", "一致性"),
("rag", "法规依据"),
],
max_length=40,
),
),
(
"severity",
models.CharField(
choices=[
("blocking", "阻断项"),
("high", "高风险"),
("medium", "中风险"),
("low", "低风险"),
("info", "提示"),
],
max_length=20,
),
),
("title", models.CharField(max_length=255)),
("detail", models.TextField(blank=True, default="")),
("suggestion", models.TextField(blank=True, default="")),
(
"status",
models.CharField(
choices=[
("open", "待处理"),
("resolved", "已整改"),
("accepted", "已接受"),
],
default="open",
max_length=20,
),
),
("evidence", models.JSONField(blank=True, default=dict)),
("citations", models.JSONField(blank=True, default=list)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "ra_regulatory_issue",
"ordering": ["severity", "id"],
},
),
migrations.CreateModel(
name="RegulatoryNotificationRecord",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"channel",
models.CharField(
choices=[("mock", "模拟"), ("feishu", "飞书")],
default="mock",
max_length=20,
),
),
("target", models.CharField(blank=True, default="", max_length=160)),
("payload", models.JSONField(blank=True, default=dict)),
(
"status",
models.CharField(
choices=[
("pending", "待发送"),
("sent", "已发送"),
("failed", "失败"),
],
default="pending",
max_length=20,
),
),
("error_message", models.TextField(blank=True, default="")),
("created_at", models.DateTimeField(auto_now_add=True)),
("sent_at", models.DateTimeField(blank=True, null=True)),
],
options={
"db_table": "ra_regulatory_notification_record",
"ordering": ["-created_at", "-id"],
},
),
migrations.CreateModel(
name="RegulatoryReviewBatch",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("batch_no", models.CharField(max_length=64, unique=True)),
(
"status",
models.CharField(
choices=[
("pending", "待执行"),
("running", "执行中"),
("success", "成功"),
("failed", "失败"),
],
default="pending",
max_length=20,
),
),
("risk_summary", models.JSONField(blank=True, default=dict)),
("work_dir", models.CharField(blank=True, default="", max_length=500)),
("error_message", models.TextField(blank=True, default="")),
("created_at", models.DateTimeField(auto_now_add=True)),
("started_at", models.DateTimeField(blank=True, null=True)),
("finished_at", models.DateTimeField(blank=True, null=True)),
],
options={
"db_table": "ra_regulatory_review_batch",
"ordering": ["-created_at", "-id"],
},
),
migrations.CreateModel(
name="RegulatoryRuleVersion",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("code", models.CharField(max_length=80, unique=True)),
("name", models.CharField(max_length=160)),
("yaml_path", models.CharField(max_length=500)),
("yaml_hash", models.CharField(max_length=128)),
(
"rag_collection",
models.CharField(blank=True, default="", max_length=120),
),
(
"rag_index_version",
models.CharField(blank=True, default="", max_length=80),
),
(
"rag_index_hash",
models.CharField(blank=True, default="", max_length=128),
),
(
"status",
models.CharField(
choices=[
("active", "启用"),
("outdated", "待更新"),
("disabled", "停用"),
],
default="active",
max_length=20,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"db_table": "ra_regulatory_rule_version",
"ordering": ["-updated_at", "-id"],
},
),
migrations.AddField(
model_name="exportedsummaryfile",
name="export_category",
field=models.CharField(blank=True, default="summary", max_length=40),
),
migrations.AddField(
model_name="exportedsummaryfile",
name="workflow_batch_id",
field=models.PositiveBigIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name="exportedsummaryfile",
name="workflow_type",
field=models.CharField(blank=True, default="file_summary", max_length=40),
),
migrations.AddField(
model_name="workflowevent",
name="conversation",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="workflow_events",
to="review_agent.conversation",
),
),
migrations.AddField(
model_name="workflowevent",
name="workflow_batch_id",
field=models.PositiveBigIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name="workflowevent",
name="workflow_type",
field=models.CharField(blank=True, default="file_summary", max_length=40),
),
migrations.AddField(
model_name="workflownoderun",
name="node_group",
field=models.CharField(blank=True, default="file_summary", max_length=40),
),
migrations.AddField(
model_name="workflownoderun",
name="workflow_batch_id",
field=models.PositiveBigIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name="workflownoderun",
name="workflow_type",
field=models.CharField(blank=True, default="file_summary", max_length=40),
),
migrations.AlterField(
model_name="exportedsummaryfile",
name="export_type",
field=models.CharField(
choices=[
("markdown", "Markdown"),
("excel", "Excel"),
("json", "JSON"),
],
max_length=20,
),
),
migrations.AlterField(
model_name="workflowevent",
name="batch",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="events",
to="review_agent.filesummarybatch",
),
),
migrations.AlterField(
model_name="workflownoderun",
name="batch",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="node_runs",
to="review_agent.filesummarybatch",
),
),
migrations.AddIndex(
model_name="exportedsummaryfile",
index=models.Index(
fields=["workflow_type", "workflow_batch_id"],
name="idx_ra_export_workflow",
),
),
migrations.AddIndex(
model_name="workflowevent",
index=models.Index(
fields=["workflow_type", "workflow_batch_id", "id"],
name="idx_ra_event_workflow_id",
),
),
migrations.AddIndex(
model_name="workflownoderun",
index=models.Index(
fields=["workflow_type", "workflow_batch_id"],
name="idx_ra_node_workflow",
),
),
migrations.AddField(
model_name="regulatoryreviewbatch",
name="conversation",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="regulatory_review_batches",
to="review_agent.conversation",
),
),
migrations.AddField(
model_name="regulatoryreviewbatch",
name="source_summary_batch",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="regulatory_review_batches",
to="review_agent.filesummarybatch",
),
),
migrations.AddField(
model_name="regulatoryreviewbatch",
name="trigger_message",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="triggered_regulatory_batches",
to="review_agent.message",
),
),
migrations.AddField(
model_name="regulatoryreviewbatch",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="review_regulatory_batches",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="regulatorynotificationrecord",
name="batch",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to="review_agent.regulatoryreviewbatch",
),
),
migrations.AddField(
model_name="regulatoryissue",
name="batch",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="issues",
to="review_agent.regulatoryreviewbatch",
),
),
migrations.AddField(
model_name="regulatoryartifact",
name="batch",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="artifacts",
to="review_agent.regulatoryreviewbatch",
),
),
migrations.AddIndex(
model_name="regulatoryruleversion",
index=models.Index(
fields=["code", "status"], name="idx_ra_rule_code_status"
),
),
migrations.AddField(
model_name="regulatoryreviewbatch",
name="rule_version",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="review_batches",
to="review_agent.regulatoryruleversion",
),
),
migrations.AddIndex(
model_name="regulatorynotificationrecord",
index=models.Index(
fields=["batch", "status"], name="idx_ra_rr_notify_status"
),
),
migrations.AddIndex(
model_name="regulatoryissue",
index=models.Index(
fields=["batch", "severity"], name="idx_ra_rr_issue_severity"
),
),
migrations.AddIndex(
model_name="regulatoryissue",
index=models.Index(
fields=["batch", "category"], name="idx_ra_rr_issue_category"
),
),
migrations.AddIndex(
model_name="regulatoryartifact",
index=models.Index(
fields=["batch", "artifact_type"], name="idx_ra_rr_artifact_type"
),
),
migrations.AddIndex(
model_name="regulatoryreviewbatch",
index=models.Index(
fields=["conversation", "created_at"], name="idx_ra_rr_batch_conv"
),
),
migrations.AddIndex(
model_name="regulatoryreviewbatch",
index=models.Index(
fields=["user", "created_at"], name="idx_ra_rr_batch_user"
),
),
migrations.AddIndex(
model_name="regulatoryreviewbatch",
index=models.Index(
fields=["status", "created_at"], name="idx_ra_rr_batch_status"
),
),
]

View File

@@ -261,8 +261,13 @@ class WorkflowNodeRun(models.Model):
batch = models.ForeignKey(
FileSummaryBatch,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="node_runs",
)
workflow_type = models.CharField(max_length=40, blank=True, default="file_summary")
workflow_batch_id = models.PositiveBigIntegerField(null=True, blank=True)
node_group = models.CharField(max_length=40, blank=True, default="file_summary")
node_code = models.CharField(max_length=40)
node_name = models.CharField(max_length=80)
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
@@ -278,6 +283,10 @@ class WorkflowNodeRun(models.Model):
]
indexes = [
models.Index(fields=["batch", "status"], name="idx_ra_node_batch_status"),
models.Index(
fields=["workflow_type", "workflow_batch_id"],
name="idx_ra_node_workflow",
),
]
@@ -287,8 +296,19 @@ class WorkflowEvent(models.Model):
batch = models.ForeignKey(
FileSummaryBatch,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="events",
)
workflow_type = models.CharField(max_length=40, blank=True, default="file_summary")
workflow_batch_id = models.PositiveBigIntegerField(null=True, blank=True)
conversation = models.ForeignKey(
Conversation,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="workflow_events",
)
event_type = models.CharField(max_length=40)
payload = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
@@ -299,6 +319,10 @@ class WorkflowEvent(models.Model):
indexes = [
models.Index(fields=["batch", "id"], name="idx_ra_event_batch_id"),
models.Index(fields=["batch", "created_at"], name="idx_ra_event_batch_created"),
models.Index(
fields=["workflow_type", "workflow_batch_id", "id"],
name="idx_ra_event_workflow_id",
),
]
@@ -308,6 +332,7 @@ class ExportedSummaryFile(models.Model):
class ExportType(models.TextChoices):
MARKDOWN = "markdown", "Markdown"
EXCEL = "excel", "Excel"
JSON = "json", "JSON"
class Status(models.TextChoices):
SUCCESS = "success", "成功"
@@ -318,6 +343,9 @@ class ExportedSummaryFile(models.Model):
on_delete=models.CASCADE,
related_name="exports",
)
workflow_type = models.CharField(max_length=40, blank=True, default="file_summary")
workflow_batch_id = models.PositiveBigIntegerField(null=True, blank=True)
export_category = models.CharField(max_length=40, blank=True, default="summary")
export_type = models.CharField(max_length=20, choices=ExportType.choices)
file_name = models.CharField(max_length=255)
storage_path = models.CharField(max_length=500)
@@ -331,4 +359,210 @@ class ExportedSummaryFile(models.Model):
indexes = [
models.Index(fields=["batch", "export_type"], name="idx_ra_export_batch_type"),
models.Index(fields=["batch", "created_at"], name="idx_ra_export_batch_created"),
models.Index(
fields=["workflow_type", "workflow_batch_id"],
name="idx_ra_export_workflow",
),
]
class RegulatoryRuleVersion(models.Model):
"""Tracks the local regulatory rule YAML and its matching RAG index."""
class Status(models.TextChoices):
ACTIVE = "active", "启用"
OUTDATED = "outdated", "待更新"
DISABLED = "disabled", "停用"
code = models.CharField(max_length=80, unique=True)
name = models.CharField(max_length=160)
yaml_path = models.CharField(max_length=500)
yaml_hash = models.CharField(max_length=128)
rag_collection = models.CharField(max_length=120, blank=True, default="")
rag_index_version = models.CharField(max_length=80, blank=True, default="")
rag_index_hash = models.CharField(max_length=128, blank=True, default="")
status = models.CharField(max_length=20, choices=Status.choices, default=Status.ACTIVE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "ra_regulatory_rule_version"
ordering = ["-updated_at", "-id"]
indexes = [
models.Index(fields=["code", "status"], name="idx_ra_rule_code_status"),
]
def __str__(self) -> str:
return self.code
class RegulatoryReviewBatch(models.Model):
"""Tracks one NMPA regulatory review workflow run."""
class Status(models.TextChoices):
PENDING = "pending", "待执行"
RUNNING = "running", "执行中"
SUCCESS = "success", "成功"
FAILED = "failed", "失败"
conversation = models.ForeignKey(
Conversation,
on_delete=models.CASCADE,
related_name="regulatory_review_batches",
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="review_regulatory_batches",
)
trigger_message = models.ForeignKey(
Message,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="triggered_regulatory_batches",
)
source_summary_batch = models.ForeignKey(
FileSummaryBatch,
on_delete=models.PROTECT,
related_name="regulatory_review_batches",
)
rule_version = models.ForeignKey(
RegulatoryRuleVersion,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="review_batches",
)
batch_no = models.CharField(max_length=64, unique=True)
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
risk_summary = models.JSONField(default=dict, blank=True)
work_dir = models.CharField(max_length=500, blank=True, default="")
error_message = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
started_at = models.DateTimeField(null=True, blank=True)
finished_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = "ra_regulatory_review_batch"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["conversation", "created_at"], name="idx_ra_rr_batch_conv"),
models.Index(fields=["user", "created_at"], name="idx_ra_rr_batch_user"),
models.Index(fields=["status", "created_at"], name="idx_ra_rr_batch_status"),
]
def __str__(self) -> str:
return self.batch_no
class RegulatoryIssue(models.Model):
"""Stores one regulatory finding after risk consolidation."""
class Severity(models.TextChoices):
BLOCKING = "blocking", "阻断项"
HIGH = "high", "高风险"
MEDIUM = "medium", "中风险"
LOW = "low", "低风险"
INFO = "info", "提示"
class Category(models.TextChoices):
COMPLETENESS = "completeness", "完整性"
STRUCTURE = "structure", "章节"
CONSISTENCY = "consistency", "一致性"
RAG = "rag", "法规依据"
class Status(models.TextChoices):
OPEN = "open", "待处理"
RESOLVED = "resolved", "已整改"
ACCEPTED = "accepted", "已接受"
batch = models.ForeignKey(
RegulatoryReviewBatch,
on_delete=models.CASCADE,
related_name="issues",
)
rule_code = models.CharField(max_length=120, blank=True, default="")
category = models.CharField(max_length=40, choices=Category.choices)
severity = models.CharField(max_length=20, choices=Severity.choices)
title = models.CharField(max_length=255)
detail = models.TextField(blank=True, default="")
suggestion = models.TextField(blank=True, default="")
status = models.CharField(max_length=20, choices=Status.choices, default=Status.OPEN)
evidence = models.JSONField(default=dict, blank=True)
citations = models.JSONField(default=list, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "ra_regulatory_issue"
ordering = ["severity", "id"]
indexes = [
models.Index(fields=["batch", "severity"], name="idx_ra_rr_issue_severity"),
models.Index(fields=["batch", "category"], name="idx_ra_rr_issue_category"),
]
def __str__(self) -> str:
return self.title
class RegulatoryArtifact(models.Model):
"""Stores regulatory review intermediate and exported artifacts."""
class ArtifactType(models.TextChoices):
MARKDOWN = "markdown", "Markdown"
EXCEL = "excel", "Excel"
JSON = "json", "JSON"
TEXT = "text", "文本"
batch = models.ForeignKey(
RegulatoryReviewBatch,
on_delete=models.CASCADE,
related_name="artifacts",
)
artifact_type = models.CharField(max_length=20, choices=ArtifactType.choices)
name = models.CharField(max_length=160)
storage_path = models.CharField(max_length=500)
content_hash = models.CharField(max_length=128, blank=True, default="")
metadata = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "ra_regulatory_artifact"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["batch", "artifact_type"], name="idx_ra_rr_artifact_type"),
]
class RegulatoryNotificationRecord(models.Model):
"""Stores mock notification records for future Feishu integration."""
class Channel(models.TextChoices):
MOCK = "mock", "模拟"
FEISHU = "feishu", "飞书"
class Status(models.TextChoices):
PENDING = "pending", "待发送"
SENT = "sent", "已发送"
FAILED = "failed", "失败"
batch = models.ForeignKey(
RegulatoryReviewBatch,
on_delete=models.CASCADE,
related_name="notifications",
)
channel = models.CharField(max_length=20, choices=Channel.choices, default=Channel.MOCK)
target = models.CharField(max_length=160, blank=True, default="")
payload = models.JSONField(default=dict, blank=True)
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
error_message = models.TextField(blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
sent_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = "ra_regulatory_notification_record"
ordering = ["-created_at", "-id"]
indexes = [
models.Index(fields=["batch", "status"], name="idx_ra_rr_notify_status"),
]