Skip to content

Commit bfcc91a

Browse files
committed
Clarify hybrid search score handling
1 parent 92aa484 commit bfcc91a

File tree

8 files changed

+72
-1
lines changed

8 files changed

+72
-1
lines changed

agent_memory_server/api.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
ModelNameLiteral,
2828
RunSummaryViewPartitionRequest,
2929
RunSummaryViewRequest,
30+
SearchModeEnum,
3031
SearchRequest,
3132
SessionListResponse,
3233
SummaryView,
@@ -657,6 +658,15 @@ async def search_long_term_memory(
657658
if not settings.long_term_memory:
658659
raise HTTPException(status_code=400, detail="Long-term memory is disabled")
659660

661+
if (
662+
payload.distance_threshold is not None
663+
and payload.search_mode != SearchModeEnum.SEMANTIC
664+
):
665+
raise HTTPException(
666+
status_code=400,
667+
detail="distance_threshold is only supported for semantic search mode",
668+
)
669+
660670
# Extract filter objects from the payload
661671
filters = payload.get_filters()
662672

agent_memory_server/cli.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,12 @@ def search(
613613

614614
configure_logging()
615615

616+
if distance_threshold is not None and search_mode != "semantic":
617+
raise click.BadParameter(
618+
"distance_threshold is only supported for semantic search mode",
619+
param_hint="--distance-threshold",
620+
)
621+
616622
async def run_search():
617623
# Build filter objects
618624
namespace_filter = Namespace(eq=namespace) if namespace else None

agent_memory_server/long_term_memory.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,11 @@ async def search_long_term_memories(
11231123
Returns:
11241124
MemoryRecordResults containing matching memories
11251125
"""
1126+
if distance_threshold is not None and search_mode != SearchModeEnum.SEMANTIC:
1127+
raise ValueError(
1128+
"distance_threshold is only supported for semantic search mode"
1129+
)
1130+
11261131
# If no query text is provided, perform a filter-only listing (no semantic search).
11271132
# This enables patterns like: "return all memories for this user/namespace".
11281133
if not (text or "").strip():

agent_memory_server/mcp.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,11 @@ async def search_long_term_memory(
585585
if namespace is None and settings.default_mcp_namespace:
586586
namespace = Namespace(eq=settings.default_mcp_namespace)
587587

588+
if distance_threshold is not None and search_mode != SearchModeEnum.SEMANTIC:
589+
raise ValueError(
590+
"distance_threshold is only supported for semantic search mode"
591+
)
592+
588593
try:
589594
payload = SearchRequest(
590595
text=text,

agent_memory_server/memory_vector_db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ def _normalize_rank_scores(self, raw_scores: list[float]) -> list[float]:
499499

500500
max_score = max(raw_scores)
501501
if max_score <= 0:
502-
return [1.0 for _ in raw_scores]
502+
return [0.0 for _ in raw_scores]
503503

504504
return [min(max(score / max_score, 0.0), 1.0) for score in raw_scores]
505505

tests/test_api.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,27 @@ async def test_search_with_optimize_query_explicit_true(self, mock_search, clien
902902
assert data["memories"][0]["id"] == "1"
903903
assert data["memories"][0]["text"] == "Optimized result"
904904

905+
@patch("agent_memory_server.api.long_term_memory.search_long_term_memories")
906+
@pytest.mark.asyncio
907+
async def test_search_rejects_distance_threshold_for_keyword_mode(
908+
self, mock_search, client
909+
):
910+
"""distance_threshold only applies to semantic search."""
911+
payload = {
912+
"text": "preference",
913+
"search_mode": "keyword",
914+
"distance_threshold": 0.5,
915+
}
916+
917+
response = await client.post("/v1/long-term-memory/search", json=payload)
918+
919+
assert response.status_code == 400
920+
assert (
921+
response.json()["detail"]
922+
== "distance_threshold is only supported for semantic search mode"
923+
)
924+
mock_search.assert_not_called()
925+
905926

906927
@pytest.mark.requires_api_keys
907928
class TestMemoryPromptEndpoint:

tests/test_cli.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,22 @@ def test_search_with_distance_threshold(self, mock_search):
805805
call_kwargs = mock_search.call_args[1]
806806
assert call_kwargs["distance_threshold"] == 0.5
807807

808+
@patch("agent_memory_server.long_term_memory.search_long_term_memories")
809+
def test_search_rejects_distance_threshold_for_keyword_mode(self, mock_search):
810+
"""distance_threshold should fail fast outside semantic mode."""
811+
runner = CliRunner()
812+
result = runner.invoke(
813+
search,
814+
["test query", "--search-mode", "keyword", "--distance-threshold", "0.5"],
815+
)
816+
817+
assert result.exit_code != 0
818+
assert (
819+
"distance_threshold is only supported for semantic search mode"
820+
in result.output
821+
)
822+
mock_search.assert_not_called()
823+
808824
@patch("agent_memory_server.long_term_memory.search_long_term_memories")
809825
def test_search_json_output(self, mock_search):
810826
"""Test search with JSON output format."""

tests/test_memory_vector_db.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,14 @@ def test_data_to_memory_result_decodes_legacy_pipe_delimited_tags(self):
488488
assert result.topics == ["cooking", "italian"]
489489
assert result.entities == ["pasta", "rome"]
490490

491+
def test_normalize_rank_scores_zero_scores_remain_zero(self):
492+
"""Zero-score batches should not be normalized into perfect matches."""
493+
mock_index = MagicMock()
494+
mock_embeddings = MockEmbeddings()
495+
db = RedisVLMemoryVectorDatabase(mock_index, mock_embeddings)
496+
497+
assert db._normalize_rank_scores([0.0, 0.0]) == [0.0, 0.0]
498+
491499
@pytest.mark.asyncio
492500
async def test_list_memories_does_not_overwrite_dist_with_score(self):
493501
"""Filter-only listings should keep neutral distance semantics."""

0 commit comments

Comments
 (0)