feat(application-form-fill): 接入自动填表工作流触发
This commit is contained in:
27
review_agent/application_form_fill/events.py
Normal file
27
review_agent/application_form_fill/events.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from review_agent.application_form_fill.constants import WORKFLOW_TYPE
|
||||
from review_agent.models import ApplicationFormFillBatch, WorkflowEvent
|
||||
|
||||
|
||||
def record_event(
|
||||
batch: ApplicationFormFillBatch,
|
||||
event_type: str,
|
||||
payload: dict | None = None,
|
||||
) -> WorkflowEvent:
|
||||
return WorkflowEvent.objects.create(
|
||||
workflow_type=WORKFLOW_TYPE,
|
||||
workflow_batch_id=batch.pk,
|
||||
conversation=batch.conversation,
|
||||
event_type=event_type,
|
||||
payload=payload or {},
|
||||
)
|
||||
|
||||
|
||||
def serialize_event(event: WorkflowEvent) -> dict[str, object]:
|
||||
return {
|
||||
"id": event.pk,
|
||||
"event_type": event.event_type,
|
||||
"payload": event.payload,
|
||||
"created_at": event.created_at.isoformat(),
|
||||
}
|
||||
@@ -1,21 +1,151 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from review_agent.application_form_fill.constants import FORM_FILL_NODE_DEFINITIONS, WORKFLOW_TYPE
|
||||
import logging
|
||||
from threading import Thread
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from review_agent.application_form_fill.constants import DEFAULT_OUTPUT_TYPES, FORM_FILL_NODE_DEFINITIONS, WORKFLOW_TYPE
|
||||
from review_agent.application_form_fill.events import record_event
|
||||
from review_agent.application_form_fill.storage import build_batch_work_dir
|
||||
from review_agent.models import ApplicationFormFillBatch, Conversation, FileSummaryBatch, Message, WorkflowNodeRun
|
||||
|
||||
|
||||
logger = logging.getLogger("review_agent.application_form_fill.workflow")
|
||||
|
||||
|
||||
def build_batch_no() -> str:
|
||||
return f"AFF-{timezone.localtime().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:6]}"
|
||||
|
||||
|
||||
def find_latest_successful_summary_batch(conversation: Conversation) -> FileSummaryBatch | None:
|
||||
return (
|
||||
FileSummaryBatch.objects.filter(
|
||||
conversation=conversation,
|
||||
status=FileSummaryBatch.Status.SUCCESS,
|
||||
)
|
||||
.order_by("-finished_at", "-created_at", "-id")
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_application_form_fill_batch(
|
||||
*,
|
||||
conversation: Conversation,
|
||||
user,
|
||||
source_summary_batch: FileSummaryBatch,
|
||||
trigger_message: Message | None = None,
|
||||
requested_templates: list[str] | None = None,
|
||||
output_types: list[str] | None = None,
|
||||
) -> ApplicationFormFillBatch:
|
||||
batch_no = build_batch_no()
|
||||
work_dir = build_batch_work_dir(batch_no=batch_no)
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
batch = ApplicationFormFillBatch.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
trigger_message=trigger_message,
|
||||
source_summary_batch=source_summary_batch,
|
||||
batch_no=batch_no,
|
||||
requested_templates=requested_templates or [],
|
||||
output_types=output_types or DEFAULT_OUTPUT_TYPES,
|
||||
work_dir=str(work_dir),
|
||||
)
|
||||
for code, name, group in FORM_FILL_NODE_DEFINITIONS:
|
||||
WorkflowNodeRun.objects.create(
|
||||
workflow_type=WORKFLOW_TYPE,
|
||||
workflow_batch_id=batch.pk,
|
||||
node_group=group,
|
||||
node_code=code,
|
||||
node_name=name,
|
||||
)
|
||||
record_event(batch, "workflow_created", {"batch_id": batch.pk, "batch_no": batch.batch_no})
|
||||
return batch
|
||||
|
||||
|
||||
class FormFillWorkflowExecutor:
|
||||
"""Workflow executor scaffold filled in by later AFF stages."""
|
||||
"""Runs the auto-fill workflow skeleton; later stages fill node bodies."""
|
||||
|
||||
def __init__(self, batch):
|
||||
def __init__(self, batch: ApplicationFormFillBatch):
|
||||
self.batch = batch
|
||||
|
||||
def run(self) -> None:
|
||||
raise NotImplementedError("application_form_fill workflow is implemented in later AFF stages.")
|
||||
logger.info("自动填表工作流开始 batch_no=%s batch_id=%s", self.batch.batch_no, self.batch.pk)
|
||||
self.batch.status = ApplicationFormFillBatch.Status.RUNNING
|
||||
self.batch.started_at = timezone.now()
|
||||
self.batch.save(update_fields=["status", "started_at"])
|
||||
record_event(self.batch, "workflow_started", {"batch_id": self.batch.pk})
|
||||
|
||||
try:
|
||||
for node in self._nodes():
|
||||
if node.status in {WorkflowNodeRun.Status.SUCCESS, WorkflowNodeRun.Status.SKIPPED}:
|
||||
continue
|
||||
self._run_node(node)
|
||||
except Exception as exc:
|
||||
logger.exception("Application form fill workflow failed", extra={"batch_id": self.batch.pk})
|
||||
self.batch.status = ApplicationFormFillBatch.Status.FAILED
|
||||
self.batch.error_message = str(exc)
|
||||
self.batch.finished_at = timezone.now()
|
||||
self.batch.save(update_fields=["status", "error_message", "finished_at"])
|
||||
record_event(self.batch, "workflow_failed", {"message": str(exc)})
|
||||
return
|
||||
|
||||
self.batch.status = ApplicationFormFillBatch.Status.SUCCESS
|
||||
self.batch.finished_at = timezone.now()
|
||||
self.batch.save(update_fields=["status", "finished_at"])
|
||||
record_event(self.batch, "workflow_completed", {"batch_id": self.batch.pk})
|
||||
logger.info("自动填表工作流完成 batch_no=%s", self.batch.batch_no)
|
||||
|
||||
def _nodes(self):
|
||||
return WorkflowNodeRun.objects.filter(
|
||||
workflow_type=WORKFLOW_TYPE,
|
||||
workflow_batch_id=self.batch.pk,
|
||||
).order_by("id")
|
||||
|
||||
def _run_node(self, node: WorkflowNodeRun) -> None:
|
||||
node.status = WorkflowNodeRun.Status.RUNNING
|
||||
node.progress = 10
|
||||
node.started_at = timezone.now()
|
||||
node.message = f"{node.node_name}处理中"
|
||||
node.save(update_fields=["status", "progress", "started_at", "message"])
|
||||
record_event(
|
||||
self.batch,
|
||||
"node_progress",
|
||||
{"node_code": node.node_code, "status": node.status, "progress": node.progress, "message": node.message},
|
||||
)
|
||||
|
||||
if node.node_code == "pdf_convert":
|
||||
node.status = WorkflowNodeRun.Status.SKIPPED
|
||||
node.progress = 100
|
||||
node.finished_at = timezone.now()
|
||||
node.message = "PDF 转换为后续增强项,本次跳过"
|
||||
node.save(update_fields=["status", "progress", "finished_at", "message"])
|
||||
record_event(
|
||||
self.batch,
|
||||
"node_progress",
|
||||
{"node_code": node.node_code, "status": node.status, "progress": node.progress, "message": node.message},
|
||||
)
|
||||
return
|
||||
|
||||
node.status = WorkflowNodeRun.Status.SUCCESS
|
||||
node.progress = 100
|
||||
node.finished_at = timezone.now()
|
||||
node.message = f"{node.node_name}完成"
|
||||
node.save(update_fields=["status", "progress", "finished_at", "message"])
|
||||
record_event(
|
||||
self.batch,
|
||||
"node_progress",
|
||||
{"node_code": node.node_code, "status": node.status, "progress": node.progress, "message": node.message},
|
||||
)
|
||||
|
||||
|
||||
def start_application_form_fill_workflow(batch, *, async_run: bool = True) -> None:
|
||||
def start_application_form_fill_workflow(batch: ApplicationFormFillBatch, *, async_run: bool = True) -> None:
|
||||
executor = FormFillWorkflowExecutor(batch)
|
||||
if async_run:
|
||||
if not async_run:
|
||||
executor.run()
|
||||
return
|
||||
executor.run()
|
||||
Thread(target=executor.run, daemon=True).start()
|
||||
|
||||
Reference in New Issue
Block a user