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:
234
server/tests/test_steward_graph_planner.py
Normal file
234
server/tests/test_steward_graph_planner.py
Normal file
@@ -0,0 +1,234 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.api.v1.endpoints import steward as steward_endpoint
|
||||
from app.core.config import get_settings
|
||||
from app.schemas.steward import StewardPlanRequest
|
||||
from app.services.steward_graph_planner import StewardGraphPlannerService
|
||||
from app.services.steward_intent_agent import StewardIntentAgentResult
|
||||
from app.services.steward_planner import StewardPlannerService
|
||||
|
||||
|
||||
class GraphTravelApplicationIntentAgent:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
def detect(self, request, *, base_date, canonical_fields):
|
||||
self.calls += 1
|
||||
return StewardIntentAgentResult(
|
||||
payload={
|
||||
"thinking_events": [
|
||||
{
|
||||
"stage": "task_split",
|
||||
"title": "识别出差申请草稿",
|
||||
"content": "模型识别到用户要创建上海出差申请,并保存草稿。",
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"task_type": "expense_application",
|
||||
"title": "上海出差申请",
|
||||
"summary": (
|
||||
"2026-02-20 至 2026-02-23 前往上海,"
|
||||
"国网仿生产服务器部署,火车出行。"
|
||||
),
|
||||
"requested_action": "save_draft",
|
||||
"confidence": 0.95,
|
||||
"ontology_fields": {
|
||||
"time_range": "2026-02-20 至 2026-02-23",
|
||||
"location": "上海",
|
||||
"expense_type": "差旅",
|
||||
"reason": "国网仿生产服务器部署",
|
||||
"transport_type": "火车",
|
||||
},
|
||||
"missing_fields": [],
|
||||
}
|
||||
],
|
||||
"attachment_groups": [],
|
||||
},
|
||||
model_call_traces=[
|
||||
{
|
||||
"slot": "main",
|
||||
"provider": "MiniMax",
|
||||
"model": "abab-test",
|
||||
"attempt": 1,
|
||||
"status": "succeeded",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class GraphSubmitTravelApplicationIntentAgent:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
def detect(self, request, *, base_date, canonical_fields):
|
||||
self.calls += 1
|
||||
return StewardIntentAgentResult(
|
||||
payload={
|
||||
"thinking_events": [
|
||||
{
|
||||
"stage": "task_split",
|
||||
"title": "识别出差申请提交",
|
||||
"content": "模型识别到用户要创建上海出差申请,并直接提交。",
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"task_type": "expense_application",
|
||||
"title": "上海出差申请",
|
||||
"summary": (
|
||||
"2026-02-20 至 2026-02-23 前往上海,"
|
||||
"辅助国网仿生产服务器部署,火车出行。"
|
||||
),
|
||||
"requested_action": "submit",
|
||||
"confidence": 0.96,
|
||||
"ontology_fields": {
|
||||
"time_range": "2026-02-20 至 2026-02-23",
|
||||
"location": "上海",
|
||||
"expense_type": "差旅",
|
||||
"reason": "辅助国网仿生产服务器部署",
|
||||
"transport_mode": "火车",
|
||||
},
|
||||
"missing_fields": [],
|
||||
}
|
||||
],
|
||||
"attachment_groups": [],
|
||||
},
|
||||
model_call_traces=[
|
||||
{
|
||||
"slot": "main",
|
||||
"provider": "MiniMax",
|
||||
"model": "abab-test",
|
||||
"attempt": 1,
|
||||
"status": "succeeded",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class GraphEmptyIntentAgent:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
def detect(self, request, *, base_date, canonical_fields):
|
||||
self.calls += 1
|
||||
return None
|
||||
|
||||
|
||||
def test_langgraph_planner_preserves_llm_save_draft_plan() -> None:
|
||||
intent_agent = GraphTravelApplicationIntentAgent()
|
||||
service = StewardGraphPlannerService(intent_agent=intent_agent)
|
||||
|
||||
result = service.build_plan(
|
||||
StewardPlanRequest(
|
||||
message="2026-02-20 至 2026-02-23,上海出差,国网仿生产服务器部署,火车,保存草稿",
|
||||
client_now_iso="2026-02-10T09:00:00+08:00",
|
||||
)
|
||||
)
|
||||
|
||||
assert intent_agent.calls == 1
|
||||
assert result.planning_source == "llm_function_call"
|
||||
assert result.tasks[0].requested_action == "save_draft"
|
||||
assert result.tasks[0].ontology_fields["time_range"] == "2026-02-20 至 2026-02-23"
|
||||
assert result.tasks[0].ontology_fields["transport_mode"] == "train"
|
||||
assert result.model_call_traces[0]["provider"] == "MiniMax"
|
||||
|
||||
|
||||
def test_langgraph_planner_builds_submit_action_steps_for_application() -> None:
|
||||
intent_agent = GraphSubmitTravelApplicationIntentAgent()
|
||||
service = StewardGraphPlannerService(intent_agent=intent_agent)
|
||||
|
||||
result = service.build_plan(
|
||||
StewardPlanRequest(
|
||||
message="2026-02-20 至 2026-02-23,去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交",
|
||||
client_now_iso="2026-02-10T09:00:00+08:00",
|
||||
)
|
||||
)
|
||||
|
||||
assert intent_agent.calls == 1
|
||||
assert result.planning_source == "llm_function_call"
|
||||
assert result.action_steps[0].action_type == "detect_intent"
|
||||
assert [step.action_type for step in result.tasks[0].action_steps] == [
|
||||
"fill_application_fields",
|
||||
"build_application_preview",
|
||||
"validate_required_fields",
|
||||
"run_duplicate_precheck",
|
||||
"submit_application",
|
||||
]
|
||||
assert result.tasks[0].action_steps[0].payload["ontology_fields"]["location"] == "上海"
|
||||
assert result.tasks[0].action_steps[-1].requires_confirmation is True
|
||||
assert result.tasks[0].action_steps[-1].status == "pending_confirmation"
|
||||
|
||||
|
||||
def test_langgraph_planner_falls_back_when_model_returns_no_tool_call() -> None:
|
||||
intent_agent = GraphEmptyIntentAgent()
|
||||
service = StewardGraphPlannerService(intent_agent=intent_agent)
|
||||
|
||||
result = service.build_plan(
|
||||
StewardPlanRequest(
|
||||
message="2026-02-20 至 2026-02-23,上海出差,国网仿生产服务器部署,火车,保存草稿",
|
||||
client_now_iso="2026-02-10T09:00:00+08:00",
|
||||
)
|
||||
)
|
||||
|
||||
assert intent_agent.calls == 1
|
||||
assert result.planning_source == "rule_fallback"
|
||||
assert result.tasks[0].requested_action == "save_draft"
|
||||
assert result.tasks[0].ontology_fields["time_range"] == "2026-02-20 至 2026-02-23"
|
||||
assert result.tasks[0].ontology_fields["transport_mode"] == "train"
|
||||
assert result.model_call_traces == []
|
||||
|
||||
|
||||
def test_langgraph_planner_rule_fallback_builds_save_draft_action_steps() -> None:
|
||||
intent_agent = GraphEmptyIntentAgent()
|
||||
service = StewardGraphPlannerService(intent_agent=intent_agent)
|
||||
|
||||
result = service.build_plan(
|
||||
StewardPlanRequest(
|
||||
message="2026-02-20 至 2026-02-23,上海出差,国网仿生产服务器部署,火车,保存草稿",
|
||||
client_now_iso="2026-02-10T09:00:00+08:00",
|
||||
)
|
||||
)
|
||||
|
||||
assert result.planning_source == "rule_fallback"
|
||||
assert result.tasks[0].requested_action == "save_draft"
|
||||
assert [step.action_type for step in result.tasks[0].action_steps] == [
|
||||
"fill_application_fields",
|
||||
"build_application_preview",
|
||||
"validate_required_fields",
|
||||
"save_application_draft",
|
||||
]
|
||||
assert result.tasks[0].action_steps[-1].status == "planned"
|
||||
|
||||
|
||||
def test_build_steward_planner_uses_langgraph_runtime_when_enabled(monkeypatch) -> None:
|
||||
monkeypatch.setenv("STEWARD_AGENT_RUNTIME", "langgraph")
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
planner = steward_endpoint._build_steward_planner(db=object())
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
|
||||
assert isinstance(planner, StewardGraphPlannerService)
|
||||
|
||||
|
||||
def test_build_steward_planner_defaults_to_langgraph_runtime(monkeypatch) -> None:
|
||||
monkeypatch.delenv("STEWARD_AGENT_RUNTIME", raising=False)
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
planner = steward_endpoint._build_steward_planner(db=object())
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
|
||||
assert isinstance(planner, StewardGraphPlannerService)
|
||||
|
||||
|
||||
def test_build_steward_planner_can_fall_back_to_legacy_runtime(monkeypatch) -> None:
|
||||
monkeypatch.setenv("STEWARD_AGENT_RUNTIME", "legacy")
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
planner = steward_endpoint._build_steward_planner(db=object())
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
|
||||
assert isinstance(planner, StewardPlannerService)
|
||||
Reference in New Issue
Block a user