refactor(server): steward 决策链路改用 LangGraph 编排
- 新增 StewardGraphPlannerService,用 LangGraph 状态图编排意图识别→流程判断→模型/规则分支→兜底,替代原 planner 内线性调用 - 新增 StewardGraphRuntimeService 编排运行时决策与槽位决策;StewardActionContracts/Executor 统一动作合约与执行 - steward_intent_agent/application_fact_resolver/runtime_chat 适配图执行器,config 暴露图相关开关 - pyproject/uv.lock 新增 langgraph 依赖 - 新增 graph_planner/graph_runtime/action_executor 测试,更新 intent_agent/planner/fact_resolver/runtime_chat/reimbursement 测试
This commit is contained in:
224
server/src/app/services/steward_action_contracts.py
Normal file
224
server/src/app/services/steward_action_contracts.py
Normal file
@@ -0,0 +1,224 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.schemas.steward import (
|
||||
StewardActionStatus,
|
||||
StewardActionStep,
|
||||
StewardPlanResponse,
|
||||
StewardTask,
|
||||
)
|
||||
|
||||
|
||||
class StewardActionPlanBuilder:
|
||||
"""把小财管家任务转换为确定性的白名单动作步骤。"""
|
||||
|
||||
def attach_action_steps(self, plan: StewardPlanResponse) -> StewardPlanResponse:
|
||||
if not plan.tasks:
|
||||
return plan.model_copy(update={"action_steps": []})
|
||||
|
||||
tasks: list[StewardTask] = []
|
||||
plan_steps = [self._build_detect_intent_step(plan)]
|
||||
for task in plan.tasks:
|
||||
task_steps = self.build_task_action_steps(task)
|
||||
tasks.append(task.model_copy(update={"action_steps": task_steps}))
|
||||
plan_steps.extend(task_steps)
|
||||
return plan.model_copy(update={"tasks": tasks, "action_steps": plan_steps})
|
||||
|
||||
def build_task_action_steps(self, task: StewardTask) -> list[StewardActionStep]:
|
||||
if task.task_type == "expense_application":
|
||||
return self._build_application_steps(task)
|
||||
return self._build_reimbursement_steps(task)
|
||||
|
||||
def _build_application_steps(self, task: StewardTask) -> list[StewardActionStep]:
|
||||
steps = [
|
||||
self._build_task_step(
|
||||
task,
|
||||
1,
|
||||
"fill_application_fields",
|
||||
"填充申请字段",
|
||||
payload=self._field_payload(task),
|
||||
),
|
||||
self._build_task_step(
|
||||
task,
|
||||
2,
|
||||
"build_application_preview",
|
||||
"展示申请核对表",
|
||||
depends_on_index=1,
|
||||
payload={"task_id": task.task_id, "ontology_fields": task.ontology_fields},
|
||||
),
|
||||
self._build_task_step(
|
||||
task,
|
||||
3,
|
||||
"validate_required_fields",
|
||||
"校验申请必填字段",
|
||||
status=self._validation_status(task),
|
||||
depends_on_index=2,
|
||||
payload={"missing_fields": task.missing_fields},
|
||||
),
|
||||
]
|
||||
if task.requested_action == "save_draft":
|
||||
steps.append(
|
||||
self._build_task_step(
|
||||
task,
|
||||
4,
|
||||
"save_application_draft",
|
||||
"保存申请草稿",
|
||||
status=self._side_effect_status(task, requires_confirmation=False),
|
||||
depends_on_index=3,
|
||||
payload={
|
||||
"task_id": task.task_id,
|
||||
"requested_action": task.requested_action,
|
||||
"ontology_fields": task.ontology_fields,
|
||||
},
|
||||
)
|
||||
)
|
||||
elif task.requested_action == "submit":
|
||||
steps.append(
|
||||
self._build_task_step(
|
||||
task,
|
||||
4,
|
||||
"run_duplicate_precheck",
|
||||
"检查重复或冲突申请",
|
||||
status=self._side_effect_status(task, requires_confirmation=False),
|
||||
depends_on_index=3,
|
||||
payload={"task_id": task.task_id, "precheck_type": "travel_overlap"},
|
||||
)
|
||||
)
|
||||
steps.append(
|
||||
self._build_task_step(
|
||||
task,
|
||||
5,
|
||||
"submit_application",
|
||||
"提交申请审批",
|
||||
status=self._side_effect_status(task, requires_confirmation=True),
|
||||
requires_confirmation=not bool(task.missing_fields),
|
||||
depends_on_index=4,
|
||||
payload={
|
||||
"task_id": task.task_id,
|
||||
"requested_action": task.requested_action,
|
||||
"confirmation_required": True,
|
||||
},
|
||||
)
|
||||
)
|
||||
return steps
|
||||
|
||||
def _build_reimbursement_steps(self, task: StewardTask) -> list[StewardActionStep]:
|
||||
steps = [
|
||||
self._build_task_step(
|
||||
task,
|
||||
1,
|
||||
"fill_reimbursement_fields",
|
||||
"填充报销字段",
|
||||
payload=self._field_payload(task),
|
||||
),
|
||||
self._build_task_step(
|
||||
task,
|
||||
2,
|
||||
"build_reimbursement_preview",
|
||||
"展示报销核对表",
|
||||
depends_on_index=1,
|
||||
payload={"task_id": task.task_id, "ontology_fields": task.ontology_fields},
|
||||
),
|
||||
self._build_task_step(
|
||||
task,
|
||||
3,
|
||||
"validate_required_fields",
|
||||
"校验报销必填字段",
|
||||
status=self._validation_status(task),
|
||||
depends_on_index=2,
|
||||
payload={"missing_fields": task.missing_fields},
|
||||
),
|
||||
self._build_task_step(
|
||||
task,
|
||||
4,
|
||||
"create_reimbursement_draft",
|
||||
"创建报销草稿",
|
||||
status=self._side_effect_status(task, requires_confirmation=False),
|
||||
depends_on_index=3,
|
||||
payload={
|
||||
"task_id": task.task_id,
|
||||
"requested_action": task.requested_action,
|
||||
"ontology_fields": task.ontology_fields,
|
||||
},
|
||||
),
|
||||
]
|
||||
if task.ontology_fields.get("attachments"):
|
||||
steps.append(
|
||||
self._build_task_step(
|
||||
task,
|
||||
5,
|
||||
"associate_attachments",
|
||||
"关联报销附件",
|
||||
status=self._side_effect_status(task, requires_confirmation=False),
|
||||
depends_on_index=4,
|
||||
payload={"task_id": task.task_id, "attachments": task.ontology_fields["attachments"]},
|
||||
)
|
||||
)
|
||||
return steps
|
||||
|
||||
@staticmethod
|
||||
def _build_detect_intent_step(plan: StewardPlanResponse) -> StewardActionStep:
|
||||
return StewardActionStep(
|
||||
step_id="plan:00:detect_intent",
|
||||
action_type="detect_intent",
|
||||
label="识别业务意图",
|
||||
status="completed",
|
||||
payload={
|
||||
"planning_source": plan.planning_source,
|
||||
"plan_status": plan.plan_status,
|
||||
},
|
||||
)
|
||||
|
||||
def _build_task_step(
|
||||
self,
|
||||
task: StewardTask,
|
||||
index: int,
|
||||
action_type: str,
|
||||
label: str,
|
||||
*,
|
||||
status: StewardActionStatus = "planned",
|
||||
requires_confirmation: bool = False,
|
||||
depends_on_index: int | None = None,
|
||||
payload: dict[str, Any] | None = None,
|
||||
) -> StewardActionStep:
|
||||
step_id = self._step_id(task, index)
|
||||
depends_on = [self._step_id(task, depends_on_index)] if depends_on_index is not None else []
|
||||
return StewardActionStep(
|
||||
step_id=step_id,
|
||||
action_type=action_type, # type: ignore[arg-type]
|
||||
label=label,
|
||||
target_task_id=task.task_id,
|
||||
status=status,
|
||||
requires_confirmation=requires_confirmation,
|
||||
depends_on=depends_on,
|
||||
payload=payload or {},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _field_payload(task: StewardTask) -> dict[str, Any]:
|
||||
return {
|
||||
"task_id": task.task_id,
|
||||
"task_type": task.task_type,
|
||||
"requested_action": task.requested_action,
|
||||
"ontology_fields": task.ontology_fields,
|
||||
"missing_fields": task.missing_fields,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _validation_status(task: StewardTask) -> StewardActionStatus:
|
||||
return "blocked" if task.missing_fields else "planned"
|
||||
|
||||
@staticmethod
|
||||
def _side_effect_status(
|
||||
task: StewardTask,
|
||||
*,
|
||||
requires_confirmation: bool,
|
||||
) -> StewardActionStatus:
|
||||
if task.missing_fields:
|
||||
return "blocked"
|
||||
return "pending_confirmation" if requires_confirmation else "planned"
|
||||
|
||||
@staticmethod
|
||||
def _step_id(task: StewardTask, index: int) -> str:
|
||||
return f"{task.task_id}:{index:02d}"
|
||||
Reference in New Issue
Block a user