Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fadb705781 |
No files matched your search
@@ -14,11 +14,6 @@ Hy Memory and MemHub are versioned and released independently.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Clarified that `add(metadata=...)` carries legacy SDK processing options,
|
||||
while persistent caller business context belongs in `meta`.
|
||||
- Added an opt-in `get(..., include_raw=True)` path for reading `L1_RAW`
|
||||
memories, including memories written in lite mode. The default remains
|
||||
hidden for backward compatibility.
|
||||
- Removed MemHub's task-scoped HyPermission store wrappers and global
|
||||
model-content consent switch. The trusted embedded Hy Memory SDK now uses
|
||||
its native local stores directly.
|
||||
|
||||
@@ -106,15 +106,6 @@ result = client.add(
|
||||
)
|
||||
```
|
||||
|
||||
`metadata` 与 `meta` 不是同一字段:
|
||||
|
||||
- `metadata` 是兼容旧调用方保留的 SDK 处理选项,会进入内部
|
||||
`WriteRequest.extra`,不保证持久化或随查询结果返回。
|
||||
- `meta` 是调用方的业务上下文,会持久化并在 `search()`、`get()` 等结果中返回。
|
||||
同一记忆被多次合并时,各次 `meta` 会按写入顺序保存在列表中。
|
||||
|
||||
需要以后读回的数据必须放在 `meta`,不要放在 `metadata`。
|
||||
|
||||
`scene` 的公开取值:
|
||||
|
||||
- `None`:普通文本直接走 chat;messages 含 tool 时执行快速场景判断。
|
||||
@@ -272,8 +263,6 @@ catalog 也可以在没有基础 L0 时形成只读的合成画像。
|
||||
|
||||
```python
|
||||
memory = client.get("memory_id")
|
||||
# lite 模式写入的是 L1_RAW;按 ID 读取时必须显式选择返回原文。
|
||||
raw_memory = client.get("memory_id", include_raw=True)
|
||||
result = client.update("memory_id", "更新后的内容")
|
||||
result = client.delete("memory_id")
|
||||
result = client.delete_all(user_id="user_001")
|
||||
@@ -293,10 +282,6 @@ result = client.clone_user("user_001", "user_002", agent_id=None)
|
||||
对应异步方法为 `async_get()`、`async_update()`、`async_delete()`、
|
||||
`async_delete_all()`、`async_list_memories()`、`async_clone_user()`。
|
||||
|
||||
`get()` / `async_get()` 默认继续隐藏 `L1_RAW`,因此不会改变现有调用方的原文访问
|
||||
边界。只有显式传入 `include_raw=True` 才返回该层;这也是 lite 模式按 ID 读取
|
||||
写入内容及其 `meta` 的方式。MemHub 的 HTTP 读取接口不开放此参数。
|
||||
|
||||
普通 `list_memories()` 返回 `vdb` 桶,并在 Graph 有结果时附带 `graph`;传入
|
||||
`workspace_id` 时返回生产力记忆的 `scene="productivity"` 和 `experiences`。
|
||||
|
||||
|
||||
+12
-39
@@ -1597,9 +1597,7 @@ class HyMemoryClient:
|
||||
session_id: Session ID(三级隔离 key)
|
||||
source: 调用来源,例如 openclaw / cursor / codex;与 agent_id 正交。
|
||||
写入前执行 strip().lower(),默认 "default"。
|
||||
metadata: 旧版 SDK 处理选项,会并入内部 WriteRequest.extra。
|
||||
不保证持久化或随查询返回;调用方需要保存并读回的业务字段
|
||||
请使用 meta。
|
||||
metadata: 自定义元数据
|
||||
memory_at: 记忆时间戳(不传则用当前时间),用于导入历史记忆
|
||||
scene: 显式指定写入路由,跳过 LLM 自动判定。
|
||||
- None(默认):含 tool 时 LLM 判 chat/working/coding;
|
||||
@@ -1625,8 +1623,8 @@ class HyMemoryClient:
|
||||
branch: Coding memory 路径专用。分支名。同 workspace_id。chat 路径忽略。
|
||||
meta: 调用方自定义业务 meta,随本次产出的记忆存储、查询时原样返回。
|
||||
不 embed、不参与召回、不作过滤条件。
|
||||
与 metadata 不同:metadata 是旧版内部处理选项,
|
||||
meta 独立持久化,只做业务透传。
|
||||
与 metadata 不同:metadata 会并入 extra(含 SDK 内部控制键),
|
||||
meta 独立存放,只做透传。
|
||||
语义为「该次 write 时刻的上下文」,故存为 list——同一节点被
|
||||
多次写入合并时会追加而非覆盖,详见 get() 返回的 meta 字段。
|
||||
|
||||
@@ -1793,26 +1791,17 @@ class HyMemoryClient:
|
||||
query_embedding=query_embedding,
|
||||
))
|
||||
|
||||
def get(
|
||||
self,
|
||||
memory_id: str,
|
||||
*,
|
||||
include_raw: bool = False,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
def get(self, memory_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取单条记忆。
|
||||
|
||||
Args:
|
||||
memory_id: 记忆 ID
|
||||
include_raw: 是否允许返回 L1_RAW 原始记忆。默认 False,保持原有隐藏行为;
|
||||
lite 模式的记忆均为 L1_RAW,需显式设为 True 才能按 ID 读取。
|
||||
|
||||
Returns:
|
||||
{"memory_id", "content", "layer", "request_id", "elapsed_ms"} 或 None(不存在时)
|
||||
"""
|
||||
return self._loop_thread.run(
|
||||
self.async_get(memory_id, include_raw=include_raw)
|
||||
)
|
||||
return self._loop_thread.run(self.async_get(memory_id))
|
||||
|
||||
def update(
|
||||
self,
|
||||
@@ -3661,21 +3650,12 @@ class HyMemoryClient:
|
||||
# ================================================================
|
||||
|
||||
@_ensure_internal_loop
|
||||
async def async_get(
|
||||
self,
|
||||
memory_id: str,
|
||||
*,
|
||||
include_raw: bool = False,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
async def async_get(self, memory_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取单条记忆(异步)。
|
||||
|
||||
VDB + Graph 双路并行查询,哪个有结果返回哪个。
|
||||
|
||||
Args:
|
||||
memory_id: 记忆 ID
|
||||
include_raw: 是否允许返回 L1_RAW 原始记忆,默认 False。
|
||||
|
||||
Returns:
|
||||
{"memory_id", "content", "layer", "source", "request_id", "elapsed_ms"} 或 None
|
||||
"""
|
||||
@@ -3703,24 +3683,17 @@ class HyMemoryClient:
|
||||
|
||||
vdb_node, graph_node = await _asyncio_get.gather(_get_vdb(), _get_graph())
|
||||
|
||||
# L1_RAW 默认不对外可见:按 ID 直取走主键查询、绕过向量库的 layer
|
||||
# filter,故在此统一挡掉。SDK 调用方可通过 include_raw=True 显式读取;
|
||||
# HTTP 接口不传该参数,继续保持隐藏。内部写链路不经过此方法。
|
||||
# L1_RAW 不对外可见:按 ID 直取走主键查询、绕过向量库的 layer filter,
|
||||
# 故在此统一挡掉。async_get 是 GET /v1/memories/{id} 与
|
||||
# /v1/memories/batch 两个对外接口的共同必经处,而内部写链路不经过它
|
||||
# (writer 写后校验直接调 vector_store.get_by_id),挡在此处不影响内部。
|
||||
#
|
||||
# 置于 gather 之后、埋点之前:命中 raw 仍照常落 GET 埋点(保留可观测性,
|
||||
# 便于发现有调用方拿 raw id 试探),只是不把内容返回给调用方。
|
||||
# 对调用方的表现与"该 id 不存在"完全一致,不额外暴露"存在但被挡"的信息。
|
||||
if (
|
||||
not include_raw
|
||||
and vdb_node is not None
|
||||
and vdb_node.layer == MemoryLayer.L1_RAW
|
||||
):
|
||||
if vdb_node is not None and vdb_node.layer == MemoryLayer.L1_RAW:
|
||||
vdb_node = None
|
||||
if (
|
||||
not include_raw
|
||||
and graph_node is not None
|
||||
and getattr(graph_node, "layer", None) == MemoryLayer.L1_RAW
|
||||
):
|
||||
if graph_node is not None and getattr(graph_node, "layer", None) == MemoryLayer.L1_RAW:
|
||||
graph_node = None
|
||||
|
||||
elapsed = round((time.perf_counter() - t0) * 1000, 2)
|
||||
|
||||
@@ -12,9 +12,8 @@ L1_RAW 对外读路径不可见:闸门焊在向量库唯一查询入口。
|
||||
hybrid_search_native / list_by_user 四个查询方法的共同必经处,
|
||||
上层无法绕开,新增接口自动受保护。
|
||||
|
||||
按 ID 直取走主键查询、不经过 filter,故在 `client.async_get` 处默认挡住。
|
||||
Python SDK 调用方可显式传 `include_raw=True` 读取;HTTP 接口不传该参数,
|
||||
继续保持原有隐藏行为。
|
||||
按 ID 直取走主键查询、不经过 filter,故在 `client.async_get` 处单独挡
|
||||
(GET /v1/memories/{id} 与 /v1/memories/batch 的共同必经处)。
|
||||
|
||||
内部管线需读原始对话时显式 allow_raw_layer=True 放行:
|
||||
- `writer._get_recent_dialogue_history`:提取前拼多轮对话历史喂 LLM
|
||||
@@ -125,63 +124,28 @@ class TestListNoLongerFiltersInProcess:
|
||||
"list 又出现进程内过滤 raw 的写法,应交由 vdb 侧 filter 完成"
|
||||
|
||||
|
||||
class TestGetByIdRawOptIn:
|
||||
"""按 ID 直取默认隐藏 raw,只有 SDK 显式选择时才返回。"""
|
||||
class TestGetByIdPathBlocksRaw:
|
||||
"""按 ID 直取:走主键查询不经 filter,须在 async_get 处挡住。"""
|
||||
|
||||
@staticmethod
|
||||
def _client(node):
|
||||
from hy_memory.client import HyMemoryClient
|
||||
|
||||
class Store:
|
||||
async def get_by_id(self, memory_id):
|
||||
return node if memory_id == node.node_id else None
|
||||
|
||||
async def no_op_log(**kwargs):
|
||||
return None
|
||||
|
||||
client = HyMemoryClient.__new__(HyMemoryClient)
|
||||
client._vector_store = Store()
|
||||
client._graph_store = None
|
||||
client._backend_identity = lambda: {}
|
||||
client._maybe_store_op_log = no_op_log
|
||||
return client
|
||||
|
||||
@staticmethod
|
||||
def _raw_node():
|
||||
return MemoryNode(
|
||||
node_id="raw-1",
|
||||
user_id="u1",
|
||||
agent_id="a1",
|
||||
session_id="s1",
|
||||
content="原始对话",
|
||||
layer=MemoryLayer.L1_RAW,
|
||||
status=MemoryStatus.ACTIVE,
|
||||
custom={"meta": [{"source": "test"}]},
|
||||
)
|
||||
|
||||
def test_default_still_hides_raw(self):
|
||||
from hy_memory.client import HyMemoryClient
|
||||
|
||||
client = self._client(self._raw_node())
|
||||
raw_get = HyMemoryClient.async_get.__wrapped__
|
||||
assert run(raw_get(client, "raw-1")) is None
|
||||
|
||||
def test_explicit_include_raw_returns_lite_memory(self):
|
||||
from hy_memory.client import HyMemoryClient
|
||||
|
||||
client = self._client(self._raw_node())
|
||||
raw_get = HyMemoryClient.async_get.__wrapped__
|
||||
result = run(raw_get(client, "raw-1", include_raw=True))
|
||||
|
||||
assert result["memory_id"] == "raw-1"
|
||||
assert result["content"] == "原始对话"
|
||||
assert result["layer"] == MemoryLayer.L1_RAW.value
|
||||
assert result["meta"] == [{"source": "test"}]
|
||||
|
||||
def test_public_get_exposes_keyword_only_opt_in(self):
|
||||
def test_async_get_drops_raw_node(self):
|
||||
import inspect
|
||||
from hy_memory.client import HyMemoryClient
|
||||
src = inspect.getsource(HyMemoryClient.async_get)
|
||||
assert "MemoryLayer.L1_RAW" in src, \
|
||||
"async_get 未挡 raw,batch 与按 ID 单取会漏出原始对话"
|
||||
|
||||
param = inspect.signature(HyMemoryClient.get).parameters["include_raw"]
|
||||
assert param.default is False
|
||||
assert param.kind is inspect.Parameter.KEYWORD_ONLY
|
||||
def test_block_happens_before_return(self):
|
||||
"""挡的位置必须在两处 return 之前,否则等于没挡。"""
|
||||
import inspect
|
||||
from hy_memory.client import HyMemoryClient
|
||||
lines = inspect.getsource(HyMemoryClient.async_get).splitlines()
|
||||
block_at = next(
|
||||
i for i, l in enumerate(lines)
|
||||
if "MemoryLayer.L1_RAW" in l and "vdb_node" in l
|
||||
)
|
||||
first_return_at = next(
|
||||
i for i, l in enumerate(lines)
|
||||
if l.strip().startswith("return {")
|
||||
)
|
||||
assert block_at < first_return_at, \
|
||||
f"挡 raw 在第 {block_at} 行、首个 return 在第 {first_return_at} 行,顺序错了"
|
||||
Reference in new issue
Block a user