feat(regulatory): 增加mock通知留痕
This commit is contained in:
@@ -75,6 +75,13 @@ def build_markdown_report(batch: RegulatoryReviewBatch) -> str:
|
|||||||
passed = sum(1 for item in items if item.get("status") == RegulatoryIssue.Status.REVIEW_PASSED)
|
passed = sum(1 for item in items if item.get("status") == RegulatoryIssue.Status.REVIEW_PASSED)
|
||||||
failed = sum(1 for item in items if item.get("status") == RegulatoryIssue.Status.REVIEW_FAILED)
|
failed = sum(1 for item in items if item.get("status") == RegulatoryIssue.Status.REVIEW_FAILED)
|
||||||
lines.append(f"| {record.get('file_summary_batch_no')} | {len(items)} | {passed} | {failed} |")
|
lines.append(f"| {record.get('file_summary_batch_no')} | {len(items)} | {passed} | {failed} |")
|
||||||
|
notifications = _notification_records(batch)
|
||||||
|
if notifications:
|
||||||
|
lines.extend(["", "## 通知记录", "", "| 渠道 | 对象 | 状态 | 问题 |", "| --- | --- | --- | --- |"])
|
||||||
|
for record in notifications:
|
||||||
|
lines.append(
|
||||||
|
f"| {record['channel']} | {record['target'] or '-'} | {record['status']} | {record['payload'].get('title', '-')} |"
|
||||||
|
)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -99,6 +106,7 @@ def build_result_payload(batch: RegulatoryReviewBatch) -> dict[str, object]:
|
|||||||
for issue in batch.issues.order_by("id")
|
for issue in batch.issues.order_by("id")
|
||||||
],
|
],
|
||||||
"review_records": _review_records(batch),
|
"review_records": _review_records(batch),
|
||||||
|
"notifications": _notification_records(batch),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -159,7 +167,7 @@ def _create_excel_export(batch: RegulatoryReviewBatch, path: Path) -> ExportedSu
|
|||||||
workbook = Workbook()
|
workbook = Workbook()
|
||||||
sheet = workbook.active
|
sheet = workbook.active
|
||||||
sheet.title = "法规问题清单"
|
sheet.title = "法规问题清单"
|
||||||
sheet.append(["等级", "类别", "规则", "问题", "状态", "建议", "法规依据"])
|
sheet.append(["等级", "类别", "规则", "问题", "状态", "建议", "法规依据", "通知记录"])
|
||||||
for issue in batch.issues.order_by("id"):
|
for issue in batch.issues.order_by("id"):
|
||||||
sheet.append(
|
sheet.append(
|
||||||
[
|
[
|
||||||
@@ -170,6 +178,7 @@ def _create_excel_export(batch: RegulatoryReviewBatch, path: Path) -> ExportedSu
|
|||||||
issue.status,
|
issue.status,
|
||||||
issue.suggestion,
|
issue.suggestion,
|
||||||
"; ".join(str(item.get("source", "")) for item in issue.citations),
|
"; ".join(str(item.get("source", "")) for item in issue.citations),
|
||||||
|
_notification_summary_for_issue(batch, issue.pk),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
workbook.save(path)
|
workbook.save(path)
|
||||||
@@ -192,3 +201,25 @@ def _review_records(batch: RegulatoryReviewBatch) -> list[dict[str, object]]:
|
|||||||
except (OSError, json.JSONDecodeError):
|
except (OSError, json.JSONDecodeError):
|
||||||
continue
|
continue
|
||||||
return records
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
def _notification_records(batch: RegulatoryReviewBatch) -> list[dict[str, object]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"channel": record.channel,
|
||||||
|
"target": record.target,
|
||||||
|
"status": record.status,
|
||||||
|
"payload": record.payload,
|
||||||
|
"sent_at": record.sent_at.isoformat() if record.sent_at else "",
|
||||||
|
}
|
||||||
|
for record in batch.notifications.order_by("created_at", "id")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _notification_summary_for_issue(batch: RegulatoryReviewBatch, issue_id: int) -> str:
|
||||||
|
records = [
|
||||||
|
record
|
||||||
|
for record in batch.notifications.all()
|
||||||
|
if isinstance(record.payload, dict) and record.payload.get("issue_id") == issue_id
|
||||||
|
]
|
||||||
|
return "; ".join(f"{record.channel}:{record.status}" for record in records)
|
||||||
|
|||||||
39
review_agent/regulatory_review/services/feishu_notifier.py
Normal file
39
review_agent/regulatory_review/services/feishu_notifier.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from review_agent.models import RegulatoryNotificationRecord, RegulatoryReviewBatch
|
||||||
|
|
||||||
|
|
||||||
|
NOTIFIABLE_SEVERITIES = {"blocking", "high", "medium"}
|
||||||
|
|
||||||
|
|
||||||
|
def create_mock_notifications(batch: RegulatoryReviewBatch) -> list[RegulatoryNotificationRecord]:
|
||||||
|
records = []
|
||||||
|
existing_issue_ids = {
|
||||||
|
item.get("issue_id")
|
||||||
|
for item in RegulatoryNotificationRecord.objects.filter(batch=batch, channel=RegulatoryNotificationRecord.Channel.MOCK).values_list(
|
||||||
|
"payload", flat=True
|
||||||
|
)
|
||||||
|
if isinstance(item, dict)
|
||||||
|
}
|
||||||
|
for issue in batch.issues.order_by("id"):
|
||||||
|
if issue.severity not in NOTIFIABLE_SEVERITIES or issue.pk in existing_issue_ids:
|
||||||
|
continue
|
||||||
|
records.append(
|
||||||
|
RegulatoryNotificationRecord.objects.create(
|
||||||
|
batch=batch,
|
||||||
|
channel=RegulatoryNotificationRecord.Channel.MOCK,
|
||||||
|
target="法规整改负责人",
|
||||||
|
status=RegulatoryNotificationRecord.Status.SENT,
|
||||||
|
sent_at=timezone.now(),
|
||||||
|
payload={
|
||||||
|
"issue_id": issue.pk,
|
||||||
|
"rule_code": issue.rule_code,
|
||||||
|
"severity": issue.severity,
|
||||||
|
"title": issue.title,
|
||||||
|
"suggestion": issue.suggestion,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return records
|
||||||
@@ -20,6 +20,7 @@ from review_agent.models import (
|
|||||||
from review_agent.regulatory_review.services.completeness_check import run_completeness_check
|
from review_agent.regulatory_review.services.completeness_check import run_completeness_check
|
||||||
from review_agent.regulatory_review.services.consistency_check import run_consistency_check
|
from review_agent.regulatory_review.services.consistency_check import run_consistency_check
|
||||||
from review_agent.regulatory_review.services.export import build_assistant_summary, export_review_results
|
from review_agent.regulatory_review.services.export import build_assistant_summary, export_review_results
|
||||||
|
from review_agent.regulatory_review.services.feishu_notifier import create_mock_notifications
|
||||||
from review_agent.regulatory_review.services.info_extract import detect_regulatory_condition_candidates
|
from review_agent.regulatory_review.services.info_extract import detect_regulatory_condition_candidates
|
||||||
from review_agent.regulatory_review.services.risk_assess import persist_findings
|
from review_agent.regulatory_review.services.risk_assess import persist_findings
|
||||||
from review_agent.regulatory_review.services.rule_loader import load_rule_file
|
from review_agent.regulatory_review.services.rule_loader import load_rule_file
|
||||||
@@ -195,6 +196,7 @@ class RegulatoryWorkflowExecutor:
|
|||||||
return
|
return
|
||||||
if node_code == "risk_assess":
|
if node_code == "risk_assess":
|
||||||
issues = persist_findings(self.batch, self.findings)
|
issues = persist_findings(self.batch, self.findings)
|
||||||
|
create_mock_notifications(self.batch)
|
||||||
save_artifact(
|
save_artifact(
|
||||||
self.batch,
|
self.batch,
|
||||||
name="rag_result_json.json",
|
name="rag_result_json.json",
|
||||||
|
|||||||
79
tests/test_regulatory_notification.py
Normal file
79
tests/test_regulatory_notification.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from review_agent.models import (
|
||||||
|
Conversation,
|
||||||
|
FileSummaryBatch,
|
||||||
|
RegulatoryIssue,
|
||||||
|
RegulatoryNotificationRecord,
|
||||||
|
RegulatoryReviewBatch,
|
||||||
|
)
|
||||||
|
from review_agent.regulatory_review.services.export import build_markdown_report, build_result_payload
|
||||||
|
from review_agent.regulatory_review.services.feishu_notifier import create_mock_notifications
|
||||||
|
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_mock_notifications_for_medium_and_above(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-NOTIFY",
|
||||||
|
status=FileSummaryBatch.Status.SUCCESS,
|
||||||
|
)
|
||||||
|
batch = RegulatoryReviewBatch.objects.create(
|
||||||
|
conversation=conversation,
|
||||||
|
user=user,
|
||||||
|
source_summary_batch=summary,
|
||||||
|
batch_no="RR-NOTIFY",
|
||||||
|
)
|
||||||
|
high = RegulatoryIssue.objects.create(
|
||||||
|
batch=batch,
|
||||||
|
rule_code="attachment4_1_2_application_form",
|
||||||
|
category=RegulatoryIssue.Category.COMPLETENESS,
|
||||||
|
severity=RegulatoryIssue.Severity.HIGH,
|
||||||
|
title="缺少申请表",
|
||||||
|
)
|
||||||
|
RegulatoryIssue.objects.create(
|
||||||
|
batch=batch,
|
||||||
|
rule_code="info",
|
||||||
|
category=RegulatoryIssue.Category.RAG,
|
||||||
|
severity=RegulatoryIssue.Severity.INFO,
|
||||||
|
title="提示项",
|
||||||
|
)
|
||||||
|
|
||||||
|
records = create_mock_notifications(batch)
|
||||||
|
|
||||||
|
assert len(records) == 1
|
||||||
|
assert records[0].channel == RegulatoryNotificationRecord.Channel.MOCK
|
||||||
|
assert records[0].status == RegulatoryNotificationRecord.Status.SENT
|
||||||
|
assert records[0].payload["issue_id"] == high.pk
|
||||||
|
|
||||||
|
|
||||||
|
def test_notification_records_enter_reports(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-NOTIFY",
|
||||||
|
status=FileSummaryBatch.Status.SUCCESS,
|
||||||
|
)
|
||||||
|
batch = RegulatoryReviewBatch.objects.create(
|
||||||
|
conversation=conversation,
|
||||||
|
user=user,
|
||||||
|
source_summary_batch=summary,
|
||||||
|
batch_no="RR-NOTIFY",
|
||||||
|
)
|
||||||
|
RegulatoryNotificationRecord.objects.create(
|
||||||
|
batch=batch,
|
||||||
|
channel=RegulatoryNotificationRecord.Channel.MOCK,
|
||||||
|
target="法规整改负责人",
|
||||||
|
status=RegulatoryNotificationRecord.Status.SENT,
|
||||||
|
payload={"title": "缺少申请表", "severity": "high"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "通知记录" in build_markdown_report(batch)
|
||||||
|
assert build_result_payload(batch)["notifications"][0]["channel"] == "mock"
|
||||||
Reference in New Issue
Block a user