569 lines
20 KiB
Python
569 lines
20 KiB
Python
from django.conf import settings
|
|
from django.db import models
|
|
|
|
|
|
class Conversation(models.Model):
|
|
"""Stores a user's review-agent conversation shell."""
|
|
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name="review_conversations",
|
|
)
|
|
title = models.CharField(max_length=120, blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ["-updated_at", "-id"]
|
|
|
|
def __str__(self) -> str:
|
|
return self.title or f"对话 {self.pk}"
|
|
|
|
|
|
class Message(models.Model):
|
|
"""Stores one user or assistant message in a conversation."""
|
|
|
|
class Role(models.TextChoices):
|
|
USER = "user", "用户"
|
|
ASSISTANT = "assistant", "助手"
|
|
|
|
conversation = models.ForeignKey(
|
|
Conversation,
|
|
on_delete=models.CASCADE,
|
|
related_name="messages",
|
|
)
|
|
role = models.CharField(max_length=20, choices=Role.choices)
|
|
content = models.TextField()
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
ordering = ["created_at", "id"]
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.get_role_display()} - {self.conversation_id}"
|
|
|
|
|
|
class FileAttachment(models.Model):
|
|
"""Stores an uploaded file version for one conversation."""
|
|
|
|
class UploadStatus(models.TextChoices):
|
|
UPLOADED = "uploaded", "已上传"
|
|
BOUND = "bound", "已绑定"
|
|
DELETED = "deleted", "已删除"
|
|
|
|
conversation = models.ForeignKey(
|
|
Conversation,
|
|
on_delete=models.CASCADE,
|
|
related_name="file_attachments",
|
|
)
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name="review_file_attachments",
|
|
)
|
|
original_name = models.CharField(max_length=255)
|
|
version_no = models.PositiveIntegerField(default=1)
|
|
is_active = models.BooleanField(default=True)
|
|
storage_path = models.CharField(max_length=500)
|
|
file_size = models.BigIntegerField(default=0)
|
|
content_type = models.CharField(max_length=120, blank=True, default="")
|
|
upload_status = models.CharField(
|
|
max_length=20,
|
|
choices=UploadStatus.choices,
|
|
default=UploadStatus.UPLOADED,
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = "ra_file_attachment"
|
|
ordering = ["-created_at", "-id"]
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["conversation", "original_name", "version_no"],
|
|
name="uq_ra_attachment_conv_name_version",
|
|
)
|
|
]
|
|
indexes = [
|
|
models.Index(
|
|
fields=["conversation", "created_at"],
|
|
name="idx_ra_attachment_conv_created",
|
|
),
|
|
models.Index(
|
|
fields=["user", "created_at"],
|
|
name="idx_ra_attachment_user_created",
|
|
),
|
|
models.Index(
|
|
fields=["conversation", "original_name", "is_active"],
|
|
name="idx_ra_attachment_active",
|
|
),
|
|
]
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.original_name} v{self.version_no}"
|
|
|
|
|
|
class FileSummaryBatch(models.Model):
|
|
"""Tracks one automatic file inventory and page-count workflow run."""
|
|
|
|
class Status(models.TextChoices):
|
|
PENDING = "pending", "待执行"
|
|
RUNNING = "running", "执行中"
|
|
SUCCESS = "success", "成功"
|
|
FAILED = "failed", "失败"
|
|
|
|
conversation = models.ForeignKey(
|
|
Conversation,
|
|
on_delete=models.CASCADE,
|
|
related_name="file_summary_batches",
|
|
)
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name="review_file_summary_batches",
|
|
)
|
|
trigger_message = models.ForeignKey(
|
|
Message,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="triggered_file_summary_batches",
|
|
)
|
|
batch_no = models.CharField(max_length=64, unique=True)
|
|
product_name = models.CharField(max_length=200, blank=True, default="")
|
|
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
|
|
total_files = models.IntegerField(default=0)
|
|
supported_files = models.IntegerField(default=0)
|
|
success_files = models.IntegerField(default=0)
|
|
failed_files = models.IntegerField(default=0)
|
|
unsupported_files = models.IntegerField(default=0)
|
|
uncertain_files = models.IntegerField(default=0)
|
|
total_pages = models.IntegerField(default=0)
|
|
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_file_summary_batch"
|
|
ordering = ["-created_at", "-id"]
|
|
indexes = [
|
|
models.Index(fields=["conversation", "created_at"], name="idx_ra_batch_conv_created"),
|
|
models.Index(fields=["user", "created_at"], name="idx_ra_batch_user_created"),
|
|
models.Index(fields=["status", "created_at"], name="idx_ra_batch_status"),
|
|
]
|
|
|
|
def __str__(self) -> str:
|
|
return self.batch_no
|
|
|
|
|
|
class FileSummaryBatchAttachment(models.Model):
|
|
"""Binds a workflow batch to the exact attachment versions it uses."""
|
|
|
|
class SourceRole(models.TextChoices):
|
|
ARCHIVE = "archive", "压缩包"
|
|
MULTI_FILE = "multi_file", "多文件"
|
|
|
|
batch = models.ForeignKey(
|
|
FileSummaryBatch,
|
|
on_delete=models.CASCADE,
|
|
related_name="batch_attachments",
|
|
)
|
|
attachment = models.ForeignKey(
|
|
FileAttachment,
|
|
on_delete=models.CASCADE,
|
|
related_name="batch_bindings",
|
|
)
|
|
source_role = models.CharField(
|
|
max_length=20,
|
|
choices=SourceRole.choices,
|
|
default=SourceRole.MULTI_FILE,
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = "ra_file_summary_batch_attachment"
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["batch", "attachment"],
|
|
name="uq_ra_batch_attachment",
|
|
)
|
|
]
|
|
indexes = [
|
|
models.Index(
|
|
fields=["batch", "created_at"],
|
|
name="idx_ra_batch_attachment_batch",
|
|
),
|
|
models.Index(fields=["attachment"], name="idx_ra_batch_attach_file"),
|
|
]
|
|
|
|
|
|
class FileSummaryItem(models.Model):
|
|
"""Stores one scanned file and its page-count result."""
|
|
|
|
class StatisticsStatus(models.TextChoices):
|
|
SUCCESS = "success", "成功"
|
|
FAILED = "failed", "失败"
|
|
UNSUPPORTED = "unsupported", "不支持"
|
|
UNCERTAIN = "uncertain", "不确定"
|
|
SKIPPED = "skipped", "跳过"
|
|
|
|
batch = models.ForeignKey(
|
|
FileSummaryBatch,
|
|
on_delete=models.CASCADE,
|
|
related_name="items",
|
|
)
|
|
file_index = models.PositiveIntegerField()
|
|
directory_level = models.CharField(max_length=300, blank=True, default="")
|
|
file_name = models.CharField(max_length=255)
|
|
file_type = models.CharField(max_length=20)
|
|
relative_path = models.CharField(max_length=500)
|
|
storage_path = models.CharField(max_length=500)
|
|
page_count = models.IntegerField(null=True, blank=True)
|
|
statistics_status = models.CharField(
|
|
max_length=20,
|
|
choices=StatisticsStatus.choices,
|
|
default=StatisticsStatus.SKIPPED,
|
|
)
|
|
retry_count = models.PositiveIntegerField(default=0)
|
|
error_message = models.TextField(blank=True, default="")
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = "ra_file_summary_item"
|
|
ordering = ["file_index", "id"]
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["batch", "relative_path"],
|
|
name="uq_ra_item_batch_relative_path",
|
|
)
|
|
]
|
|
indexes = [
|
|
models.Index(fields=["batch", "file_index"], name="idx_ra_item_batch_index"),
|
|
models.Index(fields=["batch", "statistics_status"], name="idx_ra_item_batch_status"),
|
|
models.Index(fields=["batch", "file_type"], name="idx_ra_item_batch_type"),
|
|
]
|
|
|
|
|
|
class WorkflowNodeRun(models.Model):
|
|
"""Stores recoverable status for one workflow node."""
|
|
|
|
class Status(models.TextChoices):
|
|
PENDING = "pending", "等待中"
|
|
RUNNING = "running", "执行中"
|
|
RETRYING = "retrying", "重试中"
|
|
SUCCESS = "success", "成功"
|
|
FAILED = "failed", "失败"
|
|
SKIPPED = "skipped", "跳过"
|
|
|
|
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)
|
|
progress = models.PositiveIntegerField(default=0)
|
|
message = models.TextField(blank=True, default="")
|
|
started_at = models.DateTimeField(null=True, blank=True)
|
|
finished_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
db_table = "ra_workflow_node_run"
|
|
constraints = [
|
|
models.UniqueConstraint(fields=["batch", "node_code"], name="uq_ra_node_batch_code")
|
|
]
|
|
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",
|
|
),
|
|
]
|
|
|
|
|
|
class WorkflowEvent(models.Model):
|
|
"""Persists workflow events for SSE replay and diagnostics."""
|
|
|
|
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)
|
|
|
|
class Meta:
|
|
db_table = "ra_workflow_event"
|
|
ordering = ["id"]
|
|
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",
|
|
),
|
|
]
|
|
|
|
|
|
class ExportedSummaryFile(models.Model):
|
|
"""Stores generated report files for permission-checked download."""
|
|
|
|
class ExportType(models.TextChoices):
|
|
MARKDOWN = "markdown", "Markdown"
|
|
EXCEL = "excel", "Excel"
|
|
JSON = "json", "JSON"
|
|
|
|
class Status(models.TextChoices):
|
|
SUCCESS = "success", "成功"
|
|
FAILED = "failed", "失败"
|
|
|
|
batch = models.ForeignKey(
|
|
FileSummaryBatch,
|
|
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)
|
|
status = models.CharField(max_length=20, choices=Status.choices, default=Status.SUCCESS)
|
|
error_message = models.TextField(blank=True, default="")
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = "ra_exported_summary_file"
|
|
ordering = ["-created_at", "-id"]
|
|
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"),
|
|
]
|