Skip to content

Commit db8cfe8

Browse files
Hanaclaude
andcommitted
style(brain/memory/search): Task 5 review cleanups
- combined_search(seed_id=..., domain=...) now post-filters spreading results by domain. The Hebbian graph is domain-agnostic, so the pre-scoring _candidates() filter can't apply; post-filter honours the documented "domain filter is a pre-scoring restriction" contract for the seed-only path too. - Docstring on combined_search calls out the /100 emotion normalisation as a rough heuristic (not principled — tuning expected in v1.1), and clarifies that domain is pre-filter for query/emotions but post-filter for seed_id. Tests added: - combined_search_seed_id_surfaces_connected_memories (seed-only path) - combined_search_seed_id_honours_domain_filter (the new post-filter) - combined_search_blends_query_and_emotions (multi-filter accumulation — m hitting both signals outranks one hitting either alone) 191 tests total, ruff clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 494e7b3 commit db8cfe8

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

brain/memory/search.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ def combined_search(
111111
"""Blend sub-queries with equal weight (1.0 each). Returns
112112
(memory, combined_score) ordered desc.
113113
114+
Emotion scores are divided by 100 as a rough scale heuristic so
115+
they don't dwarf cosine similarity (0..1). Not principled —
116+
tuning expected in v1.1.
117+
118+
Domain filter: applied pre-scoring for query/emotions via
119+
_candidates(); applied post-scoring for seed_id since the
120+
Hebbian graph is domain-agnostic.
121+
114122
Returns [] if no filters are specified.
115123
"""
116124
if not any((query, emotions, seed_id)):
@@ -134,6 +142,11 @@ def combined_search(
134142
for mem, act in self.spreading_search(
135143
seed_id, depth=2, decay_per_hop=0.5, limit=limit * 2
136144
):
145+
# spreading_search has no domain parameter — the graph is
146+
# domain-agnostic — so post-filter here to honour the
147+
# domain contract documented at the module level.
148+
if domain is not None and mem.domain != domain:
149+
continue
137150
scores[mem.id] = scores.get(mem.id, 0.0) + act
138151
ref_memory[mem.id] = mem
139152

tests/unit/brain/memory/test_search.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,60 @@ def test_combined_search_empty_inputs_returns_empty(search: MemorySearch) -> Non
149149
search.store.create(_mem("x"))
150150
results = search.combined_search(limit=5)
151151
assert results == []
152+
153+
154+
def test_combined_search_seed_id_surfaces_connected_memories(
155+
search: MemorySearch,
156+
) -> None:
157+
"""combined_search with only seed_id returns spreading-activation neighbours."""
158+
seed = _mem("seed memory")
159+
near = _mem("near neighbour")
160+
far = _mem("unrelated")
161+
for m in (seed, near, far):
162+
search.store.create(m)
163+
search.hebbian.strengthen(seed.id, near.id, delta=0.8)
164+
165+
results = search.combined_search(seed_id=seed.id, limit=5)
166+
returned_ids = [m.id for m, _ in results]
167+
assert near.id in returned_ids
168+
assert seed.id not in returned_ids # seed always excluded
169+
assert far.id not in returned_ids # no edge to far
170+
171+
172+
def test_combined_search_seed_id_honours_domain_filter(search: MemorySearch) -> None:
173+
"""combined_search post-filters spreading results by domain (graph is
174+
domain-agnostic, so the filter runs after activation).
175+
"""
176+
seed = _mem("seed", domain="us")
177+
us_neighbour = _mem("us neighbour", domain="us")
178+
work_neighbour = _mem("work neighbour", domain="work")
179+
for m in (seed, us_neighbour, work_neighbour):
180+
search.store.create(m)
181+
search.hebbian.strengthen(seed.id, us_neighbour.id, delta=0.7)
182+
search.hebbian.strengthen(seed.id, work_neighbour.id, delta=0.7)
183+
184+
results = search.combined_search(seed_id=seed.id, domain="us", limit=5)
185+
returned_ids = [m.id for m, _ in results]
186+
assert us_neighbour.id in returned_ids
187+
assert work_neighbour.id not in returned_ids
188+
189+
190+
def test_combined_search_blends_query_and_emotions(search: MemorySearch) -> None:
191+
"""When both query and emotions are given, scores from both sub-queries
192+
accumulate on matching memories — a memory hitting both filters outranks
193+
one hitting only one.
194+
"""
195+
# m1 matches both the query content AND the emotion filter
196+
m1 = _mem("strong love memory", emotions={"love": 9.0})
197+
# m2 matches only the query content
198+
m2 = _mem("strong love memory", emotions={"anger": 8.0})
199+
# m3 matches only the emotion filter
200+
m3 = _mem("unrelated", emotions={"love": 9.0})
201+
for m in (m1, m2, m3):
202+
search.store.create(m)
203+
204+
results = search.combined_search(query="strong love memory", emotions={"love": 9.0}, limit=5)
205+
assert len(results) >= 1
206+
# m1 accumulates both signals → top rank
207+
top_id = results[0][0].id
208+
assert top_id == m1.id

0 commit comments

Comments
 (0)