Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions src/powermem/core/async_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,13 @@ def _is_llm_disabled(self) -> bool:
"""Return True when PowerMem is running without LLM-backed features."""
return self.llm_provider == "noop" or getattr(self.llm, "is_noop", False) is True

def _is_embedding_disabled(self) -> bool:
"""Return True when embedding is explicitly disabled (EMBEDDING_PROVIDER=none)."""
return (
getattr(self, "embedding_provider", None) == "none"
or getattr(getattr(self, "embedding", None), "is_noop", False) is True
)

def _get_component_config(self, component: str) -> Dict[str, Any]:
"""
Helper method to get component configuration uniformly.
Expand Down Expand Up @@ -1046,6 +1053,13 @@ async def search(
filters: Optional[Dict[str, Any]] = None,
limit: int = 30,
threshold: Optional[float] = None,
retrieval_mode: str = "auto",
fusion: str = "rrf",
vector_weight: Optional[float] = None,
fts_weight: Optional[float] = None,
rrf_k: int = 60,
candidate_limit: Optional[int] = None,
include_explanation: bool = False,
) -> Dict[str, Any]:
"""Search for memories asynchronously.

Expand Down Expand Up @@ -1073,8 +1087,23 @@ async def search(
# Select embedding service based on filters (for sub-store routing)
embedding_service = self._get_embedding_service(filters)

# Generate query embedding asynchronously
query_embedding = await asyncio.to_thread(embedding_service.embed, query, memory_action="search")
query_embedding = None
if retrieval_mode != "fts":
if self._is_embedding_disabled():
return {
"results": [],
"relations": []
}
try:
query_embedding = await asyncio.to_thread(
embedding_service.embed, query, memory_action="search"
)
except Exception as exc:
logger.warning(
"Search embedding failed; falling back to text search "
"when available: %s",
exc,
)


# Search in storage asynchronously - pass query text to enable hybrid search
Expand All @@ -1085,7 +1114,15 @@ async def search(
run_id=run_id,
filters=filters,
limit=limit,
query=query # Pass query text for hybrid search (vector + full-text)
query=query, # Pass query text for hybrid search (vector + full-text)
threshold=threshold,
retrieval_mode=retrieval_mode,
fusion=fusion,
vector_weight=vector_weight,
fts_weight=fts_weight,
rrf_k=rrf_k,
candidate_limit=candidate_limit,
include_explanation=include_explanation,
)

# Process results with intelligence manager (only if enabled to avoid unnecessary calls)
Expand Down Expand Up @@ -1125,7 +1162,9 @@ async def search(
# Quality score represents absolute similarity quality (0-1 range)
# It's calculated from weighted average of all search paths' similarity scores
metadata = result.get("metadata", {})
quality_score = metadata.get("_quality_score")
quality_score = result.get("_quality_score")
if quality_score is None:
quality_score = metadata.get("_quality_score")

# If quality_score is not available (e.g., from older data or non-hybrid search),
# fall back to using the ranking score
Expand All @@ -1139,7 +1178,7 @@ async def search(

transformed_result = {
"memory": result.get("memory", ""),
"metadata": metadata, # Keep metadata as-is from storage (includes debug info like _quality_score)
"metadata": metadata,
"score": score,
}
# Preserve other fields if needed
Expand Down
8 changes: 8 additions & 0 deletions src/powermem/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ def search(
run_id: Optional[str] = None,
filters: Optional[Dict[str, Any]] = None,
limit: int = 30,
threshold: Optional[float] = None,
retrieval_mode: str = "auto",
fusion: str = "rrf",
vector_weight: Optional[float] = None,
fts_weight: Optional[float] = None,
rrf_k: int = 60,
candidate_limit: Optional[int] = None,
include_explanation: bool = False,
) -> Dict[str, Any]:
"""
Search for memories.
Expand Down
67 changes: 62 additions & 5 deletions src/powermem/core/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,14 @@ def search(
run_id: Optional[str] = None,
filters: Optional[Dict[str, Any]] = None,
limit: int = 30,
threshold: Optional[float] = None,
retrieval_mode: str = "auto",
fusion: str = "rrf",
vector_weight: Optional[float] = None,
fts_weight: Optional[float] = None,
rrf_k: int = 60,
candidate_limit: Optional[int] = None,
include_explanation: bool = False,
**_: Any,
) -> Dict[str, Any]:
data = self._request(
Expand All @@ -266,6 +274,14 @@ def search(
"run_id": run_id,
"filters": filters,
"limit": limit,
"threshold": threshold,
"retrieval_mode": retrieval_mode,
"fusion": fusion,
"vector_weight": vector_weight,
"fts_weight": fts_weight,
"rrf_k": rrf_k,
"candidate_limit": candidate_limit,
"include_explanation": include_explanation,
},
)
results = data.get("results", []) if isinstance(data, dict) else []
Expand Down Expand Up @@ -807,7 +823,10 @@ def _is_llm_disabled(self) -> bool:

def _is_embedding_disabled(self) -> bool:
"""Return True when embedding is explicitly disabled (EMBEDDING_PROVIDER=none)."""
return self.embedding_provider == "none" or getattr(self.embedding, "is_noop", False) is True
return (
getattr(self, "embedding_provider", None) == "none"
or getattr(getattr(self, "embedding", None), "is_noop", False) is True
)

def _embed(self, text: str) -> Optional[List[float]]:
"""Embed text, returning None when embedding is disabled or fails."""
Expand Down Expand Up @@ -1700,6 +1719,13 @@ def search(
filters: Optional[Dict[str, Any]] = None,
limit: int = 30,
threshold: Optional[float] = None,
retrieval_mode: str = "auto",
fusion: str = "rrf",
vector_weight: Optional[float] = None,
fts_weight: Optional[float] = None,
rrf_k: int = 60,
candidate_limit: Optional[int] = None,
include_explanation: bool = False,
) -> Dict[str, Any]:
"""Search for memories.

Expand Down Expand Up @@ -1727,6 +1753,13 @@ def search(
filters=filters,
limit=limit,
threshold=threshold,
retrieval_mode=retrieval_mode,
fusion=fusion,
vector_weight=vector_weight,
fts_weight=fts_weight,
rrf_k=rrf_k,
candidate_limit=candidate_limit,
include_explanation=include_explanation,
)

if not query or not query.strip():
Expand All @@ -1738,8 +1771,23 @@ def search(
# Select embedding service based on filters (for sub-store routing)
embedding_service = self._get_embedding_service(filters)

# Generate query embedding
query_embedding = embedding_service.embed(query, memory_action="search")
query_embedding = None
if retrieval_mode != "fts":
if self._is_embedding_disabled():
return {
"results": [],
"relations": []
}
try:
query_embedding = embedding_service.embed(
query, memory_action="search"
)
except Exception as exc:
logger.warning(
"Search embedding failed; falling back to text search "
"when available: %s",
exc,
)


# Search in storage - pass query text to enable hybrid search
Expand All @@ -1752,6 +1800,13 @@ def search(
limit=limit,
query=query, # Pass query text for hybrid search (vector + full-text + sparse vector)
threshold=threshold, # Pass threshold to storage for native hybrid search condition check
retrieval_mode=retrieval_mode,
fusion=fusion,
vector_weight=vector_weight,
fts_weight=fts_weight,
rrf_k=rrf_k,
candidate_limit=candidate_limit,
include_explanation=include_explanation,
)

# Process results with intelligence manager (only if enabled to avoid unnecessary calls)
Expand Down Expand Up @@ -1805,7 +1860,9 @@ def search(
# Quality score represents absolute similarity quality (0-1 range)
# It's calculated from weighted average of all search paths' similarity scores
metadata = result.get("metadata", {})
quality_score = metadata.get("_quality_score")
quality_score = result.get("_quality_score")
if quality_score is None:
quality_score = metadata.get("_quality_score")

# If quality_score is not available (e.g., from older data or non-hybrid search),
# fall back to using the ranking score
Expand All @@ -1819,7 +1876,7 @@ def search(

transformed_result = {
"memory": result.get("memory", ""),
"metadata": metadata, # Keep metadata as-is from storage (includes debug info like _quality_score)
"metadata": metadata,
"score": score,
}
# Preserve other fields if needed
Expand Down
Loading
Loading