feat(sdk): search raw through existing search API (#134)

Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
authored and GitHub committed 2026-10-21 17:58:29 +08:00
1 parent 635d2572c9
commit 8aeb042bb7
3 files changed
+147 -2

No files matched your search

+3
View File
@@ -32,6 +32,9 @@ Hy Memory and MemHub are versioned and released independently.
## Unreleased
- `search(..., layers=["l1_raw"])` now explicitly searches SDK raw records in
pro/ultra mode and keeps existing user/agent/session/source filters; default
search continues to hide raw.
- Added `import_memory_exact` for idempotent migration of an existing semantic
memory with its canonical ID, lifecycle and supersession links, without
extraction or reconciliation.
+78 -2
View File
@@ -3469,6 +3469,66 @@ class HyMemoryClient:
"offset": offset,
}
async def _search_explicit_raw_layer(
self,
query: str,
*,
user_ids: List[str],
agent_ids: List[str],
session_ids: List[str],
include_sources: List[str],
exclude_sources: List[str],
limit: int,
min_score: float,
created_after: Optional[float],
request_id: str,
t0: float,
query_embedding: Optional[List[float]],
) -> Dict[str, Any]:
"""Implement `search(..., layers=['l1_raw'])` without a second API."""
from .models.memory import MemoryLayer, MemoryStatus
embedding = query_embedding or await self._embed_service.embed(query)
fetch = min(max(limit * (10 if session_ids else 2), limit), 10_000)
hits = await self._vector_store.search(
query_embedding=embedding,
user_ids=user_ids,
agent_ids=agent_ids,
include_sources=include_sources,
exclude_sources=exclude_sources,
layers=[MemoryLayer.L1_RAW],
limit=fetch,
score_threshold=min_score,
status_filter=[MemoryStatus.ACTIVE],
only_latest=False,
created_after=created_after,
)
sessions = set(session_ids)
rows: List[Dict[str, Any]] = []
for hit in hits:
node = hit.get("node")
if node is None or node.layer != MemoryLayer.L1_RAW:
continue
if sessions and node.session_id not in sessions:
continue
row = self._raw_node_to_public(node)
rows.append(
{
"memory_id": row.pop("raw_id"),
**row,
"layer": MemoryLayer.L1_RAW.value,
"status": MemoryStatus.ACTIVE.value,
"score": round(float(hit.get("score", 0.0)), 4),
}
)
if len(rows) >= limit:
break
return {
"request_id": request_id,
"memories": {"schema": [], "proactive": [], "normal": rows},
"elapsed_ms": round((time.perf_counter() - t0) * 1000, 2),
}
@staticmethod
def _public_search_views(
result_memories: List[Dict[str, Any]],
@@ -4129,6 +4189,22 @@ class HyMemoryClient:
effective_search_layers = [MemoryLayer.L1_RAW] if lite_mode else layers
effective_schema_limit = 0 if lite_mode else schema_limit
effective_intention_limit = 0 if lite_mode else intention_limit
normalized_search_layers = self._normalize_layers(effective_search_layers)
if normalized_search_layers == [MemoryLayer.L1_RAW]:
return await self._search_explicit_raw_layer(
effective_target_query,
user_ids=effective_user_ids,
agent_ids=effective_agent_ids,
session_ids=effective_session_ids,
include_sources=effective_include_sources,
exclude_sources=effective_exclude_sources,
limit=limit,
min_score=min_score,
created_after=effective_created_after,
request_id=request_id,
t0=t0,
query_embedding=query_embedding,
)
read_req = ReadRequest(
query=effective_target_query,
@@ -4149,7 +4225,7 @@ class HyMemoryClient:
layers=(
[
layer.value
for layer in self._normalize_layers(effective_search_layers)
for layer in normalized_search_layers
]
if effective_search_layers else None
),
@@ -4174,7 +4250,7 @@ class HyMemoryClient:
_wanted_layers = (
{
layer.value
for layer in self._normalize_layers(effective_search_layers)
for layer in normalized_search_layers
}
if effective_search_layers else None
)
+66
View File
@@ -65,6 +65,32 @@ class RawStore:
and (not include_sources or node.source in include_sources)
]
async def search(
self,
*,
user_ids,
agent_ids,
include_sources,
exclude_sources,
layers,
limit,
**_kwargs,
):
rows = []
for node in self.nodes.values():
if node.user_id not in user_ids:
continue
if agent_ids and node.agent_id not in agent_ids:
continue
if include_sources and node.source not in include_sources:
continue
if exclude_sources and node.source in exclude_sources:
continue
if node.layer not in layers:
continue
rows.append({"node": node, "score": 0.9})
return rows[:limit]
def _client():
client = HyMemoryClient.__new__(HyMemoryClient)
@@ -225,3 +251,43 @@ async def test_exact_memory_import_rejects_raw_layer():
layer="l1_raw",
request_id="request-memory-1",
)
@pytest.mark.asyncio
async def test_existing_search_contract_can_explicitly_search_raw_with_source_filter():
client = _client()
for raw_id, session_id, source in (
("raw-1", "s1", "cursor"),
("raw-2", "s2", "claude"),
):
await HyMemoryClient.async_import_raw.__wrapped__(
client,
f"content {raw_id}",
user_id="u1",
agent_id="a1",
session_id=session_id,
source=source,
raw_id=raw_id,
request_id=f"request-{raw_id}",
)
result = await client._search_explicit_raw_layer(
"content",
user_ids=["u1"],
agent_ids=["a1"],
session_ids=["s2"],
include_sources=["claude"],
exclude_sources=["cursor"],
limit=5,
min_score=0.0,
created_after=None,
request_id="search-1",
t0=0.0,
query_embedding=[0.1, 0.2],
)
assert [item["memory_id"] for item in result["memories"]["normal"]] == [
"raw-2"
]
assert result["memories"]["normal"][0]["layer"] == "l1_raw"
assert result["memories"]["normal"][0]["source"] == "claude"