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)