from __future__ import annotations from app.schemas.steward import StewardSlotDecisionRequest from app.services.runtime_chat import RuntimeChatCallTrace, RuntimeChatToolCall, RuntimeToolCallResult from app.services.steward_slot_decision_agent import ( STEWARD_SLOT_DECISION_FUNCTION_NAME, StewardSlotDecisionAgent, ) class FakeSlotRuntime: def __init__(self, arguments=None): self.arguments = arguments self.messages = None def complete_with_tool_call(self, messages, **kwargs): self.messages = messages if self.arguments is None: return RuntimeToolCallResult(tool_call=None, calls=[]) return RuntimeToolCallResult( tool_call=RuntimeChatToolCall( name=STEWARD_SLOT_DECISION_FUNCTION_NAME, arguments=self.arguments, ), calls=[ RuntimeChatCallTrace( slot="main", provider="OpenAI Compatible", model="fake", attempt=1, status="succeeded", ) ], ) def test_steward_slot_decision_uses_function_calling_result() -> None: runtime = FakeSlotRuntime( { "next_action": "ask_user", "required_fields": ["expense_type", "time_range", "location", "reason", "transport_mode"], "missing_fields": ["transport_mode"], "question": "请问你这次打算怎么出行?", "options": [ {"field_key": "transport_mode", "label": "飞机", "value": "飞机"}, {"field_key": "transport_mode", "label": "火车", "value": "火车"}, ], "rationale": "出行方式会影响交通费用测算。", } ) result = StewardSlotDecisionAgent(runtime).decide( StewardSlotDecisionRequest( task_type="expense_application", user_message="明天出差北京3天,支撑国网仿生产部署", ontology_fields={ "expense_type": "travel", "time_range": "2026-06-05 至 2026-06-07", "location": "北京", "reason": "支撑国网仿生产部署", }, missing_fields=["transport_mode"], ) ) assert result.decision_source == "llm_function_call" assert result.next_action == "ask_user" assert result.missing_fields == ["transport_mode"] assert [item.value for item in result.options] == ["飞机", "火车"] assert "出行方式会影响" in result.rationale def test_steward_slot_decision_falls_back_to_intent_missing_fields_only() -> None: runtime = FakeSlotRuntime(arguments=None) result = StewardSlotDecisionAgent(runtime).decide( StewardSlotDecisionRequest( task_type="expense_application", user_message="还需要补充:出行方式(例如高铁、飞机、自驾、出租车)", ontology_fields={ "expense_type": "travel", "location": "北京", "reason": "支撑国网仿生产部署", }, missing_fields=["transport_mode"], ) ) assert result.decision_source == "rule_fallback" assert result.next_action == "ask_user" assert result.missing_fields == ["transport_mode"] assert [item.value for item in result.options] == ["火车", "飞机", "轮船"] assert "高铁" not in result.required_fields def test_steward_slot_decision_does_not_ask_user_for_application_profile_or_attachments() -> None: runtime = FakeSlotRuntime( { "next_action": "ask_user", "required_fields": [ "expense_type", "time_range", "location", "reason", "amount", "attachments", "employee_no", ], "missing_fields": ["attachments", "employee_no"], "question": "请补充附件和员工编号。", "options": [], "rationale": "附件/凭证和员工编号为合规必需字段。", } ) result = StewardSlotDecisionAgent(runtime).decide( StewardSlotDecisionRequest( task_type="expense_application", user_message="明天出差北京3天,支撑国网仿生产部署", ontology_fields={ "expense_type": "travel", "time_range": "2026-06-05 至 2026-06-07", "location": "北京", "reason": "支撑国网仿生产部署", }, missing_fields=["attachments", "employee_no"], ) ) assert result.decision_source == "llm_function_call" assert result.next_action == "render_preview" assert result.missing_fields == [] assert "attachments" not in result.required_fields assert "employee_no" not in result.required_fields assert result.options == [] assert "合规必需字段" not in result.rationale