1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富 统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」; 同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。 2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本), 新增《文档规整方案与开发前待决事项-2026-09-17》。 3) 客服agent 四份交付文档首次纳入本分支。
103 lines
5.2 KiB
Python
103 lines
5.2 KiB
Python
"""记忆召回演示(只读):**同一个客户,换不同身份去读,看谁能读到、谁读不到。**
|
||
|
||
## 为什么需要它
|
||
|
||
"记忆系统的权限"这套东西光讲没用 —— 演示时最好当场看到:
|
||
用**客户本人**的身份能读到自己,用**别的客户**身份读到的是空的,
|
||
用**有权限且有归属**的风控身份能读到,用**没权限**的运营身份读不到(而且日志会点名原因)。
|
||
|
||
这个脚本就是干这个的:按平台**真实**的身份解析(`IdentityService`,读的是库里的
|
||
角色/权限/客户归属)构造上下文,再走**产线同一条召回链路**
|
||
(`PlatformGovernance` + `build_memory_recall_service`,含 Redis 热缓存与 Milvus 语义通道),
|
||
逐个身份打印召回结果与降级原因。全部只读,不写任何数据。
|
||
|
||
用法::
|
||
|
||
python tools/memory_recall_demo.py # 默认看客户 9001
|
||
python tools/memory_recall_demo.py --customer 10001
|
||
python tools/memory_recall_demo.py --identity 9002 # 只看某个身份
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import asyncio
|
||
import sys
|
||
|
||
from app.core.contracts import RequestContext
|
||
from app.core.memory_scope import REQUIRED_EMPLOYEE_PERMISSION, customer_memory_scope
|
||
from app.service.agent.bootstrap import build_memory_recall_service
|
||
from app.service.agent.governance import PlatformGovernance
|
||
from app.service.identity_service import IdentityService
|
||
|
||
if hasattr(sys.stdout, "reconfigure"):
|
||
sys.stdout.reconfigure(errors="replace")
|
||
|
||
#: 演示身份:(显示名, user_id, 说明)。真实的角色/权限/归属都从库里查,不在这里编。
|
||
DEMO_IDENTITIES: tuple[tuple[str, str, str], ...] = (
|
||
("客户本人(9001 / cust_t)", "9001", "客户身份:只读自己"),
|
||
("另一个客户(12001)", "12001", "客户身份:读的是**他自己**名下,不是 9001"),
|
||
("风控专员(9002 / risk_t)", "9002", "员工:有 memory:read:customer 且 9001 已分配给他"),
|
||
("投顾(9020 / advisor_t)", "9020", "员工:同上,看归属表里有没有"),
|
||
("运营(9006 / offsite_t)", "9006", "员工:**没有** memory:read:customer → 失败关闭"),
|
||
("管理员(9003 / admin_t)", "9003", "员工:有权限码,但要看归属表"),
|
||
)
|
||
|
||
|
||
def _reason(context: RequestContext, has_scope: bool) -> str:
|
||
"""把"为什么 0 条"讲成人话(判定本身仍由 `customer_memory_scope` 做,这里只解释)。"""
|
||
if not has_scope:
|
||
if REQUIRED_EMPLOYEE_PERMISSION not in context.permissions:
|
||
return (f"该身份缺少 {REQUIRED_EMPLOYEE_PERMISSION} 能力码 → **失败关闭**"
|
||
"(归属关系只解决'谁负责谁',不构成读他人客户数据的授权)")
|
||
return ("该身份在 `sys_customer_assignment` 里**没有生效的归属客户** → **失败关闭**"
|
||
"(这不是记忆坏了,是没授权;要读指定客户可先维护分配关系)")
|
||
return "可读范围内这些客户确实没有 active 的长期记忆(或都过期了)"
|
||
|
||
|
||
async def show(customer_id: int, only: str | None) -> None:
|
||
governance = PlatformGovernance(recall_factory=build_memory_recall_service)
|
||
identity = IdentityService()
|
||
|
||
for label, user_id, note in DEMO_IDENTITIES:
|
||
if only and only != user_id:
|
||
continue
|
||
context = await identity.resolve(
|
||
RequestContext(user_id=user_id, trace_id=f"memory-recall-demo-{user_id}")
|
||
)
|
||
items = await governance.recall(context)
|
||
scope = customer_memory_scope(context)
|
||
print("=" * 78)
|
||
print(f"{label} ({note})")
|
||
print(f" 角色={list(context.roles)} 数据范围={context.data_scope} "
|
||
f"归属客户={list(context.customer_ids)}")
|
||
print(f" memory:read:customer = "
|
||
f"{REQUIRED_EMPLOYEE_PERMISSION in context.permissions}")
|
||
print(f" 可读客户范围 = {list(scope) or '(空)'}")
|
||
if not items:
|
||
print(f" 召回:**0 条** —— 原因:{_reason(context, bool(scope))}")
|
||
continue
|
||
print(f" 召回:{len(items)} 条")
|
||
# `RecalledMemory` 就是**送进模型上下文**的那一份(只有 uuid/客户号/正文),
|
||
# 置信度与来源在更下层的 `RecallItem` 里,这里刻意不展开 —— 演示时讲清这一点即可。
|
||
for item in items:
|
||
print(f" · 客户 {item.customer_id}:{item.content[:60]} "
|
||
f"(uuid={item.memory_uuid[:8]}…)")
|
||
print("=" * 78)
|
||
print(f"提示:这里看的是**召回**(把记忆塞进模型上下文)。"
|
||
f"客户 {customer_id} 的记忆本体在 memory_unit 表里,用 "
|
||
f"`python tools/probe_memory_state.py {customer_id}` 看。")
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="记忆召回按身份的演示(只读)")
|
||
parser.add_argument("--customer", type=int, default=9001, help="被观察的客户号(默认 9001)")
|
||
parser.add_argument("--identity", default=None, help="只看某一个身份(user_id)")
|
||
args = parser.parse_args()
|
||
asyncio.run(show(args.customer, args.identity))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|