235 lines
8.8 KiB
Python
235 lines
8.8 KiB
Python
|
|
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)
|