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))
|