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
3 changes: 2 additions & 1 deletion automem/api/recall.py
Original file line number Diff line number Diff line change
Expand Up @@ -1968,7 +1968,8 @@ def _run_single_query(
per_query_limit,
min(per_query_limit * RECALL_VECTOR_OVERFETCH, RECALL_VECTOR_FETCH_CAP),
)
if tag_filters and (query_str or embedding_param):
priority_tags = (context_profile or {}).get("priority_tags") or set()
if (tag_filters or priority_tags) and (query_str or embedding_param):
vector_fetch_limit = max(
per_query_limit,
min(max(vector_fetch_limit, recall_max_limit), RECALL_VECTOR_FETCH_CAP),
Expand Down
63 changes: 63 additions & 0 deletions lab/test_sets/queries_context_tags_20260704.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"metadata": {
"created": "2026-07-04T00:00:00Z",
"source": "issue-201-context-tags-probes",
"query_count": 5,
"notes": "Probe rows exercise per-query context_tags in the recall lab harness."
},
"queries": [
{
"query": "What issues do React SPA editors face with WP Fusion integrations regarding data loss?",
"expected_ids": [
"d4fc88f1-52d1-489b-b39c-72982e406910"
],
"category": "context_tags",
"context_tags": [
"wp-fusion"
]
},
{
"query": "What decisions were made regarding the AutoApp chat UI on April 21, 2026?",
"expected_ids": [
"d7f35a92-50b6-46cb-9f00-e5f0d4f94819"
],
"category": "context_tags",
"context_tags": [
"autoapp"
]
},
{
"query": "What upgrades were made to the AutoHub voice latency settings?",
"expected_ids": [
"de37ff1a-db7f-4310-98af-22c03b2622b8"
],
"category": "context_tags",
"context_tags": [
"autohub",
"voice"
]
},
{
"query": "What issues were identified with mcp-wp taxonomy tools and their interaction with slugs?",
"expected_ids": [
"cd5115b8-e003-46c0-9178-b6b3c0d46117"
],
"category": "context_tags",
"context_tags": [
"mcp-wp",
"wordpress"
]
},
{
"query": "What design constraints did Jack mention for the AutoMem server in June 2026?",
"expected_ids": [
"24997f3a-3bb4-406c-bed3-4172dfb1caa2"
],
"category": "context_tags",
"context_tags": [
"automem",
"mcp-automem"
]
}
]
}
3 changes: 3 additions & 0 deletions scripts/lab/lab_corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def recall(
expand_relations: bool = False,
current_only: bool = True,
recency_bias: Optional[str] = None,
context_tags: Optional[List[str]] = None,
http_get=requests.get,
) -> Dict[str, Any]:
"""GET /recall with explicit recall parameters; returns parsed JSON."""
Expand All @@ -29,6 +30,8 @@ def recall(
params["current_only"] = "true" if current_only else "false"
if recency_bias is not None:
params["recency_bias"] = recency_bias
if context_tags:
params["context_tags"] = context_tags
resp = http_get(f"{api_url}/recall", params=params, headers=headers, timeout=30)
resp.raise_for_status()
return resp.json()
Expand Down
4 changes: 3 additions & 1 deletion scripts/lab/run_recall_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,9 @@ def run_single_query(
expected_ids = query_data.get("expected_ids", [])
category = query_data.get("category", "unknown")
distractor_ids = distractor_ids or set()
recall_params = recall_params or {}
recall_params = dict(recall_params or {})
if query_data.get("context_tags"):
recall_params["context_tags"] = query_data["context_tags"]

start = time.perf_counter()
try:
Expand Down
2 changes: 2 additions & 0 deletions tests/lab/test_lab_corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def fake_get(url, params=None, headers=None, timeout=None):
expand_relations=True,
current_only=False,
recency_bias="auto",
context_tags=["automem", "mcp-automem"],
http_get=fake_get,
)
assert captured["url"].endswith("/recall")
Expand All @@ -38,6 +39,7 @@ def fake_get(url, params=None, headers=None, timeout=None):
assert captured["params"]["expand_relations"] == "true"
assert captured["params"]["current_only"] == "false"
assert captured["params"]["recency_bias"] == "auto"
assert captured["params"]["context_tags"] == ["automem", "mcp-automem"]


def test_recall_omits_recency_bias_when_none():
Expand Down
32 changes: 32 additions & 0 deletions tests/lab/test_run_recall_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json

import pytest
import run_recall_test as rr


Expand Down Expand Up @@ -74,3 +75,34 @@ def test_save_results_keeps_query_distractor_rate(tmp_path):
data = json.loads(output_path.read_text())

assert data["queries"][0]["distractor_rate_10"] == 0.5


def test_run_single_query_forwards_row_context_tags(monkeypatch):
captured = {}

def fake_recall(api_url, headers, query, **params):
captured["api_url"] = api_url
captured["headers"] = headers
captured["query"] = query
captured["params"] = params
return {"results": [{"id": "target"}]}

monkeypatch.setattr(rr, "recall", fake_recall)
monkeypatch.setattr(rr, "get_headers", lambda: {"Authorization": "Bearer test"})

result = rr.run_single_query(
{
"query": "What did the AutoMem server prefer?",
"expected_ids": ["target"],
"category": "context_tags",
"context_tags": ["automem", "mcp-automem"],
},
"http://api",
recall_params={"limit": 5, "context_tags": ["fallback"]},
)

assert captured["api_url"] == "http://api"
assert captured["query"] == "What did the AutoMem server prefer?"
assert captured["params"]["limit"] == 5
assert captured["params"]["context_tags"] == ["automem", "mcp-automem"]
assert result.recall_5 == pytest.approx(1.0)
69 changes: 69 additions & 0 deletions tests/test_recall_overfetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import app
import automem.api.recall as recall_module
import automem.search.runtime_recall_helpers as recall_helpers
from automem.utils.scoring import _compute_metadata_score
from tests.support.fake_graph import FakeGraph


Expand Down Expand Up @@ -138,6 +139,74 @@ def test_tag_scoped_vector_fetch_respects_cap(overfetch_env, monkeypatch):
assert overfetch_env.last_limit == 20


def test_context_tags_widen_vector_fetch_before_scoring(monkeypatch) -> None:
monkeypatch.setattr(recall_module, "RECALL_VECTOR_OVERFETCH", 4)
monkeypatch.setattr(recall_module, "RECALL_VECTOR_FETCH_CAP", 200)

requested_limits = []

def _result(memory_id, score, *, tags=None):
return {
"id": memory_id,
"score": score,
"match_score": score,
"match_type": "vector",
"source": "qdrant",
"memory": {
"id": memory_id,
"content": f"unrelated content for {memory_id}",
"tags": tags or [],
"importance": 0.0,
"confidence": 0.0,
"type": "Context",
"enriched": True,
"timestamp": "2026-05-18T00:00:00+00:00",
},
"relations": [],
}

vector_results = [_result(f"decoy-{i}", 0.95 - i * 0.001) for i in range(24)] + [
_result("context-target", 0.01, tags=["target-tag"]),
]

def _vector_search(_qdrant, _graph, _query, _embedding, limit, seen_ids, *_args):
requested_limits.append(limit)
matches = [dict(result) for result in vector_results[:limit]]
for result in matches:
seen_ids.add(result["id"])
return matches

with app.app.test_request_context(
"/recall?query=quarterly%20metrics&context_tags=target-tag&limit=5&current_only=false"
):
response = recall_module.handle_recall(
get_memory_graph=lambda: None,
get_qdrant_client=lambda: object(),
normalize_tag_list=lambda value: value if isinstance(value, list) else [],
normalize_timestamp=lambda value: value,
parse_time_expression=lambda _value: (None, None),
extract_keywords=lambda _query: ["quarterly", "metrics"],
compute_metadata_score=_compute_metadata_score,
result_passes_filters=lambda *_args, **_kwargs: True,
graph_keyword_search=lambda *_args, **_kwargs: [],
vector_search=_vector_search,
vector_filter_only_tag_search=lambda *_args, **_kwargs: [],
metadata_keyword_search=lambda *_args, **_kwargs: [],
recall_max_limit=50,
logger=SimpleNamespace(
debug=lambda *_args, **_kwargs: None,
info=lambda *_args, **_kwargs: None,
exception=lambda *_args, **_kwargs: None,
),
)

data = response.get_json()
ids = [result["id"] for result in data["results"]]
assert requested_limits == [50]
assert "context-target" in ids
assert len(ids) <= 5


def test_vector_overfetch_hydrates_relations_after_trim(overfetch_env, monkeypatch):
relation_calls = []

Expand Down
Loading