feat(regulatory-info-package): 接入对话和前端卡片
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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。"
|
||||||
"可选 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 用于用户要求阅读、提取、分析、总结、查看上传附件内容。"
|
"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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user