* fix(faiss): reconstruct embeddings for dedup and episode summaries Normal upsert paths store only the payload and the FAISS index position. get_embeddings() looked for an "embedding" key that only update_payload ever wrote, so it returned an empty mapping for every upserted node. Search dedup and episode summaries therefore saw no vectors on the FAISS backend. Reconstruct the vector from the live index position, guarded by the same _idx_to_id check search() uses, and keep the stored fallback for nodes updated through update_payload. Add a regression test. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(search): pair dedup markers with their source search result The public items are filtered by the requested layers, but the private dedup view was built with zip(result.memories, public_items). Once one result is filtered out every later _memory_kind shifts by one, so a canonical_state can enter fuzzy dedup and be shadowed, or an ordinary fact can silently skip dedup. Build both views in the same loop, from the same source element, and add a regression test for the filtered case. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(episode): use the writer's vector store for background summaries _schedule_episode_summary accessed self.vector_store, which MemoryWriter never defines. The AttributeError was swallowed by the task's except block, so every closed episode silently skipped its L3 summary. Resolve the store through the existing async _get_vector_store() accessor and cover the scheduled task with a regression test. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: qiye.sjh <shijiahao_0802@youzan.com> Co-authored-by: Claude Code <noreply@anthropic.com>
58 lines
1.8 KiB
Python
58 lines
1.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""FAISS 后端的 get_embeddings 必须能取回普通 upsert 写入的向量。
|
|
|
|
``_data`` 只保存 payload 与内部下标,向量本体在 FAISS index 里。去重和
|
|
episode 摘要依赖 get_embeddings,取不到向量会让这些能力静默失效。
|
|
"""
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
pytest.importorskip("faiss")
|
|
|
|
from hy_memory.config import MemoryConfig # noqa: E402
|
|
from hy_memory.data.vector_store_faiss import FaissVectorStore # noqa: E402
|
|
from hy_memory.models.memory import MemoryLayer, MemoryNode # noqa: E402
|
|
|
|
|
|
def _config(tmp_path):
|
|
return MemoryConfig.from_dict({
|
|
"vector_store": {
|
|
"provider": "faiss",
|
|
"collection_name": "get_embeddings_test",
|
|
"persist_directory": str(tmp_path / "faiss"),
|
|
"embedding_dims": 4,
|
|
},
|
|
})
|
|
|
|
|
|
def test_get_embeddings_reconstructs_upserted_and_updated_vectors(tmp_path):
|
|
async def _run():
|
|
store = FaissVectorStore(_config(tmp_path))
|
|
await store.initialize()
|
|
try:
|
|
node = MemoryNode(
|
|
user_id="u1",
|
|
agent_id="a1",
|
|
session_id="s1",
|
|
source="default",
|
|
layer=MemoryLayer.L1_RAW,
|
|
content="hello",
|
|
embedding=[1.0, 0.0, 0.0, 0.0],
|
|
)
|
|
await store.upsert(node)
|
|
|
|
got = await store.get_embeddings([node.node_id])
|
|
assert got[node.node_id] == pytest.approx([1.0, 0.0, 0.0, 0.0])
|
|
|
|
assert await store.update_embedding(node.node_id, [0.0, 1.0, 0.0, 0.0])
|
|
got = await store.get_embeddings([node.node_id])
|
|
assert got[node.node_id] == pytest.approx([0.0, 1.0, 0.0, 0.0])
|
|
|
|
assert await store.get_embeddings(["missing"]) == {}
|
|
finally:
|
|
await store.close()
|
|
|
|
asyncio.run(_run())
|