Skip to content

Commit 9b1c318

Browse files
authored
Merge pull request #45 from redis/feature/forgetting-recency
Feat: Forgetting mechanism and recency boost
2 parents 9dcc4c1 + be0abca commit 9b1c318

23 files changed

+1581
-435
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,5 +231,5 @@ libs/redis/docs/.Trash*
231231
.cursor
232232

233233
*.pyc
234-
ai
234+
.ai
235235
.claude

CLAUDE.md

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,68 @@ This project uses Redis 8, which is the redis:8 docker image.
55
Do not use Redis Stack or other earlier versions of Redis.
66

77
## Frequently Used Commands
8-
Get started in a new environment by installing `uv`:
9-
```bash
10-
pip install uv
11-
```
128

9+
### Project Setup
10+
Get started in a new environment by installing `uv`:
1311
```bash
14-
# Development workflow
12+
pip install uv # Install uv (once)
1513
uv venv # Create a virtualenv (once)
16-
source .venv/bin/activate # Activate the virtualenv (start of terminal session)
1714
uv install --all-extras # Install dependencies
1815
uv sync --all-extras # Sync latest dependencies
16+
```
17+
18+
### Activate the virtual environment
19+
You MUST always activate the virtualenv before running commands:
20+
21+
```bash
22+
source .venv/bin/activate
23+
```
24+
25+
### Running Tests
26+
Always run tests before committing. You MUST have 100% of the tests in the
27+
code basepassing to commit.
28+
29+
Run all tests like this, including tests that require API keys in the
30+
environment:
31+
```bash
32+
uv run pytest --run-api-tests
33+
```
34+
35+
### Linting
36+
37+
```bash
1938
uv run ruff check # Run linting
2039
uv run ruff format # Format code
21-
uv run pytest --run-api-tests # Run all tests
40+
41+
### Managing Dependencies
2242
uv add <dependency> # Add a dependency to pyproject.toml and update lock file
2343
uv remove <dependency> # Remove a dependency from pyproject.toml and update lock file
2444

45+
### Running Servers
2546
# Server commands
2647
uv run agent-memory api # Start REST API server (default port 8000)
2748
uv run agent-memory mcp # Start MCP server (stdio mode)
2849
uv run agent-memory mcp --mode sse --port 9000 # Start MCP server (SSE mode)
2950

51+
### Database Operations
3052
# Database/Redis operations
3153
uv run agent-memory rebuild-index # Rebuild Redis search index
3254
uv run agent-memory migrate-memories # Run memory migrations
3355

56+
### Background Tasks
3457
# Background task management
3558
uv run agent-memory task-worker # Start background task worker
59+
# Schedule a specific task
3660
uv run agent-memory schedule-task "agent_memory_server.long_term_memory.compact_long_term_memories"
3761

62+
### Running All Containers
3863
# Docker development
3964
docker-compose up # Start full stack (API, MCP, Redis)
4065
docker-compose up redis # Start only Redis Stack
4166
docker-compose down # Stop all services
4267
```
4368

69+
### Committing Changes
4470
IMPORTANT: This project uses `pre-commit`. You should run `pre-commit`
4571
before committing:
4672
```bash

TASK_MEMORY.md

Lines changed: 0 additions & 359 deletions
This file was deleted.

agent-memory-client/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,31 @@ results = await client.search_long_term_memory(
240240
)
241241
```
242242

243+
## Recency-Aware Search
244+
245+
```python
246+
from agent_memory_client.models import RecencyConfig
247+
248+
# Search with recency-aware ranking
249+
recency_config = RecencyConfig(
250+
recency_boost=True,
251+
semantic_weight=0.8, # Weight for semantic similarity
252+
recency_weight=0.2, # Weight for recency score
253+
freshness_weight=0.6, # Weight for freshness component
254+
novelty_weight=0.4, # Weight for novelty/age component
255+
half_life_last_access_days=7, # Last accessed decay half-life
256+
half_life_created_days=30, # Creation date decay half-life
257+
server_side_recency=True # Use server-side optimization
258+
)
259+
260+
results = await client.search_long_term_memory(
261+
text="project updates",
262+
recency=recency_config,
263+
limit=10
264+
)
265+
266+
```
267+
243268
## Error Handling
244269

245270
```python

agent-memory-client/agent_memory_client/client.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
MemoryRecordResults,
3737
MemoryTypeEnum,
3838
ModelNameLiteral,
39+
RecencyConfig,
3940
SessionListResponse,
4041
WorkingMemory,
4142
WorkingMemoryResponse,
@@ -572,6 +573,7 @@ async def search_long_term_memory(
572573
user_id: UserId | dict[str, Any] | None = None,
573574
distance_threshold: float | None = None,
574575
memory_type: MemoryType | dict[str, Any] | None = None,
576+
recency: RecencyConfig | None = None,
575577
limit: int = 10,
576578
offset: int = 0,
577579
optimize_query: bool = True,
@@ -671,6 +673,29 @@ async def search_long_term_memory(
671673
if distance_threshold is not None:
672674
payload["distance_threshold"] = distance_threshold
673675

676+
# Add recency config if provided
677+
if recency is not None:
678+
if recency.recency_boost is not None:
679+
payload["recency_boost"] = recency.recency_boost
680+
if recency.semantic_weight is not None:
681+
payload["recency_semantic_weight"] = recency.semantic_weight
682+
if recency.recency_weight is not None:
683+
payload["recency_recency_weight"] = recency.recency_weight
684+
if recency.freshness_weight is not None:
685+
payload["recency_freshness_weight"] = recency.freshness_weight
686+
if recency.novelty_weight is not None:
687+
payload["recency_novelty_weight"] = recency.novelty_weight
688+
if recency.half_life_last_access_days is not None:
689+
payload["recency_half_life_last_access_days"] = (
690+
recency.half_life_last_access_days
691+
)
692+
if recency.half_life_created_days is not None:
693+
payload["recency_half_life_created_days"] = (
694+
recency.half_life_created_days
695+
)
696+
if recency.server_side_recency is not None:
697+
payload["server_side_recency"] = recency.server_side_recency
698+
674699
# Add optimize_query as query parameter
675700
params = {"optimize_query": str(optimize_query).lower()}
676701

@@ -681,7 +706,16 @@ async def search_long_term_memory(
681706
params=params,
682707
)
683708
response.raise_for_status()
684-
return MemoryRecordResults(**response.json())
709+
data = response.json()
710+
# Some tests may stub json() as an async function; handle awaitable
711+
try:
712+
import inspect
713+
714+
if inspect.isawaitable(data):
715+
data = await data
716+
except Exception:
717+
pass
718+
return MemoryRecordResults(**data)
685719
except httpx.HTTPStatusError as e:
686720
self._handle_http_error(e.response)
687721
raise

agent-memory-client/agent_memory_client/models.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,37 @@ class MemoryRecordResult(MemoryRecord):
244244
dist: float
245245

246246

247+
class RecencyConfig(BaseModel):
248+
"""Client-side configuration for recency-aware ranking options."""
249+
250+
recency_boost: bool | None = Field(
251+
default=None, description="Enable recency-aware re-ranking"
252+
)
253+
semantic_weight: float | None = Field(
254+
default=None, description="Weight for semantic similarity"
255+
)
256+
recency_weight: float | None = Field(
257+
default=None, description="Weight for recency score"
258+
)
259+
freshness_weight: float | None = Field(
260+
default=None, description="Weight for freshness component"
261+
)
262+
novelty_weight: float | None = Field(
263+
default=None, description="Weight for novelty/age component"
264+
)
265+
266+
half_life_last_access_days: float | None = Field(
267+
default=None, description="Half-life (days) for last_accessed decay"
268+
)
269+
half_life_created_days: float | None = Field(
270+
default=None, description="Half-life (days) for created_at decay"
271+
)
272+
server_side_recency: bool | None = Field(
273+
default=None,
274+
description="If true, attempt server-side recency ranking (Redis-only)",
275+
)
276+
277+
247278
class MemoryRecordResults(BaseModel):
248279
"""Results from memory search operations"""
249280

agent-memory-client/tests/test_client.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
MemoryRecordResult,
2121
MemoryRecordResults,
2222
MemoryTypeEnum,
23+
RecencyConfig,
2324
WorkingMemoryResponse,
2425
)
2526

@@ -298,6 +299,47 @@ async def test_search_all_long_term_memories(self, enhanced_test_client):
298299
assert mock_search.call_count == 3
299300

300301

302+
class TestRecencyConfig:
303+
@pytest.mark.asyncio
304+
async def test_recency_config_descriptive_parameters(self, enhanced_test_client):
305+
"""Test that RecencyConfig descriptive parameters are properly sent to API."""
306+
with patch.object(enhanced_test_client._client, "post") as mock_post:
307+
mock_response = AsyncMock()
308+
mock_response.raise_for_status.return_value = None
309+
mock_response.json.return_value = MemoryRecordResults(
310+
total=0, memories=[], next_offset=None
311+
).model_dump()
312+
mock_post.return_value = mock_response
313+
314+
rc = RecencyConfig(
315+
recency_boost=True,
316+
semantic_weight=0.8,
317+
recency_weight=0.2,
318+
freshness_weight=0.6,
319+
novelty_weight=0.4,
320+
half_life_last_access_days=7,
321+
half_life_created_days=30,
322+
server_side_recency=True,
323+
)
324+
325+
await enhanced_test_client.search_long_term_memory(
326+
text="search query", recency=rc, limit=5
327+
)
328+
329+
# Verify payload contains descriptive parameter names
330+
args, kwargs = mock_post.call_args
331+
assert args[0] == "/v1/long-term-memory/search"
332+
body = kwargs["json"]
333+
assert body["recency_boost"] is True
334+
assert body["recency_semantic_weight"] == 0.8
335+
assert body["recency_recency_weight"] == 0.2
336+
assert body["recency_freshness_weight"] == 0.6
337+
assert body["recency_novelty_weight"] == 0.4
338+
assert body["recency_half_life_last_access_days"] == 7
339+
assert body["recency_half_life_created_days"] == 30
340+
assert body["server_side_recency"] is True
341+
342+
301343
class TestClientSideValidation:
302344
"""Tests for client-side validation methods."""
303345

0 commit comments

Comments
 (0)