From dcd829e821359d8a6a46098519a177a16efed6ed Mon Sep 17 00:00:00 2001 From: bruce Date: Wed, 10 Jun 2026 19:50:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(regulatory-info-package):=20=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E5=AF=B9=E8=AF=9D=E5=92=8C=E5=89=8D=E7=AB=AF=E5=8D=A1?= =?UTF-8?q?=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- review_agent/services.py | 56 ++++++++++++++++++++++++++++++++++++ review_agent/skill_router.py | 38 ++++++++++++++++++++++-- review_agent/urls.py | 14 +++++++++ review_agent/views.py | 43 ++++++++++++++++++++++++++- static/js/app.js | 2 ++ templates/home.html | 6 ++++ 6 files changed, 156 insertions(+), 3 deletions(-) diff --git a/review_agent/services.py b/review_agent/services.py index 0bd9c7e..bd12ad8 100644 --- a/review_agent/services.py +++ b/review_agent/services.py @@ -19,6 +19,12 @@ from .application_form_fill.workflow import ( find_latest_successful_summary_batch as find_latest_successful_form_fill_summary_batch, start_application_form_fill_workflow, ) +from .regulatory_info_package.constants import WORKFLOW_TYPE as REGULATORY_INFO_PACKAGE_WORKFLOW_TYPE +from .regulatory_info_package.services.input_select import select_instruction_input +from .regulatory_info_package.workflow import ( + create_regulatory_info_package_batch, + start_regulatory_info_package_workflow, +) from .regulatory_review.workflow import ( create_regulatory_review_batch, find_latest_successful_summary_batch, @@ -342,6 +348,56 @@ def stream_message(conversation: Conversation, content: str): ) return + if route.starts_regulatory_info_package: + selection = select_instruction_input(conversation, content) + if selection.status != "selected": + reply_content = selection.message or "请先在当前对话右侧上传产品说明书 docx 文件,然后再发送第1章监管信息生成指令。" + assistant_message = append_assistant_message(conversation, reply_content) + yield sse_event("chunk", {"delta": reply_content}) + yield sse_event( + "done", + { + "assistant_message_id": assistant_message.pk, + "conversation_id": conversation.pk, + "title": conversation.title, + }, + ) + return + batch = create_regulatory_info_package_batch( + conversation=conversation, + user=conversation.user, + trigger_message=user_message, + source_attachment=selection.attachment, + source_summary_batch=selection.source_summary_batch, + source_summary_item_id=selection.source_summary_item_id, + source_file_name=selection.file_name, + source_storage_path=selection.storage_path, + ) + start_regulatory_info_package_workflow( + batch, + async_run=getattr(settings, "REGULATORY_INFO_PACKAGE_ASYNC", True), + ) + reply_content = f"已启动第1章监管信息材料包生成工作流,批次号:{batch.batch_no}。" + assistant_message = append_assistant_message(conversation, reply_content) + yield sse_event( + "workflow_started", + { + "workflow_type": REGULATORY_INFO_PACKAGE_WORKFLOW_TYPE, + "batch_id": batch.pk, + "batch_no": batch.batch_no, + }, + ) + yield sse_event("chunk", {"delta": reply_content}) + yield sse_event( + "done", + { + "assistant_message_id": assistant_message.pk, + "conversation_id": conversation.pk, + "title": conversation.title, + }, + ) + return + if route.starts_regulatory_review: source_summary_batch = find_latest_successful_summary_batch(conversation) if not source_summary_batch: diff --git a/review_agent/skill_router.py b/review_agent/skill_router.py index 24e668a..99d29c8 100644 --- a/review_agent/skill_router.py +++ b/review_agent/skill_router.py @@ -11,6 +11,10 @@ from .file_summary.workflow_trigger import ( from .application_form_fill.constants import FORM_FILL_TRIGGER_KEYWORDS, WORKFLOW_TYPE as FORM_FILL_WORKFLOW_TYPE from .llm import LLMConfigurationError, LLMRequestError, generate_completion from .models import Conversation, FileAttachment +from .regulatory_info_package.constants import ( + REGULATORY_INFO_PACKAGE_TRIGGER_KEYWORDS, + WORKFLOW_TYPE as REGULATORY_INFO_PACKAGE_WORKFLOW_TYPE, +) logger = logging.getLogger(__name__) @@ -18,6 +22,7 @@ logger = logging.getLogger(__name__) ROUTE_ACTIONS = {"normal_chat", "attachment_reader", "file_summary"} ROUTE_ACTIONS.add("regulatory_review") ROUTE_ACTIONS.add(FORM_FILL_WORKFLOW_TYPE) +ROUTE_ACTIONS.add(REGULATORY_INFO_PACKAGE_WORKFLOW_TYPE) @dataclass(frozen=True) @@ -45,6 +50,10 @@ class SkillRoute: def starts_application_form_fill(self) -> bool: return self.action == FORM_FILL_WORKFLOW_TYPE + @property + def starts_regulatory_info_package(self) -> bool: + return self.action == REGULATORY_INFO_PACKAGE_WORKFLOW_TYPE + @property def is_normal_chat(self) -> bool: return self.action == "normal_chat" @@ -80,6 +89,14 @@ def route_message_intent(conversation: Conversation, content: str) -> SkillRoute def _deterministic_workflow_route(conversation: Conversation, content: str) -> SkillRoute | None: + if _matches_regulatory_info_package(content): + return SkillRoute( + action=REGULATORY_INFO_PACKAGE_WORKFLOW_TYPE, + workflow_type=REGULATORY_INFO_PACKAGE_WORKFLOW_TYPE, + confidence=0.9, + reason="命中明确第1章监管信息材料包生成关键词。", + source="rule_preflight", + ) if _matches_application_form_fill(content): return SkillRoute( action=FORM_FILL_WORKFLOW_TYPE, @@ -144,7 +161,9 @@ def _route_with_llm( return SkillRoute( action=action, skill_name="attachment_reader" if action == "attachment_reader" else "", - workflow_type=action if action in {"file_summary", "regulatory_review", FORM_FILL_WORKFLOW_TYPE} else "", + workflow_type=action + if action in {"file_summary", "regulatory_review", FORM_FILL_WORKFLOW_TYPE, REGULATORY_INFO_PACKAGE_WORKFLOW_TYPE} + else "", confidence=_float_or_zero(payload.get("confidence")), reason=str(payload.get("reason") or ""), source="llm", @@ -152,6 +171,15 @@ def _route_with_llm( def _route_with_rules(conversation: Conversation, content: str) -> SkillRoute: + if _matches_regulatory_info_package(content): + return SkillRoute( + action=REGULATORY_INFO_PACKAGE_WORKFLOW_TYPE, + workflow_type=REGULATORY_INFO_PACKAGE_WORKFLOW_TYPE, + confidence=0.7, + reason="命中第1章监管信息材料包生成关键词。", + source="rule_fallback", + ) + if _matches_application_form_fill(content): return SkillRoute( action=FORM_FILL_WORKFLOW_TYPE, @@ -210,11 +238,12 @@ def _router_system_prompt() -> str: return ( "你是审核智能体的工具路由器,只判断是否需要调用工具,不直接回答用户。" "你必须只输出 JSON 对象,不要输出 Markdown。" - "可选 action:normal_chat、attachment_reader、file_summary、regulatory_review、application_form_fill。" + "可选 action:normal_chat、attachment_reader、file_summary、regulatory_review、application_form_fill、regulatory_info_package。" "attachment_reader 用于用户要求阅读、提取、分析、总结、查看上传附件内容。" "file_summary 用于用户要求自动汇总文件目录、页数、清单或生成目录页数报告。" "regulatory_review 用于用户要求法规核查、NMPA核查、完整性核查、章节一致性核查、风险预警或整改建议。" "application_form_fill 用于用户要求填注册证、生成申报模板、填写对应表格、安全和性能基本原则清单或自动填表。" + "regulatory_info_package 用于用户要求根据说明书生成第1章监管信息、监管信息材料包、申请表、产品列表或声明材料包。" "normal_chat 用于不需要读取附件或执行工作流的一般问答。" "输出字段:action、confidence、reason。" ) @@ -268,6 +297,11 @@ def _matches_regulatory_review(content: str) -> bool: return any(keyword in normalized for keyword in keywords) +def _matches_regulatory_info_package(content: str) -> bool: + normalized = "".join((content or "").lower().split()) + return any("".join(keyword.lower().split()) in normalized for keyword in REGULATORY_INFO_PACKAGE_TRIGGER_KEYWORDS) + + def _matches_application_form_fill(content: str) -> bool: normalized = content.lower() return any(keyword.lower() in normalized for keyword in FORM_FILL_TRIGGER_KEYWORDS) diff --git a/review_agent/urls.py b/review_agent/urls.py index 4d46250..59aa2c1 100644 --- a/review_agent/urls.py +++ b/review_agent/urls.py @@ -21,6 +21,10 @@ from .application_form_fill.views import ( batch_status as application_form_fill_batch_status, start as application_form_fill_start, ) +from .regulatory_info_package.views import ( + batch_status as regulatory_info_package_batch_status, + start as regulatory_info_package_start, +) from .views import ( knowledge_base_document_detail, knowledge_base_document_index, @@ -112,6 +116,16 @@ urlpatterns = [ application_form_fill_batch_status, name="application_form_fill_batch_status", ), + path( + "api/review-agent/regulatory-info-package/start/", + regulatory_info_package_start, + name="regulatory_info_package_start", + ), + path( + "api/review-agent/regulatory-info-package//status/", + regulatory_info_package_batch_status, + name="regulatory_info_package_batch_status", + ), path( "api/review-agent/knowledge-base/status/", knowledge_base_status, diff --git a/review_agent/views.py b/review_agent/views.py index 2933923..5613cdd 100644 --- a/review_agent/views.py +++ b/review_agent/views.py @@ -16,7 +16,15 @@ from .services import ( send_message, stream_message, ) -from .models import ApplicationFormFillBatch, Conversation, FileAttachment, FileSummaryBatch, RegulatoryReviewBatch, WorkflowNodeRun +from .models import ( + ApplicationFormFillBatch, + Conversation, + FileAttachment, + FileSummaryBatch, + RegulatoryInfoPackageBatch, + RegulatoryReviewBatch, + WorkflowNodeRun, +) from .knowledge_base import build_knowledge_base_context, search_knowledge_base from .knowledge_base import ( build_knowledge_base_context_for_user, @@ -329,6 +337,25 @@ def build_workflow_cards(conversation: Conversation) -> list[dict[str, object]]: ), } ) + rip_batches = RegulatoryInfoPackageBatch.objects.filter(conversation=conversation, is_deleted=False) + for batch in rip_batches: + cards.append( + { + "id": batch.pk, + "workflow_type": "regulatory_info_package", + "batch_no": batch.batch_no, + "status": batch.status, + "error_message": batch.error_message, + "risk_label": _format_regulatory_info_package_label(batch), + "created_at": batch.created_at, + "nodes": list( + WorkflowNodeRun.objects.filter( + workflow_type="regulatory_info_package", + workflow_batch_id=batch.pk, + ).order_by("id") + ), + } + ) return sorted(cards, key=lambda item: item["created_at"], reverse=True)[:5] @@ -374,6 +401,20 @@ def _format_form_fill_label(batch: ApplicationFormFillBatch) -> str: return " · ".join(parts) +def _format_regulatory_info_package_label(batch: RegulatoryInfoPackageBatch) -> str: + parts = [] + if batch.product_name: + parts.append(batch.product_name) + if batch.generated_files: + success_count = sum(1 for item in batch.generated_files if item.get("status") in {"success", "fallback_success"}) + parts.append(f"生成 {success_count}/7") + if batch.missing_fields: + parts.append(f"缺失 {len(batch.missing_fields)}") + if batch.conflict_fields: + parts.append(f"冲突 {len(batch.conflict_fields)}") + return " · ".join(parts) + + def build_home_dashboard_context(user) -> dict[str, object]: conversations = Conversation.objects.filter(user=user) active_attachments = FileAttachment.objects.filter(user=user).exclude( diff --git a/static/js/app.js b/static/js/app.js index 015a1f5..f99d460 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -517,6 +517,8 @@ attributeName = "data-regulatory-status-url-template"; } else if (workflow_type === "application_form_fill") { attributeName = "data-application-form-fill-status-url-template"; + } else if (workflow_type === "regulatory_info_package") { + attributeName = "data-regulatory-info-package-status-url-template"; } return templateUrl(attributeName, "__batch_id__", batchId); } diff --git a/templates/home.html b/templates/home.html index 467b64b..f5ba5eb 100644 --- a/templates/home.html +++ b/templates/home.html @@ -225,6 +225,11 @@ type="button" data-prompt-template="请基于当前对话最近成功汇总的产品资料,自动提取产品关键信息并填入申报文件模板" >申报文件填表 + @@ -241,6 +246,7 @@ data-status-url-template="/api/review-agent/file-summary/__batch_id__/status/" data-regulatory-status-url-template="/api/review-agent/regulatory-review/__batch_id__/status/" data-application-form-fill-status-url-template="/api/review-agent/application-form-fill/__batch_id__/status/" + data-regulatory-info-package-status-url-template="/api/review-agent/regulatory-info-package/__batch_id__/status/" data-events-url-template="/api/review-agent/file-summary/__batch_id__/events/" >