158 lines
6.4 KiB
Python
158 lines
6.4 KiB
Python
|
|
"""小财管家业务无关输入的引导生成 agent。
|
|||
|
|
|
|||
|
|
当用户的输入被识别为与财务任务无关时(问候、纯数字、闲聊等),
|
|||
|
|
由该 agent 用 function calling 让主模型生成一句管家对主人语气的引导回复。
|
|||
|
|
LLM 不可用或调用失败时,由调用方 fallback 到规则模板。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
from dataclasses import dataclass
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
from app.schemas.steward import StewardPlanRequest
|
|||
|
|
from app.services.runtime_chat import RuntimeChatService
|
|||
|
|
|
|||
|
|
|
|||
|
|
STEWARD_OFF_TOPIC_FUNCTION_NAME = "submit_steward_off_topic_response"
|
|||
|
|
|
|||
|
|
|
|||
|
|
STEWARD_OFF_TOPIC_SCENARIO_PROMPTS: dict[str, str] = {
|
|||
|
|
"greeting": (
|
|||
|
|
"用户发起了礼貌问候,例如「你好」「您好」「早上好」。"
|
|||
|
|
"请像管家一样礼貌地回应主人的问候,温和询问主人今天要办理什么业务,"
|
|||
|
|
"并顺势说明小财管家能帮主人整理费用申请和费用报销两类事项。"
|
|||
|
|
"可以再提示主人试试用具体的话术来表达需求。"
|
|||
|
|
),
|
|||
|
|
"off_business": (
|
|||
|
|
"用户说了有意义的话但与财务无关,例如问天气、聊生活、聊工作日常。"
|
|||
|
|
"请温和地告诉主人:这句话里没有识别到财务事项,"
|
|||
|
|
"小财管家目前只能帮主人整理费用申请和费用报销。"
|
|||
|
|
"再请主人用具体的话术告诉小财管家他/她想办什么业务。"
|
|||
|
|
),
|
|||
|
|
"meaningless": (
|
|||
|
|
"用户输入了与财务无关且难以理解的内容,例如纯数字、纯标点、重复字符。"
|
|||
|
|
"请温和地告诉主人:这句话里好像没有出现费用申请、报销、出差、交通、招待这些关键词,"
|
|||
|
|
"请主人换种说法再告诉小财管家一次。"
|
|||
|
|
),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass(frozen=True, slots=True)
|
|||
|
|
class StewardOffTopicAgentResult:
|
|||
|
|
response_text: str
|
|||
|
|
model_call_traces: list[dict[str, Any]]
|
|||
|
|
|
|||
|
|
|
|||
|
|
class StewardOffTopicAgent:
|
|||
|
|
"""使用大模型 function calling 生成小财管家对业务无关输入的多样化引导。"""
|
|||
|
|
|
|||
|
|
def __init__(self, runtime_chat_service: RuntimeChatService) -> None:
|
|||
|
|
self.runtime_chat_service = runtime_chat_service
|
|||
|
|
self.last_call_traces: list[dict[str, Any]] = []
|
|||
|
|
|
|||
|
|
def generate(
|
|||
|
|
self,
|
|||
|
|
request: StewardPlanRequest,
|
|||
|
|
*,
|
|||
|
|
scenario: str,
|
|||
|
|
) -> StewardOffTopicAgentResult | None:
|
|||
|
|
messages = self._build_messages(request, scenario=scenario)
|
|||
|
|
try:
|
|||
|
|
result = self.runtime_chat_service.complete_with_tool_call(
|
|||
|
|
messages,
|
|||
|
|
tools=[self._build_tool_schema()],
|
|||
|
|
tool_choice={
|
|||
|
|
"type": "function",
|
|||
|
|
"function": {"name": STEWARD_OFF_TOPIC_FUNCTION_NAME},
|
|||
|
|
},
|
|||
|
|
max_tokens=400,
|
|||
|
|
temperature=0.7,
|
|||
|
|
timeout_seconds=15,
|
|||
|
|
max_attempts=1,
|
|||
|
|
)
|
|||
|
|
except Exception:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
self.last_call_traces = result.calls_as_dicts()
|
|||
|
|
if result.tool_call is None or result.tool_call.name != STEWARD_OFF_TOPIC_FUNCTION_NAME:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
arguments = result.tool_call.arguments or {}
|
|||
|
|
response_text = str(arguments.get("response_text") or "").strip()
|
|||
|
|
if not response_text:
|
|||
|
|
return None
|
|||
|
|
return StewardOffTopicAgentResult(
|
|||
|
|
response_text=response_text,
|
|||
|
|
model_call_traces=self.last_call_traces,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _build_messages(
|
|||
|
|
request: StewardPlanRequest,
|
|||
|
|
*,
|
|||
|
|
scenario: str,
|
|||
|
|
) -> list[dict[str, Any]]:
|
|||
|
|
scenario_hint = STEWARD_OFF_TOPIC_SCENARIO_PROMPTS.get(
|
|||
|
|
scenario,
|
|||
|
|
STEWARD_OFF_TOPIC_SCENARIO_PROMPTS["off_business"],
|
|||
|
|
)
|
|||
|
|
return [
|
|||
|
|
{
|
|||
|
|
"role": "system",
|
|||
|
|
"content": (
|
|||
|
|
"你是 X-Financial 的小财管家,要像一位贴身的管家那样为主人服务。"
|
|||
|
|
"用户输入的内容与财务任务无关,你需要生成一段温和、尊敬的引导回复。"
|
|||
|
|
"要求:\n"
|
|||
|
|
"1. 始终称呼用户为「主人」「您」,不要用「你」。\n"
|
|||
|
|
"2. 语气尊敬、温和、主动,体现管家的服务意识。\n"
|
|||
|
|
"3. 不要每次都用相同的句式,要根据用户的输入和当前场景变化表达。\n"
|
|||
|
|
"4. 控制在 2-4 句,不要过长。\n"
|
|||
|
|
"5. 必须使用 function calling 输出 response_text,不要返回普通文本。\n"
|
|||
|
|
"6. response_text 使用 Markdown,第一行用 ### 标题,正文与引导句之间留空行。\n"
|
|||
|
|
"7. 如果主人是在问候,先礼貌回应再引导;"
|
|||
|
|
"如果主人说的是非财务话题,温和说明小财管家能做什么;"
|
|||
|
|
"如果主人的内容没有意义,请他/她换种说法。\n"
|
|||
|
|
f"当前场景:{scenario_hint}"
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"role": "user",
|
|||
|
|
"content": json.dumps(
|
|||
|
|
{
|
|||
|
|
"message": request.message,
|
|||
|
|
"scenario": scenario,
|
|||
|
|
},
|
|||
|
|
ensure_ascii=False,
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _build_tool_schema() -> dict[str, Any]:
|
|||
|
|
return {
|
|||
|
|
"type": "function",
|
|||
|
|
"function": {
|
|||
|
|
"name": STEWARD_OFF_TOPIC_FUNCTION_NAME,
|
|||
|
|
"description": (
|
|||
|
|
"提交小财管家对业务无关输入的引导回复。"
|
|||
|
|
"response_text 是完整 Markdown 文本:"
|
|||
|
|
"第一行 ### 标题(基于场景),空行,正文(歉意或回应 + 能力范围 + 引导句)。"
|
|||
|
|
),
|
|||
|
|
"parameters": {
|
|||
|
|
"type": "object",
|
|||
|
|
"properties": {
|
|||
|
|
"response_text": {
|
|||
|
|
"type": "string",
|
|||
|
|
"description": (
|
|||
|
|
"面向用户的引导回复 Markdown 文本。"
|
|||
|
|
"称呼用户为「主人」「您」,不要用「你」。"
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
"required": ["response_text"],
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
}
|