Files
group_fqcd_jr/tools/memory_recall_demo.py
T
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

103 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""记忆召回演示(只读):**同一个客户,换不同身份去读,看谁能读到、谁读不到。**
## 为什么需要它
"记忆系统的权限"这套东西光讲没用 —— 演示时最好当场看到:
用**客户本人**的身份能读到自己,用**别的客户**身份读到的是空的,
用**有权限且有归属**的风控身份能读到,用**没权限**的运营身份读不到(而且日志会点名原因)。
这个脚本就是干这个的:按平台**真实**的身份解析(`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())