198 lines
9.3 KiB
Python
198 lines
9.3 KiB
Python
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
from app.schemas.steward import (
|
|||
|
|
StewardRuntimeDecisionRequest,
|
|||
|
|
StewardRuntimeDecisionResponse,
|
|||
|
|
)
|
|||
|
|
from app.services.runtime_chat import RuntimeChatService
|
|||
|
|
|
|||
|
|
|
|||
|
|
STEWARD_RUNTIME_DECISION_FUNCTION_NAME = "submit_steward_runtime_decision"
|
|||
|
|
|
|||
|
|
RUNTIME_NEXT_ACTIONS = {
|
|||
|
|
"plan_new_tasks",
|
|||
|
|
"submit_current_application",
|
|||
|
|
"continue_next_task",
|
|||
|
|
"fill_current_slot",
|
|||
|
|
"ask_user",
|
|||
|
|
"cancel_current_action",
|
|||
|
|
"no_op",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
class StewardRuntimeDecisionAgent:
|
|||
|
|
"""用小财管家运行时上下文判断用户当前输入应落到哪个等待动作。"""
|
|||
|
|
|
|||
|
|
def __init__(self, runtime_chat_service: RuntimeChatService) -> None:
|
|||
|
|
self.runtime_chat_service = runtime_chat_service
|
|||
|
|
|
|||
|
|
def decide(self, request: StewardRuntimeDecisionRequest) -> StewardRuntimeDecisionResponse:
|
|||
|
|
normalized_request = self._normalize_request(request)
|
|||
|
|
result = self.runtime_chat_service.complete_with_tool_call(
|
|||
|
|
self._build_messages(normalized_request),
|
|||
|
|
tools=[self._build_tool_schema()],
|
|||
|
|
tool_choice={
|
|||
|
|
"type": "function",
|
|||
|
|
"function": {"name": STEWARD_RUNTIME_DECISION_FUNCTION_NAME},
|
|||
|
|
},
|
|||
|
|
max_tokens=1000,
|
|||
|
|
temperature=0.05,
|
|||
|
|
timeout_seconds=30,
|
|||
|
|
max_attempts=1,
|
|||
|
|
)
|
|||
|
|
traces = result.calls_as_dicts()
|
|||
|
|
if result.tool_call is not None and result.tool_call.name == STEWARD_RUNTIME_DECISION_FUNCTION_NAME:
|
|||
|
|
response = self._build_response_from_model_payload(result.tool_call.arguments, normalized_request, traces)
|
|||
|
|
if response is not None:
|
|||
|
|
return response
|
|||
|
|
return self._build_rule_fallback(normalized_request, traces)
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _normalize_request(request: StewardRuntimeDecisionRequest) -> StewardRuntimeDecisionRequest:
|
|||
|
|
return StewardRuntimeDecisionRequest(
|
|||
|
|
user_message=str(request.user_message or "").strip(),
|
|||
|
|
session_type=str(request.session_type or "steward").strip() or "steward",
|
|||
|
|
runtime_state=request.runtime_state if isinstance(request.runtime_state, dict) else {},
|
|||
|
|
context_json=request.context_json if isinstance(request.context_json, dict) else {},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _build_messages(request: StewardRuntimeDecisionRequest) -> list[dict[str, Any]]:
|
|||
|
|
payload = {
|
|||
|
|
"user_message": request.user_message,
|
|||
|
|
"session_type": request.session_type,
|
|||
|
|
"runtime_state": request.runtime_state,
|
|||
|
|
"context_json": request.context_json,
|
|||
|
|
}
|
|||
|
|
return [
|
|||
|
|
{
|
|||
|
|
"role": "system",
|
|||
|
|
"content": (
|
|||
|
|
"你是 X-Financial 小财管家的运行时决策智能体。"
|
|||
|
|
"你必须基于 runtime_state 判断用户当前输入对应哪个等待动作,不能把每次输入都当成全新任务。"
|
|||
|
|
"runtime_state 会包含 current_task、remaining_tasks、completed_tasks、pending_application、"
|
|||
|
|
"pending_steward_action、waiting_for、recent_structured_result 等上下文。"
|
|||
|
|
"如果用户是在确认当前申请核对表无误,应返回 submit_current_application;"
|
|||
|
|
"如果用户是在确认继续下一项,应返回 continue_next_task;"
|
|||
|
|
"如果用户补充了当前等待字段,应返回 fill_current_slot;"
|
|||
|
|
"如果当前结构化结果仍缺字段,应返回 ask_user;"
|
|||
|
|
"只有当前没有可匹配上下文,且用户输入明显是新财务事项时,才返回 plan_new_tasks。"
|
|||
|
|
"提交、入库、绑定、审批等高风险动作只返回结构化意图,实际执行由系统安全校验完成。"
|
|||
|
|
"rationale 和 response_text 必须面向用户,不暴露内部推理链。"
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _build_tool_schema() -> dict[str, Any]:
|
|||
|
|
return {
|
|||
|
|
"type": "function",
|
|||
|
|
"function": {
|
|||
|
|
"name": STEWARD_RUNTIME_DECISION_FUNCTION_NAME,
|
|||
|
|
"description": "提交小财管家基于运行时上下文的下一步动作决策。",
|
|||
|
|
"parameters": {
|
|||
|
|
"type": "object",
|
|||
|
|
"properties": {
|
|||
|
|
"next_action": {
|
|||
|
|
"type": "string",
|
|||
|
|
"enum": sorted(RUNTIME_NEXT_ACTIONS),
|
|||
|
|
},
|
|||
|
|
"target_task_id": {"type": "string"},
|
|||
|
|
"target_message_id": {"type": "string"},
|
|||
|
|
"field_key": {"type": "string"},
|
|||
|
|
"field_value": {"type": "string"},
|
|||
|
|
"confirmation_required": {"type": "boolean"},
|
|||
|
|
"question": {"type": "string"},
|
|||
|
|
"response_text": {"type": "string"},
|
|||
|
|
"rationale": {"type": "string"},
|
|||
|
|
},
|
|||
|
|
"required": [
|
|||
|
|
"next_action",
|
|||
|
|
"target_task_id",
|
|||
|
|
"target_message_id",
|
|||
|
|
"field_key",
|
|||
|
|
"field_value",
|
|||
|
|
"confirmation_required",
|
|||
|
|
"question",
|
|||
|
|
"response_text",
|
|||
|
|
"rationale",
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def _build_response_from_model_payload(
|
|||
|
|
self,
|
|||
|
|
payload: dict[str, Any],
|
|||
|
|
request: StewardRuntimeDecisionRequest,
|
|||
|
|
traces: list[dict[str, Any]],
|
|||
|
|
) -> StewardRuntimeDecisionResponse | None:
|
|||
|
|
next_action = str(payload.get("next_action") or "").strip()
|
|||
|
|
if next_action not in RUNTIME_NEXT_ACTIONS:
|
|||
|
|
return None
|
|||
|
|
return StewardRuntimeDecisionResponse(
|
|||
|
|
decision_source="llm_function_call",
|
|||
|
|
next_action=next_action, # type: ignore[arg-type]
|
|||
|
|
target_task_id=self._clean_text(payload.get("target_task_id")),
|
|||
|
|
target_message_id=self._clean_text(payload.get("target_message_id")),
|
|||
|
|
field_key=self._clean_text(payload.get("field_key")),
|
|||
|
|
field_value=self._clean_text(payload.get("field_value")),
|
|||
|
|
confirmation_required=bool(payload.get("confirmation_required")),
|
|||
|
|
question=self._clean_text(payload.get("question")),
|
|||
|
|
response_text=self._clean_text(payload.get("response_text")),
|
|||
|
|
rationale=self._clean_text(payload.get("rationale")),
|
|||
|
|
model_call_traces=traces,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _build_rule_fallback(
|
|||
|
|
self,
|
|||
|
|
request: StewardRuntimeDecisionRequest,
|
|||
|
|
traces: list[dict[str, Any]],
|
|||
|
|
) -> StewardRuntimeDecisionResponse:
|
|||
|
|
state = request.runtime_state
|
|||
|
|
pending_application = state.get("pending_application") if isinstance(state.get("pending_application"), dict) else {}
|
|||
|
|
pending_steward_action = state.get("pending_steward_action") if isinstance(state.get("pending_steward_action"), dict) else {}
|
|||
|
|
waiting_for = str(state.get("waiting_for") or "").strip()
|
|||
|
|
message = request.user_message.replace(" ", "")
|
|||
|
|
confirmation_text = message in {"确认", "确定", "无误", "确认提交", "可以提交", "提交", "没问题"}
|
|||
|
|
if confirmation_text and pending_application.get("ready_to_submit"):
|
|||
|
|
return StewardRuntimeDecisionResponse(
|
|||
|
|
decision_source="rule_fallback",
|
|||
|
|
next_action="submit_current_application",
|
|||
|
|
target_message_id=str(pending_application.get("message_id") or ""),
|
|||
|
|
target_task_id=str(pending_application.get("task_id") or ""),
|
|||
|
|
rationale="模型运行时决策暂不可用,我先按当前待提交申请单上下文处理你的确认。",
|
|||
|
|
model_call_traces=traces,
|
|||
|
|
)
|
|||
|
|
if confirmation_text and pending_steward_action:
|
|||
|
|
return StewardRuntimeDecisionResponse(
|
|||
|
|
decision_source="rule_fallback",
|
|||
|
|
next_action="continue_next_task",
|
|||
|
|
target_message_id=str(pending_steward_action.get("message_id") or ""),
|
|||
|
|
target_task_id=str(pending_steward_action.get("target_task_id") or ""),
|
|||
|
|
rationale="模型运行时决策暂不可用,我先按当前待确认的下一项任务继续处理。",
|
|||
|
|
model_call_traces=traces,
|
|||
|
|
)
|
|||
|
|
if waiting_for:
|
|||
|
|
return StewardRuntimeDecisionResponse(
|
|||
|
|
decision_source="rule_fallback",
|
|||
|
|
next_action="ask_user",
|
|||
|
|
question="我需要先确认当前等待事项,请补充或选择当前问题对应的信息。",
|
|||
|
|
rationale="模型运行时决策暂不可用,当前仍存在等待用户补充的信息。",
|
|||
|
|
model_call_traces=traces,
|
|||
|
|
)
|
|||
|
|
return StewardRuntimeDecisionResponse(
|
|||
|
|
decision_source="rule_fallback",
|
|||
|
|
next_action="plan_new_tasks",
|
|||
|
|
rationale="模型运行时决策暂不可用,当前没有可安全匹配的等待动作,回到任务规划。",
|
|||
|
|
model_call_traces=traces,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _clean_text(value: Any) -> str:
|
|||
|
|
return str(value or "").strip()
|