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:
3 files changed
+147
-2
No files matched your search
@@ -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
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user