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:
@@ -9,7 +9,9 @@ from typing import Any
|
||||
from app.schemas.steward import (
|
||||
StewardAttachmentGroup,
|
||||
StewardAttachmentInput,
|
||||
StewardCandidateFlow,
|
||||
StewardConfirmationAction,
|
||||
StewardPendingFlowConfirmation,
|
||||
StewardPlanRequest,
|
||||
StewardPlanResponse,
|
||||
StewardTask,
|
||||
@@ -107,7 +109,7 @@ class StewardPlannerService:
|
||||
base_date = self._resolve_base_date(request.client_now_iso, request.context_json)
|
||||
model_call_traces: list[dict[str, Any]] = []
|
||||
fallback_reason = ""
|
||||
if self.intent_agent is not None:
|
||||
if self.intent_agent is not None and self._should_use_model_intent_recognition(message, base_date, request):
|
||||
try:
|
||||
intent_result = self.intent_agent.detect(
|
||||
request,
|
||||
@@ -122,6 +124,17 @@ class StewardPlannerService:
|
||||
base_date=base_date,
|
||||
)
|
||||
if llm_plan is not None:
|
||||
if self._looks_like_ambiguous_travel_flow(message, base_date, request):
|
||||
return self._build_pending_flow_fallback_plan(
|
||||
request,
|
||||
base_date=base_date,
|
||||
model_call_traces=model_call_traces,
|
||||
fallback_reason=(
|
||||
"主模型返回了直接任务,但当前话术没有明确申请或报销动作;"
|
||||
"服务端已改为候选流程确认,避免误入申请流程。"
|
||||
),
|
||||
planning_source="llm_function_call",
|
||||
)
|
||||
return llm_plan
|
||||
model_call_traces = getattr(self.intent_agent, "last_call_traces", []) or model_call_traces
|
||||
fallback_reason = "主模型未返回可用的 function calling 计划,已切换到规则兜底。"
|
||||
@@ -136,6 +149,16 @@ class StewardPlannerService:
|
||||
fallback_reason=fallback_reason,
|
||||
)
|
||||
|
||||
def _should_use_model_intent_recognition(
|
||||
self,
|
||||
message: str,
|
||||
base_date: date,
|
||||
request: StewardPlanRequest,
|
||||
) -> bool:
|
||||
if self._looks_like_ambiguous_travel_flow(message, base_date, request):
|
||||
return False
|
||||
return self._has_multiple_financial_demands(message)
|
||||
|
||||
def _build_rule_fallback_plan(
|
||||
self,
|
||||
request: StewardPlanRequest,
|
||||
@@ -145,6 +168,13 @@ class StewardPlannerService:
|
||||
fallback_reason: str = "",
|
||||
) -> StewardPlanResponse:
|
||||
message = self._clean_text(request.message)
|
||||
if self._looks_like_ambiguous_travel_flow(message, base_date, request):
|
||||
return self._build_pending_flow_fallback_plan(
|
||||
request,
|
||||
base_date=base_date,
|
||||
model_call_traces=model_call_traces,
|
||||
fallback_reason=fallback_reason,
|
||||
)
|
||||
task_drafts = self._extract_task_drafts(message)
|
||||
tasks = [self._build_task(draft, base_date, request) for draft in task_drafts]
|
||||
if not tasks:
|
||||
@@ -169,6 +199,7 @@ class StewardPlannerService:
|
||||
plan_id=plan_id,
|
||||
plan_status="needs_confirmation" if confirmation_groups else "ready_to_delegate",
|
||||
planning_source="rule_fallback",
|
||||
next_action="confirm_task" if confirmation_groups else "delegate_task",
|
||||
summary=self._build_summary(tasks, attachment_groups),
|
||||
thinking_events=thinking_events,
|
||||
tasks=tasks,
|
||||
@@ -177,6 +208,91 @@ class StewardPlannerService:
|
||||
model_call_traces=model_call_traces or [],
|
||||
)
|
||||
|
||||
def _build_pending_flow_fallback_plan(
|
||||
self,
|
||||
request: StewardPlanRequest,
|
||||
*,
|
||||
base_date: date,
|
||||
model_call_traces: list[dict[str, Any]] | None = None,
|
||||
fallback_reason: str = "",
|
||||
planning_source: str = "rule_fallback",
|
||||
) -> StewardPlanResponse:
|
||||
candidates = self._build_rule_candidate_flows(request, base_date)
|
||||
pending = StewardPendingFlowConfirmation(
|
||||
status="pending",
|
||||
source_message=request.message,
|
||||
reason="当前话术描述了出差事项,但没有明确说明要补办申请还是发起报销。",
|
||||
candidate_flows=candidates,
|
||||
)
|
||||
thinking_events = []
|
||||
if fallback_reason:
|
||||
thinking_events.append(
|
||||
StewardThinkingEvent(
|
||||
event_id="intent_agent_rule_fallback",
|
||||
stage="rule_fallback",
|
||||
title="意图识别智能体进入兜底模式",
|
||||
content=fallback_reason,
|
||||
)
|
||||
)
|
||||
thinking_events.append(
|
||||
StewardThinkingEvent(
|
||||
event_id="intent_pending_flow_confirmation",
|
||||
stage="flow_confirmation",
|
||||
title="需要确认流程方向",
|
||||
content="我识别到时间、地点和出差事由,但没有识别到明确的申请或报销动作,需要先请你选择流程方向。",
|
||||
)
|
||||
)
|
||||
return StewardPlanResponse(
|
||||
plan_id=f"steward_plan_{uuid.uuid4().hex[:12]}",
|
||||
plan_status="needs_flow_confirmation",
|
||||
planning_source=planning_source, # type: ignore[arg-type]
|
||||
next_action="confirm_flow",
|
||||
summary=(
|
||||
"我识别到这是一次出差事项,但还不能确定你要做的是"
|
||||
"**补办出差申请**还是**发起费用报销**。请先选择一个方向。"
|
||||
),
|
||||
thinking_events=thinking_events,
|
||||
pending_flow_confirmation=pending,
|
||||
candidate_flows=candidates,
|
||||
model_call_traces=model_call_traces or [],
|
||||
)
|
||||
|
||||
def _build_rule_candidate_flows(
|
||||
self,
|
||||
request: StewardPlanRequest,
|
||||
base_date: date,
|
||||
) -> list[StewardCandidateFlow]:
|
||||
application_fields = self._extract_ontology_fields(
|
||||
request.message,
|
||||
"expense_application",
|
||||
base_date,
|
||||
request,
|
||||
)
|
||||
reimbursement_fields = self._extract_ontology_fields(
|
||||
request.message,
|
||||
"reimbursement",
|
||||
base_date,
|
||||
request,
|
||||
)
|
||||
return [
|
||||
StewardCandidateFlow(
|
||||
flow_id="travel_application",
|
||||
label="补办出差申请",
|
||||
confidence=0.52,
|
||||
reason="用户描述了出差时间、地点和事由,但没有明确说要报销。",
|
||||
ontology_fields=application_fields,
|
||||
missing_fields=self._resolve_missing_fields("expense_application", application_fields),
|
||||
),
|
||||
StewardCandidateFlow(
|
||||
flow_id="travel_reimbursement",
|
||||
label="发起费用报销",
|
||||
confidence=0.48,
|
||||
reason="用户描述的也可能是已发生出差事项,需要进入报销材料整理。",
|
||||
ontology_fields=reimbursement_fields,
|
||||
missing_fields=self._resolve_missing_fields("reimbursement", reimbursement_fields),
|
||||
),
|
||||
]
|
||||
|
||||
def _extract_task_drafts(self, message: str) -> list[PlannedTaskDraft]:
|
||||
drafts: list[PlannedTaskDraft] = []
|
||||
first_reimbursement = self._find_first_reimbursement_index(message)
|
||||
@@ -202,6 +318,24 @@ class StewardPlannerService:
|
||||
|
||||
return drafts
|
||||
|
||||
def _has_multiple_financial_demands(self, message: str) -> bool:
|
||||
task_drafts = self._extract_task_drafts(message)
|
||||
if len(task_drafts) > 1:
|
||||
return True
|
||||
|
||||
compact = re.sub(r"\s+", "", message)
|
||||
if not compact:
|
||||
return False
|
||||
|
||||
application_signal = self._looks_like_application(compact) or self._looks_like_future_travel_application(compact)
|
||||
reimbursement_signal = self._find_first_reimbursement_index(compact) >= 0
|
||||
if application_signal and reimbursement_signal:
|
||||
return True
|
||||
|
||||
connector_signal = re.search(r"并且|同时|另外|还有|还要|以及|再", compact)
|
||||
repeated_reimbursement_signal = len(list(REIMBURSEMENT_PATTERN.finditer(compact))) > 1
|
||||
return bool(connector_signal and repeated_reimbursement_signal)
|
||||
|
||||
@staticmethod
|
||||
def _find_first_reimbursement_index(message: str) -> int:
|
||||
candidates = [message.find(item) for item in ("我要报销", "还需要报销", "需要报销", "报销")]
|
||||
@@ -238,6 +372,35 @@ class StewardPlannerService:
|
||||
)
|
||||
return bool((business_signal or route_signal) and (time_signal or planned_route_signal))
|
||||
|
||||
def _looks_like_ambiguous_travel_flow(
|
||||
self,
|
||||
text: str,
|
||||
base_date: date,
|
||||
request: StewardPlanRequest,
|
||||
) -> bool:
|
||||
compact = re.sub(r"\s+", "", text)
|
||||
if not compact or request.attachments:
|
||||
return False
|
||||
if re.search(r"申请|报销|草稿|提交|审批|保存|发起|创建", compact):
|
||||
return False
|
||||
if not re.search(r"出差|差旅|客户现场|项目|部署|实施|支撑|支持|协助|拜访|调研|培训|会议|驻场|上线|验收", compact):
|
||||
return False
|
||||
if not self._extract_time_range(compact, base_date):
|
||||
return False
|
||||
if not self._extract_location(compact):
|
||||
return False
|
||||
return not self._is_future_or_current_time_range(compact, base_date)
|
||||
|
||||
def _is_future_or_current_time_range(self, segment: str, base_date: date) -> bool:
|
||||
normalized = self._extract_time_range(segment, base_date)
|
||||
if not normalized:
|
||||
return False
|
||||
try:
|
||||
parsed = date.fromisoformat(normalized)
|
||||
except ValueError:
|
||||
return False
|
||||
return parsed >= base_date
|
||||
|
||||
def _build_task(
|
||||
self,
|
||||
draft: PlannedTaskDraft,
|
||||
|
||||
Reference in New Issue
Block a user