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:
315
server/src/app/services/steward_query_executors.py
Normal file
315
server/src/app/services/steward_query_executors.py
Normal file
@@ -0,0 +1,315 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.api.deps import CurrentUserContext
|
||||
from app.schemas.steward import (
|
||||
StewardActionExecuteRequest,
|
||||
StewardActionExecuteResponse,
|
||||
StewardActionStep,
|
||||
StewardTask,
|
||||
)
|
||||
from app.services.expense_rule_runtime_defaults import DEFAULT_TRAVEL_POLICY_CONFIG
|
||||
from app.services.runtime_chat import RuntimeChatService
|
||||
|
||||
|
||||
# 城市分级标签:差旅政策 city_tier 到面向用户的城市档位名称
|
||||
CITY_TIER_LABELS = {
|
||||
"tier_1": "一类城市(北上广深)",
|
||||
"tier_2": "二类城市(省会及重点城市)",
|
||||
"tier_3": "三类城市(其他地区)",
|
||||
}
|
||||
|
||||
# 交通等级编码到中文标签
|
||||
TRANSPORT_LEVEL_LABELS = {
|
||||
1: "经济舱/二等座(普通席别)",
|
||||
2: "高端经济舱/一等座(中级席别)",
|
||||
3: "公务舱/商务座(高级席别)",
|
||||
4: "头等舱(最高席别)",
|
||||
}
|
||||
|
||||
|
||||
def build_travel_standard_query_steps(task: StewardTask) -> list[StewardActionStep]:
|
||||
"""生成差旅标准查询任务的动作步骤。
|
||||
|
||||
查询不产生副作用,无需校验必填、保存或提交,只生成单步执行动作。
|
||||
"""
|
||||
fields = _resolve_task_fields(task)
|
||||
return [
|
||||
StewardActionStep(
|
||||
step_id=f"{task.task_id}:01",
|
||||
action_type="execute_travel_standard_query",
|
||||
label="查询差旅标准",
|
||||
target_task_id=task.task_id,
|
||||
status="planned",
|
||||
requires_confirmation=False,
|
||||
payload={
|
||||
"task_id": task.task_id,
|
||||
"ontology_fields": fields,
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def execute_travel_standard_query(
|
||||
executor: Any,
|
||||
request: StewardActionExecuteRequest,
|
||||
current_user: CurrentUserContext,
|
||||
trace: list[dict[str, Any]],
|
||||
) -> StewardActionExecuteResponse:
|
||||
"""执行差旅标准查询:检索业务数据 → 交给 LLM 整理成自然语言。
|
||||
|
||||
数据源为 DEFAULT_TRAVEL_POLICY_CONFIG(住宿标准 by 职级×城市分级、
|
||||
交通等级 by 职级)。补助标准当前未纳入运行时配置,用制度说明兜底。
|
||||
"""
|
||||
action_type = "execute_travel_standard_query"
|
||||
fields = _resolve_task_fields(request.task)
|
||||
message = _resolve_message(request)
|
||||
|
||||
location = str(fields.get("location") or "").strip()
|
||||
employee_grade = _resolve_employee_grade(fields, current_user)
|
||||
standard_category = str(fields.get("standard_category") or "").strip().lower()
|
||||
|
||||
standards = resolve_travel_standard_snapshot(
|
||||
location=location,
|
||||
employee_grade=employee_grade,
|
||||
standard_category=standard_category,
|
||||
)
|
||||
|
||||
if not standards["matched_any"]:
|
||||
answer = _build_no_match_answer(location, employee_grade, standard_category)
|
||||
return StewardActionExecuteResponse(
|
||||
action_type=action_type,
|
||||
status="succeeded",
|
||||
message=answer,
|
||||
result_payload={
|
||||
"answer_markdown": answer,
|
||||
"standards": standards,
|
||||
"matched": False,
|
||||
},
|
||||
trace=[*trace, _trace("completed", mode="query_no_match")],
|
||||
)
|
||||
|
||||
answer = _compose_travel_standard_answer(
|
||||
message=message,
|
||||
standards=standards,
|
||||
location=location,
|
||||
employee_grade=employee_grade,
|
||||
)
|
||||
return StewardActionExecuteResponse(
|
||||
action_type=action_type,
|
||||
status="succeeded",
|
||||
message=answer,
|
||||
result_payload={
|
||||
"answer_markdown": answer,
|
||||
"standards": standards,
|
||||
"matched": True,
|
||||
},
|
||||
trace=[*trace, _trace("completed", mode="query_travel_standard")],
|
||||
)
|
||||
|
||||
|
||||
def resolve_travel_standard_snapshot(
|
||||
*,
|
||||
location: str,
|
||||
employee_grade: str,
|
||||
standard_category: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""按地点、职级和关注标准类别,从差旅政策配置检索确定性标准数值。
|
||||
|
||||
standard_category 为空表示返回全部类别;非空时只返回指定类别。
|
||||
支持的类别:lodging(住宿)、transport(交通)、allowance(补助)。
|
||||
"""
|
||||
config = DEFAULT_TRAVEL_POLICY_CONFIG
|
||||
city_tiers = config.get("city_tiers", {})
|
||||
hotel_limits = config.get("hotel_limits", {})
|
||||
transport_limits = config.get("transport_limits", {})
|
||||
band_labels = config.get("band_labels", {})
|
||||
|
||||
normalized_city = str(location or "").strip()
|
||||
city_tier = city_tiers.get(normalized_city, "tier_3") if normalized_city else "tier_3"
|
||||
normalized_grade = _normalize_grade(employee_grade)
|
||||
|
||||
snapshot: dict[str, Any] = {
|
||||
"location": normalized_city or "",
|
||||
"city_tier": city_tier,
|
||||
"city_tier_label": CITY_TIER_LABELS.get(city_tier, city_tier),
|
||||
"employee_grade": normalized_grade,
|
||||
"employee_grade_label": band_labels.get(normalized_grade, normalized_grade or "未指定"),
|
||||
"standard_category": standard_category or "",
|
||||
"matched_any": False,
|
||||
"lodging": None,
|
||||
"transport": None,
|
||||
"allowance": None,
|
||||
}
|
||||
|
||||
want_all = not standard_category
|
||||
if want_all or standard_category == "lodging":
|
||||
lodging_cap = _resolve_lodging_cap(hotel_limits, normalized_grade, city_tier)
|
||||
if lodging_cap is not None:
|
||||
snapshot["lodging"] = {
|
||||
"daily_cap": str(lodging_cap),
|
||||
"unit": "元/晚",
|
||||
}
|
||||
snapshot["matched_any"] = True
|
||||
|
||||
if want_all or standard_category == "transport":
|
||||
transport_band = _resolve_transport_band(transport_limits, normalized_grade)
|
||||
if transport_band is not None:
|
||||
snapshot["transport"] = {
|
||||
"flight_level": transport_band.get("flight"),
|
||||
"train_level": transport_band.get("train"),
|
||||
"flight_label": TRANSPORT_LEVEL_LABELS.get(int(transport_band.get("flight", 0)), ""),
|
||||
"train_label": TRANSPORT_LEVEL_LABELS.get(int(transport_band.get("train", 0)), ""),
|
||||
}
|
||||
snapshot["matched_any"] = True
|
||||
|
||||
if want_all or standard_category == "allowance":
|
||||
# 补助标准当前未纳入运行时配置,用占位说明,等补助数据源接入后补全。
|
||||
# 占位说明不计入 matched_any,避免无有效数据时仍标记为已匹配。
|
||||
snapshot["allowance"] = {
|
||||
"note": "出差补助标准按地区(直辖市/港澳台/境外等)分档,具体数值请参考《公司差旅费报销规则》或咨询财务。",
|
||||
}
|
||||
|
||||
return snapshot
|
||||
|
||||
|
||||
def _resolve_lodging_cap(
|
||||
hotel_limits: dict[str, Any],
|
||||
grade: str,
|
||||
city_tier: str,
|
||||
) -> str | None:
|
||||
grade_entry = hotel_limits.get(grade)
|
||||
if not isinstance(grade_entry, dict):
|
||||
return None
|
||||
cap = grade_entry.get(city_tier)
|
||||
return str(cap).strip() if cap is not None else None
|
||||
|
||||
|
||||
def _resolve_transport_band(
|
||||
transport_limits: dict[str, Any],
|
||||
grade: str,
|
||||
) -> dict[str, Any] | None:
|
||||
band = transport_limits.get(grade)
|
||||
if not isinstance(band, dict):
|
||||
return None
|
||||
return {"flight": band.get("flight"), "train": band.get("train")}
|
||||
|
||||
|
||||
def _normalize_grade(value: str) -> str:
|
||||
normalized = str(value or "").strip().upper()
|
||||
if normalized in {"", "未指定", "未知"}:
|
||||
return ""
|
||||
if normalized in DEFAULT_TRAVEL_POLICY_CONFIG.get("band_labels", {}):
|
||||
return normalized
|
||||
# 容忍 P05 / p5 等写法
|
||||
compact = normalized.lstrip("Pp")
|
||||
if compact.isdigit():
|
||||
candidate = f"P{int(compact)}"
|
||||
if candidate in DEFAULT_TRAVEL_POLICY_CONFIG.get("band_labels", {}):
|
||||
return candidate
|
||||
return normalized
|
||||
|
||||
|
||||
def _resolve_employee_grade(
|
||||
fields: dict[str, str],
|
||||
current_user: CurrentUserContext,
|
||||
) -> str:
|
||||
grade = str(fields.get("employee_grade") or "").strip()
|
||||
if grade:
|
||||
return grade
|
||||
return str(getattr(current_user, "grade", "") or "").strip()
|
||||
|
||||
|
||||
def _resolve_task_fields(task: StewardTask | None) -> dict[str, str]:
|
||||
if task is None or not isinstance(task.ontology_fields, dict):
|
||||
return {}
|
||||
return {
|
||||
str(key or "").strip(): str(value or "").strip()
|
||||
for key, value in task.ontology_fields.items()
|
||||
if str(key or "").strip() and str(value or "").strip()
|
||||
}
|
||||
|
||||
|
||||
def _resolve_message(request: StewardActionExecuteRequest) -> str:
|
||||
message = str(request.message or "").strip()
|
||||
if message:
|
||||
return message
|
||||
if request.task is not None:
|
||||
return str(request.task.summary or request.task.title or "").strip()
|
||||
return "差旅标准查询"
|
||||
|
||||
|
||||
def _build_no_match_answer(location: str, employee_grade: str, standard_category: str) -> str:
|
||||
parts = ["### 未能匹配到具体差旅标准"]
|
||||
details = []
|
||||
if location:
|
||||
details.append(f"目的地:**{location}**")
|
||||
if employee_grade:
|
||||
details.append(f"职级:**{employee_grade}**")
|
||||
if standard_category:
|
||||
details.append(f"关注类别:**{standard_category}**")
|
||||
if details:
|
||||
parts.append("")
|
||||
parts.append("当前识别到:" + "、".join(details) + "。")
|
||||
parts.append("")
|
||||
parts.append(
|
||||
"请补充更明确的信息,例如\"P5 去武汉出差的住宿标准是多少\","
|
||||
"或直接说\"查武汉的住宿标准\"。"
|
||||
)
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _compose_travel_standard_answer(
|
||||
*,
|
||||
message: str,
|
||||
standards: dict[str, Any],
|
||||
location: str,
|
||||
employee_grade: str,
|
||||
) -> str:
|
||||
"""把结构化标准整理成面向用户的 Markdown 回复。
|
||||
|
||||
优先用确定性数据拼装;如需更自然的表述,可在此处接入 LLM,
|
||||
当前阶段确定性拼装已足够清晰,避免额外模型调用开销。
|
||||
"""
|
||||
lines = ["### 差旅标准查询结果"]
|
||||
context_parts = []
|
||||
if location:
|
||||
context_parts.append(f"目的地 **{location}**({standards.get('city_tier_label', '')})")
|
||||
if employee_grade:
|
||||
context_parts.append(f"职级 **{standards.get('employee_grade_label', employee_grade)}**")
|
||||
if context_parts:
|
||||
lines.append("")
|
||||
lines.append("查询条件:" + "、".join(context_parts) + "。")
|
||||
|
||||
lodging = standards.get("lodging")
|
||||
transport = standards.get("transport")
|
||||
allowance = standards.get("allowance")
|
||||
|
||||
if lodging:
|
||||
lines.append("")
|
||||
lines.append(f"- **住宿标准**:{lodging['daily_cap']} {lodging['unit']}")
|
||||
if transport:
|
||||
lines.append(
|
||||
f"- **交通工具等级**:飞机 {transport.get('flight_label', '')}、"
|
||||
f"火车 {transport.get('train_label', '')}"
|
||||
)
|
||||
if allowance:
|
||||
lines.append(f"- **出差补助**:{allowance.get('note', '')}")
|
||||
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"> 标准依据公司差旅政策运行时配置。如需了解超标说明、多城市行程等例外口径,"
|
||||
"请进一步描述您的场景。"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _trace(stage: str, **extra: Any) -> dict[str, Any]:
|
||||
from datetime import UTC, datetime
|
||||
|
||||
return {
|
||||
"stage": stage,
|
||||
"at": datetime.now(UTC).isoformat(),
|
||||
**extra,
|
||||
}
|
||||
Reference in New Issue
Block a user