Refine travel reimbursement steward flow

Align planner, runtime rules, and policy assets so travel guidance
matches the updated reimbursement workflow.
This commit is contained in:
caoxiaozhu
2026-06-15 22:55:18 +08:00
parent 792741709a
commit 9f7b8b46a3
85 changed files with 9496 additions and 2555 deletions

View File

@@ -8,6 +8,8 @@ from typing import Any
from app.schemas.steward import (
StewardAttachmentGroup,
StewardAttachmentInput,
StewardCandidateFlow,
StewardPendingFlowConfirmation,
StewardPlanRequest,
StewardPlanResponse,
StewardTask,
@@ -31,7 +33,18 @@ class StewardModelPlanBuilder:
request: StewardPlanRequest,
base_date: date,
) -> StewardPlanResponse | None:
pending_flow_confirmation = self._build_pending_flow_confirmation(
intent_result.payload,
request=request,
base_date=base_date,
)
tasks = self._build_tasks_from_model_payload(intent_result.payload, request, base_date)
if not tasks and pending_flow_confirmation.status == "pending":
return self._build_pending_flow_plan(
pending_flow_confirmation,
intent_result,
request=request,
)
if not tasks:
return None
@@ -54,11 +67,33 @@ class StewardModelPlanBuilder:
plan_id=f"steward_plan_{uuid.uuid4().hex[:12]}",
plan_status="needs_confirmation" if confirmation_groups else "ready_to_delegate",
planning_source="llm_function_call",
next_action="confirm_task" if confirmation_groups else "delegate_task",
summary=self.planner._build_summary(tasks, attachment_groups),
thinking_events=thinking_events,
tasks=tasks,
attachment_groups=attachment_groups,
confirmation_groups=confirmation_groups,
pending_flow_confirmation=pending_flow_confirmation,
candidate_flows=pending_flow_confirmation.candidate_flows,
model_call_traces=intent_result.model_call_traces,
)
def _build_pending_flow_plan(
self,
pending_flow_confirmation: StewardPendingFlowConfirmation,
intent_result: StewardIntentAgentResult,
*,
request: StewardPlanRequest,
) -> StewardPlanResponse:
return StewardPlanResponse(
plan_id=f"steward_plan_{uuid.uuid4().hex[:12]}",
plan_status="needs_flow_confirmation",
planning_source="llm_function_call",
next_action="confirm_flow",
summary=self._build_pending_flow_summary(pending_flow_confirmation),
thinking_events=self._build_pending_flow_thinking_events(intent_result.payload, request),
pending_flow_confirmation=pending_flow_confirmation,
candidate_flows=pending_flow_confirmation.candidate_flows,
model_call_traces=intent_result.model_call_traces,
)
@@ -144,6 +179,134 @@ class StewardModelPlanBuilder:
return tasks
def _build_pending_flow_confirmation(
self,
payload: dict[str, Any],
*,
request: StewardPlanRequest,
base_date: date,
) -> StewardPendingFlowConfirmation:
raw_pending = payload.get("pending_flow_confirmation")
raw_candidates = payload.get("candidate_flows")
if isinstance(raw_pending, dict):
raw_candidates = raw_pending.get("candidate_flows", raw_candidates)
status = self.planner._clean_text(raw_pending.get("status")) or "pending"
source_message = self.planner._clean_text(raw_pending.get("source_message")) or request.message
reason = self.planner._clean_text(raw_pending.get("reason"))
else:
status = "pending" if isinstance(raw_candidates, list) and raw_candidates else "none"
source_message = request.message
reason = ""
candidates = self._build_candidate_flows(raw_candidates, request=request, base_date=base_date)
if status != "pending" or not candidates:
return StewardPendingFlowConfirmation()
return StewardPendingFlowConfirmation(
status="pending",
source_message=source_message,
reason=reason or "当前话术同时可能进入申请或报销流程,需要先请用户确认。",
candidate_flows=candidates,
)
def _build_candidate_flows(
self,
raw_candidates: Any,
*,
request: StewardPlanRequest,
base_date: date,
) -> list[StewardCandidateFlow]:
if not isinstance(raw_candidates, list):
return []
candidates: list[StewardCandidateFlow] = []
for raw_candidate in raw_candidates:
if not isinstance(raw_candidate, dict):
continue
flow_id = self.planner._clean_text(raw_candidate.get("flow_id"))
if flow_id not in {"travel_application", "travel_reimbursement"}:
continue
task_type = "expense_application" if flow_id == "travel_application" else "reimbursement"
fields = self._sanitize_model_ontology_fields(
raw_candidate.get("ontology_fields"),
request=request,
base_date=base_date,
)
if not fields:
fields = self.planner._extract_ontology_fields(
request.message,
task_type,
base_date,
request,
)
missing_fields = self._sanitize_model_missing_fields(
raw_candidate.get("missing_fields"),
task_type=task_type,
fields=fields,
)
label = self.planner._clean_text(raw_candidate.get("label")) or (
"补办出差申请" if flow_id == "travel_application" else "发起费用报销"
)
candidates.append(
StewardCandidateFlow(
flow_id=flow_id, # type: ignore[arg-type]
label=label,
confidence=self._clamp_confidence(raw_candidate.get("confidence"), default=0.5),
reason=self.planner._clean_text(raw_candidate.get("reason")),
ontology_fields=fields,
missing_fields=missing_fields,
)
)
return candidates[:2]
def _build_pending_flow_thinking_events(
self,
payload: dict[str, Any],
request: StewardPlanRequest,
) -> list[StewardThinkingEvent]:
events = [
StewardThinkingEvent(
event_id="intent_agent_function_call",
stage="llm_function_call",
title="识别财务事项",
content="我识别到这句话包含出差事项,但还需要确认你要进入申请流程还是报销流程。",
)
]
raw_events = payload.get("thinking_events")
if isinstance(raw_events, list):
for raw_event in raw_events[:4]:
if not isinstance(raw_event, dict):
continue
title = self.planner._clean_text(raw_event.get("title"))
content = self.planner._clean_text(raw_event.get("content"))
if not title or not content:
continue
events.append(
StewardThinkingEvent(
event_id=f"intent_agent_model_{len(events):03d}",
stage=self.planner._clean_text(raw_event.get("stage")) or "flow_confirmation",
title=title,
content=content,
)
)
if len(events) == 1:
events.append(
StewardThinkingEvent(
event_id="intent_agent_pending_flow",
stage="flow_confirmation",
title="等待确认流程方向",
content=f"当前输入“{request.message}”缺少明确动作词,需要先由你选择补办出差申请或发起费用报销。",
)
)
return events
@staticmethod
def _build_pending_flow_summary(pending_flow_confirmation: StewardPendingFlowConfirmation) -> str:
candidate_labels = [item.label for item in pending_flow_confirmation.candidate_flows if item.label]
if len(candidate_labels) >= 2:
return (
f"我识别到这是一次财务事项,但还不能确定你要做的是"
f"**{candidate_labels[0]}**还是**{candidate_labels[1]}**。请先选择一个方向。"
)
return "我识别到这是一次财务事项,但还需要先确认具体流程方向。"
def _sanitize_model_ontology_fields(
self,
raw_fields: Any,