feat(steward): 拦截业务无关输入返回 off_topic 计划

- schemas/steward.py:StewardPlanResponse 新增 suggested_prompts 字段
- steward_planner.py:新增 STEWARD_BUSINESS_SIGNAL_KEYWORDS 与
  _is_business_irrelevant_input 守卫,在 build_plan 入口前置;
  新增 _build_off_topic_plan 构造 plan_status=off_topic 的引导计划
- steward_intent_agent.py:system prompt 追加业务无关约束
- test_steward_planner.py:覆盖 123/你好/纯标点走 off_topic,
  并验证正常业务输入不受守卫影响
This commit is contained in:
caoxiaozhu
2026-06-18 14:15:20 +08:00
parent b8915a29c0
commit cce19e4c40
4 changed files with 143 additions and 0 deletions

View File

@@ -547,3 +547,69 @@ def test_steward_plan_endpoint_persists_application_and_reimbursement_state() ->
assert state["flows"]["travel_reimbursement"]["fields"]["time_range"] == "2026-06-03"
assert state["flows"]["travel_reimbursement"]["fields"]["expense_type"] == "transport"
assert all("invented_field" not in flow["fields"] for flow in state["flows"].values())
def test_steward_planner_returns_off_topic_for_business_irrelevant_input() -> None:
payload = StewardPlanRequest(
message="123",
client_now_iso="2026-06-04T09:30:00+08:00",
)
result = StewardPlannerService().build_plan(payload)
assert result.plan_status == "off_topic"
assert result.next_action == "none"
assert result.tasks == []
assert result.attachment_groups == []
assert result.confirmation_groups == []
assert result.candidate_flows == []
assert result.planning_source == "rule_fallback"
assert len(result.suggested_prompts) == 3
assert result.thinking_events[0].stage == "off_topic"
def test_steward_planner_returns_off_topic_for_pure_greeting() -> None:
payload = StewardPlanRequest(
message="你好",
client_now_iso="2026-06-04T09:30:00+08:00",
)
result = StewardPlannerService().build_plan(payload)
assert result.plan_status == "off_topic"
assert result.next_action == "none"
assert result.tasks == []
assert result.candidate_flows == []
assert result.planning_source == "rule_fallback"
assert len(result.suggested_prompts) == 3
assert result.thinking_events[0].stage == "off_topic"
def test_steward_planner_returns_off_topic_for_pure_punctuation() -> None:
payload = StewardPlanRequest(
message="??? !!!",
client_now_iso="2026-06-04T09:30:00+08:00",
)
result = StewardPlannerService().build_plan(payload)
assert result.plan_status == "off_topic"
assert result.next_action == "none"
assert result.tasks == []
assert result.candidate_flows == []
assert result.planning_source == "rule_fallback"
assert len(result.suggested_prompts) == 3
assert result.thinking_events[0].stage == "off_topic"
def test_steward_planner_preserves_normal_business_flow_after_guard() -> None:
payload = StewardPlanRequest(
message="我要报销昨天的交通费",
client_now_iso="2026-06-04T09:30:00+08:00",
)
result = StewardPlannerService().build_plan(payload)
assert result.plan_status != "off_topic"
assert len(result.tasks) >= 1
assert [task.task_type for task in result.tasks] == ["reimbursement"]