feat(regulatory-info-package): 接入对话和前端卡片

This commit is contained in:
2026-06-10 19:50:03 +08:00
parent dac8ce3c14
commit dcd829e821
6 changed files with 156 additions and 3 deletions

View File

@@ -19,6 +19,12 @@ from .application_form_fill.workflow import (
find_latest_successful_summary_batch as find_latest_successful_form_fill_summary_batch, find_latest_successful_summary_batch as find_latest_successful_form_fill_summary_batch,
start_application_form_fill_workflow, 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 ( from .regulatory_review.workflow import (
create_regulatory_review_batch, create_regulatory_review_batch,
find_latest_successful_summary_batch, find_latest_successful_summary_batch,
@@ -342,6 +348,56 @@ def stream_message(conversation: Conversation, content: str):
) )
return 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: if route.starts_regulatory_review:
source_summary_batch = find_latest_successful_summary_batch(conversation) source_summary_batch = find_latest_successful_summary_batch(conversation)
if not source_summary_batch: if not source_summary_batch:

View File

@@ -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 .application_form_fill.constants import FORM_FILL_TRIGGER_KEYWORDS, WORKFLOW_TYPE as FORM_FILL_WORKFLOW_TYPE
from .llm import LLMConfigurationError, LLMRequestError, generate_completion from .llm import LLMConfigurationError, LLMRequestError, generate_completion
from .models import Conversation, FileAttachment 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__) logger = logging.getLogger(__name__)
@@ -18,6 +22,7 @@ logger = logging.getLogger(__name__)
ROUTE_ACTIONS = {"normal_chat", "attachment_reader", "file_summary"} ROUTE_ACTIONS = {"normal_chat", "attachment_reader", "file_summary"}
ROUTE_ACTIONS.add("regulatory_review") ROUTE_ACTIONS.add("regulatory_review")
ROUTE_ACTIONS.add(FORM_FILL_WORKFLOW_TYPE) ROUTE_ACTIONS.add(FORM_FILL_WORKFLOW_TYPE)
ROUTE_ACTIONS.add(REGULATORY_INFO_PACKAGE_WORKFLOW_TYPE)
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -45,6 +50,10 @@ class SkillRoute:
def starts_application_form_fill(self) -> bool: def starts_application_form_fill(self) -> bool:
return self.action == FORM_FILL_WORKFLOW_TYPE return self.action == FORM_FILL_WORKFLOW_TYPE
@property
def starts_regulatory_info_package(self) -> bool:
return self.action == REGULATORY_INFO_PACKAGE_WORKFLOW_TYPE
@property @property
def is_normal_chat(self) -> bool: def is_normal_chat(self) -> bool:
return self.action == "normal_chat" 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: 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): if _matches_application_form_fill(content):
return SkillRoute( return SkillRoute(
action=FORM_FILL_WORKFLOW_TYPE, action=FORM_FILL_WORKFLOW_TYPE,
@@ -144,7 +161,9 @@ def _route_with_llm(
return SkillRoute( return SkillRoute(
action=action, action=action,
skill_name="attachment_reader" if action == "attachment_reader" else "", 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")), confidence=_float_or_zero(payload.get("confidence")),
reason=str(payload.get("reason") or ""), reason=str(payload.get("reason") or ""),
source="llm", source="llm",
@@ -152,6 +171,15 @@ def _route_with_llm(
def _route_with_rules(conversation: Conversation, content: str) -> SkillRoute: 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): if _matches_application_form_fill(content):
return SkillRoute( return SkillRoute(
action=FORM_FILL_WORKFLOW_TYPE, action=FORM_FILL_WORKFLOW_TYPE,
@@ -210,11 +238,12 @@ def _router_system_prompt() -> str:
return ( return (
"你是审核智能体的工具路由器,只判断是否需要调用工具,不直接回答用户。" "你是审核智能体的工具路由器,只判断是否需要调用工具,不直接回答用户。"
"你必须只输出 JSON 对象,不要输出 Markdown。" "你必须只输出 JSON 对象,不要输出 Markdown。"
"可选 actionnormal_chat、attachment_reader、file_summary、regulatory_review、application_form_fill。" "可选 actionnormal_chat、attachment_reader、file_summary、regulatory_review、application_form_fill、regulatory_info_package"
"attachment_reader 用于用户要求阅读、提取、分析、总结、查看上传附件内容。" "attachment_reader 用于用户要求阅读、提取、分析、总结、查看上传附件内容。"
"file_summary 用于用户要求自动汇总文件目录、页数、清单或生成目录页数报告。" "file_summary 用于用户要求自动汇总文件目录、页数、清单或生成目录页数报告。"
"regulatory_review 用于用户要求法规核查、NMPA核查、完整性核查、章节一致性核查、风险预警或整改建议。" "regulatory_review 用于用户要求法规核查、NMPA核查、完整性核查、章节一致性核查、风险预警或整改建议。"
"application_form_fill 用于用户要求填注册证、生成申报模板、填写对应表格、安全和性能基本原则清单或自动填表。" "application_form_fill 用于用户要求填注册证、生成申报模板、填写对应表格、安全和性能基本原则清单或自动填表。"
"regulatory_info_package 用于用户要求根据说明书生成第1章监管信息、监管信息材料包、申请表、产品列表或声明材料包。"
"normal_chat 用于不需要读取附件或执行工作流的一般问答。" "normal_chat 用于不需要读取附件或执行工作流的一般问答。"
"输出字段action、confidence、reason。" "输出字段action、confidence、reason。"
) )
@@ -268,6 +297,11 @@ def _matches_regulatory_review(content: str) -> bool:
return any(keyword in normalized for keyword in keywords) 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: def _matches_application_form_fill(content: str) -> bool:
normalized = content.lower() normalized = content.lower()
return any(keyword.lower() in normalized for keyword in FORM_FILL_TRIGGER_KEYWORDS) return any(keyword.lower() in normalized for keyword in FORM_FILL_TRIGGER_KEYWORDS)

View File

@@ -21,6 +21,10 @@ from .application_form_fill.views import (
batch_status as application_form_fill_batch_status, batch_status as application_form_fill_batch_status,
start as application_form_fill_start, 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 ( from .views import (
knowledge_base_document_detail, knowledge_base_document_detail,
knowledge_base_document_index, knowledge_base_document_index,
@@ -112,6 +116,16 @@ urlpatterns = [
application_form_fill_batch_status, application_form_fill_batch_status,
name="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/<int:batch_id>/status/",
regulatory_info_package_batch_status,
name="regulatory_info_package_batch_status",
),
path( path(
"api/review-agent/knowledge-base/status/", "api/review-agent/knowledge-base/status/",
knowledge_base_status, knowledge_base_status,

View File

@@ -16,7 +16,15 @@ from .services import (
send_message, send_message,
stream_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, search_knowledge_base
from .knowledge_base import ( from .knowledge_base import (
build_knowledge_base_context_for_user, 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] 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) 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]: def build_home_dashboard_context(user) -> dict[str, object]:
conversations = Conversation.objects.filter(user=user) conversations = Conversation.objects.filter(user=user)
active_attachments = FileAttachment.objects.filter(user=user).exclude( active_attachments = FileAttachment.objects.filter(user=user).exclude(

View File

@@ -517,6 +517,8 @@
attributeName = "data-regulatory-status-url-template"; attributeName = "data-regulatory-status-url-template";
} else if (workflow_type === "application_form_fill") { } else if (workflow_type === "application_form_fill") {
attributeName = "data-application-form-fill-status-url-template"; 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); return templateUrl(attributeName, "__batch_id__", batchId);
} }

View File

@@ -225,6 +225,11 @@
type="button" type="button"
data-prompt-template="请基于当前对话最近成功汇总的产品资料,自动提取产品关键信息并填入申报文件模板" data-prompt-template="请基于当前对话最近成功汇总的产品资料,自动提取产品关键信息并填入申报文件模板"
>申报文件填表</button> >申报文件填表</button>
<button
class="tool-chip"
type="button"
data-prompt-template="根据说明书生成第1章监管信息"
>第1章监管信息</button>
</div> </div>
<button class="send-button" type="submit" id="sendButton">发送</button> <button class="send-button" type="submit" id="sendButton">发送</button>
</div> </div>
@@ -241,6 +246,7 @@
data-status-url-template="/api/review-agent/file-summary/__batch_id__/status/" 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-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-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/" data-events-url-template="/api/review-agent/file-summary/__batch_id__/events/"
> >
<section class="summary-section upload-section"> <section class="summary-section upload-section">