1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富 统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」; 同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。 2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本), 新增《文档规整方案与开发前待决事项-2026-09-17》。 3) 客服agent 四份交付文档首次纳入本分支。
88 lines
4.3 KiB
Python
88 lines
4.3 KiB
Python
"""示例业务 Agent:查询场内基金行情,演示组员接入底座的完整生产路径。
|
||
|
||
这份实现刻意只做三件事:声明 `AgentDefinition`、实现 `handle()`、通过底座方法
|
||
`self.call_tool(...)` 调用公共只读工具。鉴权、发布配置解析、记忆召回、意图分类、
|
||
工具白名单、来源引用、合规审查、审计和持久化全部由底座完成,业务代码不重复实现。
|
||
|
||
接入要点(照做即可复用):
|
||
|
||
1. 意图码必须同时出现在三处:`AgentDefinition.supported_intents`、当前 **active**
|
||
的 `config_release` 中 `namespace=agent_tools` 的 `config_key=<agent_type>:<intent>`,
|
||
以及 `self.call_tool(..., intent=...)` 的实参;
|
||
2. 工具名必须同时出现在两处:`AgentDefinition.allowed_tools`(代码上限)与该意图的
|
||
白名单配置(发布配置只能缩小、不能放大代码声明);
|
||
3. 工具返回值只做展示与摘要,不得改写为成交、委托或持仓语义;`degraded=true` 时
|
||
必须显式提示数据降级。
|
||
"""
|
||
|
||
import re
|
||
from typing import Any
|
||
|
||
from app.core.contracts import AgentDefinition, AgentRequest, CoreResult, RequestContext
|
||
from app.service.agent.base import BaseAgent
|
||
|
||
# 意图码:与 AgentDefinition.supported_intents、发布版工具白名单的 key 必须完全一致。
|
||
INTENT_FUND_QUOTE = "fund_quote"
|
||
# 工具名:必须在 bootstrap 的 ToolRegistry 中已注册,且权限/角色满足调用方身份。
|
||
TOOL_NAME = "query_fund_quote"
|
||
# 消息里没有六位代码时的默认查询对象:深交所场内 ETF(在行情工具默认允许代码表内)。
|
||
DEFAULT_FUND_CODE = "159382"
|
||
# 单次运行最多查询的代码数量,避免消息里的数字被无边界地当作代码使用。
|
||
MAX_FUND_CODES = 3
|
||
# 六位数字代码识别:用前后界防止把「2026-09-10」这类数字串切出假代码。
|
||
CODE_PATTERN = re.compile(r"(?<!\d)(\d{6})(?!\d)")
|
||
|
||
|
||
def extract_fund_codes(message: str) -> tuple[str, ...]:
|
||
"""从用户消息里提取基金代码;没有命中时回退到默认代码。"""
|
||
found = tuple(dict.fromkeys(CODE_PATTERN.findall(message)))
|
||
return found[:MAX_FUND_CODES] or (DEFAULT_FUND_CODE,)
|
||
|
||
|
||
def describe_quote(quote: dict[str, Any]) -> str:
|
||
"""把行情字典整理成一行可读结论,保留来源与降级标记。"""
|
||
code = str(quote.get("fund_code", ""))
|
||
name = str(quote.get("fund_name") or f"基金 {code}")
|
||
nav = quote.get("nav")
|
||
nav_text = f"净值 {nav}" if nav is not None else "净值暂缺"
|
||
nav_date = str(quote.get("nav_date") or "无")
|
||
source = str(quote.get("quote_source") or "unknown")
|
||
degraded = bool(quote.get("degraded"))
|
||
if degraded:
|
||
tail = "(数据已降级,仅供参考,不构成交易依据)"
|
||
else:
|
||
tail = "(行情仅供查询参考,不构成交易依据)"
|
||
return f"{code} {name}:{nav_text},净值日期 {nav_date},来源 {source}{tail}"
|
||
|
||
|
||
class FundQueryDemoAgent(BaseAgent):
|
||
"""最小可用的业务 Agent:一个意图 + 一个公共只读工具。"""
|
||
|
||
definition = AgentDefinition(
|
||
agent_type="fund_query_demo",
|
||
version="1.0.0",
|
||
allowed_roles=("customer", "advisor", "operator", "admin"),
|
||
allowed_portals=("api",),
|
||
# 代码上限:实际可用范围由发布配置的意图白名单收窄。
|
||
allowed_tools=(TOOL_NAME,),
|
||
supported_intents=(INTENT_FUND_QUOTE,),
|
||
)
|
||
|
||
async def handle(self, request: AgentRequest, context: RequestContext) -> CoreResult:
|
||
codes = extract_fund_codes(request.message)
|
||
output = await self.call_tool(
|
||
TOOL_NAME,
|
||
{"fund_codes": list(codes), "limit": len(codes)},
|
||
intent=INTENT_FUND_QUOTE,
|
||
context=context,
|
||
)
|
||
quotes: list[dict[str, Any]] = (
|
||
[item for item in output if isinstance(item, dict)] if isinstance(output, list) else []
|
||
)
|
||
if not quotes:
|
||
joined = "、".join(codes)
|
||
return CoreResult(text=f"未查询到 {joined} 的可用行情,请稍后重试或转人工核实。")
|
||
# tool_calls 与 source_references 由底座在 handle() 返回后统一附加,
|
||
# 业务代码不得自行伪造来源引用。
|
||
return CoreResult(text="\n".join(describe_quote(quote) for quote in quotes))
|