refactor(server): steward 意图改用声明式注册表编排
- 新增 steward_intent_registry,IntentDescriptor 统一描述意图的识别关键词、动作步骤构建、字段白名单与副作用集合,替代分散的 if/else - 新增 steward_intent_bootstrap 注册 expense_application 等意图;新增 steward_query_executors 提供差旅标准查询的无副作用执行与城市/席别标签化输出 - action_contracts/action_executor/graph_planner/intent_agent/model_plan_builder/planner_extraction/fallback 适配注册表,识别与执行分发自动从注册表取数 - 新增 intent_registry/query_executors 测试,更新 intent_agent 测试
This commit is contained in:
@@ -68,3 +68,33 @@ def test_steward_intent_agent_uses_ten_second_timeout_and_three_attempts() -> No
|
||||
assert runtime_chat.kwargs["timeout_seconds"] == 10
|
||||
assert runtime_chat.kwargs["max_attempts"] == 3
|
||||
assert runtime_chat.kwargs["use_failure_cooldown"] is False
|
||||
|
||||
|
||||
def test_steward_intent_tool_schema_includes_query_task_type_from_registry() -> None:
|
||||
"""function call schema 的 task_type enum 应从注册表动态生成,包含查询意图。"""
|
||||
from app.services import steward_intent_bootstrap # noqa: F401 触发意图注册
|
||||
|
||||
schema = StewardIntentAgent._build_intent_tool_schema(
|
||||
["expense_type", "time_range", "location", "reason", "transport_mode"]
|
||||
)
|
||||
task_schema = schema["function"]["parameters"]["properties"]["tasks"]["items"]
|
||||
task_type_enum = task_schema["properties"]["task_type"]["enum"]
|
||||
|
||||
assert "expense_application" in task_type_enum
|
||||
assert "reimbursement" in task_type_enum
|
||||
assert "query_travel_standard" in task_type_enum
|
||||
|
||||
|
||||
def test_steward_intent_system_prompt_mentions_query_intent_guidance() -> None:
|
||||
"""system prompt 应包含查询意图的识别指引,避免被误识别为申请。"""
|
||||
from app.services import steward_intent_bootstrap # noqa: F401 触发意图注册
|
||||
|
||||
messages = StewardIntentAgent._build_messages(
|
||||
StewardPlanRequest(message="武汉出差标准是多少"),
|
||||
base_date=__import__("datetime").date(2026, 6, 24),
|
||||
canonical_fields=["location", "employee_grade"],
|
||||
)
|
||||
system_prompt = messages[0]["content"]
|
||||
assert "query_travel_standard" in system_prompt
|
||||
assert "差旅" in system_prompt
|
||||
assert "住宿标准" in system_prompt
|
||||
|
||||
87
server/tests/test_steward_intent_registry.py
Normal file
87
server/tests/test_steward_intent_registry.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.services import steward_intent_bootstrap # noqa: F401 触发意图注册
|
||||
from app.services.steward_intent_registry import (
|
||||
all_flow_ids,
|
||||
all_intents,
|
||||
all_signal_keywords,
|
||||
all_task_types,
|
||||
field_allowlist_for,
|
||||
get_intent,
|
||||
resolve_intent_by_action,
|
||||
resolve_task_type_for_flow,
|
||||
)
|
||||
from app.services.steward_constants import BUSINESS_CANONICAL_FIELDS
|
||||
|
||||
|
||||
def test_registry_registers_application_reimbursement_and_query_intents():
|
||||
task_types = all_task_types()
|
||||
assert "expense_application" in task_types
|
||||
assert "reimbursement" in task_types
|
||||
assert "query_travel_standard" in task_types
|
||||
|
||||
application = get_intent("expense_application")
|
||||
assert application is not None
|
||||
assert application.assigned_agent == "application_assistant"
|
||||
assert application.flow_id == "travel_application"
|
||||
|
||||
reimbursement = get_intent("reimbursement")
|
||||
assert reimbursement is not None
|
||||
assert reimbursement.assigned_agent == "reimbursement_assistant"
|
||||
assert reimbursement.flow_id == "travel_reimbursement"
|
||||
|
||||
query = get_intent("query_travel_standard")
|
||||
assert query is not None
|
||||
assert query.assigned_agent == "policy_query_assistant"
|
||||
assert query.flow_id is None # 查询意图不进入候选流程确认
|
||||
|
||||
|
||||
def test_registry_aggregates_flow_ids_and_signal_keywords():
|
||||
flow_ids = set(all_flow_ids())
|
||||
assert flow_ids == {"travel_application", "travel_reimbursement"}
|
||||
|
||||
keywords = all_signal_keywords()
|
||||
assert "出差" in keywords # 来自 expense_application
|
||||
assert "报销" in keywords # 来自 reimbursement
|
||||
assert "差旅标准" in keywords # 来自 query_travel_standard
|
||||
|
||||
|
||||
def test_registry_resolves_intent_by_action_type():
|
||||
assert resolve_intent_by_action("save_application_draft").task_type == "expense_application"
|
||||
assert resolve_intent_by_action("submit_application").task_type == "expense_application"
|
||||
assert resolve_intent_by_action("create_reimbursement_draft").task_type == "reimbursement"
|
||||
assert resolve_intent_by_action("associate_attachments").task_type == "reimbursement"
|
||||
assert resolve_intent_by_action("execute_travel_standard_query").task_type == "query_travel_standard"
|
||||
assert resolve_intent_by_action("unknown_action") is None
|
||||
|
||||
|
||||
def test_registry_resolves_task_type_for_flow():
|
||||
assert resolve_task_type_for_flow("travel_application") == "expense_application"
|
||||
assert resolve_task_type_for_flow("travel_reimbursement") == "reimbursement"
|
||||
assert resolve_task_type_for_flow("unknown_flow") is None
|
||||
|
||||
|
||||
def test_field_allowlist_uses_per_intent_overrides():
|
||||
# 申请/报销沿用全局 BUSINESS_CANONICAL_FIELDS
|
||||
application_fields = field_allowlist_for("expense_application")
|
||||
assert "location" in application_fields
|
||||
assert "amount" in application_fields
|
||||
assert application_fields == frozenset(BUSINESS_CANONICAL_FIELDS)
|
||||
|
||||
# 查询意图使用专属槽位集合
|
||||
query_fields = field_allowlist_for("query_travel_standard")
|
||||
assert "location" in query_fields
|
||||
assert "employee_grade" in query_fields
|
||||
assert "standard_category" in query_fields
|
||||
assert "amount" not in query_fields # 查询不需要金额
|
||||
|
||||
# 未注册意图回退到 fallback
|
||||
fallback_fields = field_allowlist_for("unknown", fallback=frozenset({"foo"}))
|
||||
assert fallback_fields == frozenset({"foo"})
|
||||
|
||||
|
||||
def test_query_intent_prompt_fragment_includes_identification_guidance():
|
||||
query = get_intent("query_travel_standard")
|
||||
assert query is not None
|
||||
assert "差旅" in query.prompt_fragment
|
||||
assert "住宿标准" in query.prompt_fragment
|
||||
172
server/tests/test_steward_query_executors.py
Normal file
172
server/tests/test_steward_query_executors.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.schemas.steward import StewardActionExecuteRequest, StewardTask
|
||||
from app.services.steward_query_executors import (
|
||||
build_travel_standard_query_steps,
|
||||
execute_travel_standard_query,
|
||||
resolve_travel_standard_snapshot,
|
||||
)
|
||||
|
||||
|
||||
def _build_current_user(grade: str = "P5") -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
username="test_user",
|
||||
name="测试员工",
|
||||
grade=grade,
|
||||
department_name="技术部",
|
||||
position="工程师",
|
||||
role_codes=["employee"],
|
||||
is_admin=False,
|
||||
employee_no="E00001",
|
||||
manager_name="李总",
|
||||
)
|
||||
|
||||
|
||||
def _build_request(
|
||||
*,
|
||||
location: str = "武汉",
|
||||
employee_grade: str = "",
|
||||
standard_category: str = "",
|
||||
message: str = "我去武汉出差的住宿标准是多少",
|
||||
) -> StewardActionExecuteRequest:
|
||||
ontology_fields: dict[str, str] = {}
|
||||
if location:
|
||||
ontology_fields["location"] = location
|
||||
if employee_grade:
|
||||
ontology_fields["employee_grade"] = employee_grade
|
||||
if standard_category:
|
||||
ontology_fields["standard_category"] = standard_category
|
||||
task = StewardTask(
|
||||
task_id="task_query_001",
|
||||
task_type="query_travel_standard",
|
||||
assigned_agent="policy_query_assistant",
|
||||
title="差旅标准查询",
|
||||
summary=message,
|
||||
ontology_fields=ontology_fields,
|
||||
)
|
||||
return StewardActionExecuteRequest(
|
||||
action_type="execute_travel_standard_query",
|
||||
message=message,
|
||||
task=task,
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_travel_standard_snapshot_returns_lodging_and_transport_for_known_city_and_grade():
|
||||
snapshot = resolve_travel_standard_snapshot(
|
||||
location="武汉",
|
||||
employee_grade="P5",
|
||||
)
|
||||
assert snapshot["location"] == "武汉"
|
||||
assert snapshot["city_tier"] == "tier_2" # 武汉是二类城市
|
||||
assert snapshot["employee_grade"] == "P5"
|
||||
assert snapshot["lodging"] is not None
|
||||
assert snapshot["lodging"]["daily_cap"] == "480.00" # P5 + tier_2
|
||||
assert snapshot["transport"] is not None
|
||||
assert snapshot["transport"]["flight_level"] == 1
|
||||
assert snapshot["matched_any"] is True
|
||||
|
||||
|
||||
def test_resolve_travel_standard_snapshot_filters_by_standard_category():
|
||||
lodging_only = resolve_travel_standard_snapshot(
|
||||
location="北京",
|
||||
employee_grade="P7",
|
||||
standard_category="lodging",
|
||||
)
|
||||
assert lodging_only["lodging"] is not None
|
||||
assert lodging_only["lodging"]["daily_cap"] == "900.00" # P7 + tier_1
|
||||
assert lodging_only["transport"] is None
|
||||
|
||||
transport_only = resolve_travel_standard_snapshot(
|
||||
location="北京",
|
||||
employee_grade="P7",
|
||||
standard_category="transport",
|
||||
)
|
||||
assert transport_only["transport"] is not None
|
||||
assert transport_only["transport"]["flight_level"] == 3 # P7 飞机等级
|
||||
assert transport_only["lodging"] is None
|
||||
|
||||
|
||||
def test_resolve_travel_standard_snapshot_normalizes_grade_variants():
|
||||
# "p5" 小写、未标准化写法应被归一为 "P5"
|
||||
snapshot = resolve_travel_standard_snapshot(
|
||||
location="武汉",
|
||||
employee_grade="p5",
|
||||
)
|
||||
assert snapshot["employee_grade"] == "P5"
|
||||
assert snapshot["lodging"]["daily_cap"] == "480.00"
|
||||
|
||||
|
||||
def test_resolve_travel_standard_snapshot_handles_unknown_grade():
|
||||
snapshot = resolve_travel_standard_snapshot(
|
||||
location="武汉",
|
||||
employee_grade="",
|
||||
)
|
||||
# 无职级时无法匹配住宿标准(需要职级档位);补助为占位说明,不计入 matched_any
|
||||
assert snapshot["lodging"] is None
|
||||
assert snapshot["matched_any"] is False
|
||||
assert snapshot["allowance"] is not None # 仍返回占位说明
|
||||
|
||||
|
||||
def test_execute_travel_standard_query_returns_succeeded_with_answer_markdown():
|
||||
request = _build_request(location="武汉", employee_grade="P5")
|
||||
response = execute_travel_standard_query(
|
||||
executor=None,
|
||||
request=request,
|
||||
current_user=_build_current_user("P5"),
|
||||
trace=[],
|
||||
)
|
||||
assert response.status == "succeeded"
|
||||
assert response.action_type == "execute_travel_standard_query"
|
||||
assert "差旅标准查询结果" in response.message
|
||||
assert "480.00" in response.message # 住宿标准
|
||||
assert response.result_payload["matched"] is True
|
||||
assert response.result_payload["standards"]["lodging"]["daily_cap"] == "480.00"
|
||||
|
||||
|
||||
def test_execute_travel_standard_query_falls_back_to_current_user_grade_when_field_missing():
|
||||
request = _build_request(location="武汉", employee_grade="")
|
||||
response = execute_travel_standard_query(
|
||||
executor=None,
|
||||
request=request,
|
||||
current_user=_build_current_user("P5"),
|
||||
trace=[],
|
||||
)
|
||||
assert response.status == "succeeded"
|
||||
# 应回退到 current_user.grade = P5
|
||||
assert response.result_payload["standards"]["employee_grade"] == "P5"
|
||||
|
||||
|
||||
def test_execute_travel_standard_query_returns_no_match_when_grade_and_city_unknown():
|
||||
request = _build_request(
|
||||
location="未知城市",
|
||||
employee_grade="",
|
||||
message="查差旅标准",
|
||||
)
|
||||
# ontology_fields 也没有 grade,current_user 也没有
|
||||
response = execute_travel_standard_query(
|
||||
executor=None,
|
||||
request=request,
|
||||
current_user=_build_current_user(""),
|
||||
trace=[],
|
||||
)
|
||||
assert response.status == "succeeded"
|
||||
assert response.result_payload["matched"] is False
|
||||
assert "未能匹配" in response.message
|
||||
|
||||
|
||||
def test_build_travel_standard_query_steps_generates_single_executable_step():
|
||||
task = StewardTask(
|
||||
task_id="task_query_001",
|
||||
task_type="query_travel_standard",
|
||||
assigned_agent="policy_query_assistant",
|
||||
title="差旅标准查询",
|
||||
summary="查武汉住宿标准",
|
||||
ontology_fields={"location": "武汉", "employee_grade": "P5"},
|
||||
)
|
||||
steps = build_travel_standard_query_steps(task)
|
||||
assert len(steps) == 1
|
||||
assert steps[0].action_type == "execute_travel_standard_query"
|
||||
assert steps[0].requires_confirmation is False
|
||||
assert steps[0].payload["ontology_fields"]["location"] == "武汉"
|
||||
Reference in New Issue
Block a user