Skip to content

Feat: Forgetting mechanism and recency boost #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Aug 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6b44d7e
style: formatting updates from pre-commit
abrookins Aug 8, 2025
24d321d
feat(redis): DB-level recency ranking via RedisVL VectorQuery and ada…
abrookins Aug 8, 2025
576d6c5
feat(redis): use RangeQuery when distance_threshold is provided; Vect…
abrookins Aug 8, 2025
a6d1961
feat(redis): use RedisVL paging with contextlib.suppress; fix loop va…
abrookins Aug 8, 2025
e447288
feat(redis): AggregateQuery with KNN + APPLY + SORTBY for server_side…
abrookins Aug 8, 2025
eac6268
refactor(redis): integrate RecencyAggregationQuery; fix AggregationQu…
abrookins Aug 8, 2025
5e1f7c5
test(redis): add RecencyAggregationQuery and server_side_recency adap…
abrookins Aug 8, 2025
a633044
fix(redis): coerce list fields in Redis aggregate path; add RecencyAg…
abrookins Aug 8, 2025
4c6b1c1
fix: add _parse_list_field to base adapter; tests now pass including …
abrookins Aug 8, 2025
a8fb65c
fix: address PR feedback - improve type checking, extract complex log…
abrookins Aug 11, 2025
453c7b5
feat: expand short recency parameter names to descriptive ones
abrookins Aug 12, 2025
9db522b
feat: complete vectorstore adapter parameter name updates
abrookins Aug 12, 2025
5f849dc
fix: address PR review feedback
abrookins Aug 12, 2025
c8bdeb9
feat: complete client library parameter naming updates
abrookins Aug 12, 2025
5455792
docs: add descriptive parameter examples
abrookins Aug 12, 2025
6c88daf
More variable name fixes
abrookins Aug 12, 2025
83d5abb
refactor: improve code quality and remove duplication in vectorstore …
abrookins Aug 12, 2025
a1a5a4d
refactor: move imports to top of vectorstore_adapter.py module
abrookins Aug 12, 2025
58ee06b
refactor: resolve PR review comments on recency and MCP changes
abrookins Aug 13, 2025
b324f48
merge: resolve conflicts with main branch
abrookins Aug 13, 2025
c1f0729
fix: update test imports after moving recency functions
abrookins Aug 13, 2025
6785bb1
Merge branch 'main' into feature/forgetting-recency
abrookins Aug 13, 2025
aa8c3ea
Remove task memory file
abrookins Aug 13, 2025
be0abca
fix: add robust error handling for LLM response parsing
abrookins Aug 13, 2025
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -231,5 +231,5 @@ libs/redis/docs/.Trash*
.cursor

*.pyc
ai
.ai
.claude
40 changes: 33 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,68 @@ This project uses Redis 8, which is the redis:8 docker image.
Do not use Redis Stack or other earlier versions of Redis.

## Frequently Used Commands
Get started in a new environment by installing `uv`:
```bash
pip install uv
```

### Project Setup
Get started in a new environment by installing `uv`:
```bash
# Development workflow
pip install uv # Install uv (once)
uv venv # Create a virtualenv (once)
source .venv/bin/activate # Activate the virtualenv (start of terminal session)
uv install --all-extras # Install dependencies
uv sync --all-extras # Sync latest dependencies
```

### Activate the virtual environment
You MUST always activate the virtualenv before running commands:

```bash
source .venv/bin/activate
```

### Running Tests
Always run tests before committing. You MUST have 100% of the tests in the
code basepassing to commit.

Run all tests like this, including tests that require API keys in the
environment:
```bash
uv run pytest --run-api-tests
```

### Linting

```bash
uv run ruff check # Run linting
uv run ruff format # Format code
uv run pytest --run-api-tests # Run all tests

### Managing Dependencies
uv add <dependency> # Add a dependency to pyproject.toml and update lock file
uv remove <dependency> # Remove a dependency from pyproject.toml and update lock file

### Running Servers
# Server commands
uv run agent-memory api # Start REST API server (default port 8000)
uv run agent-memory mcp # Start MCP server (stdio mode)
uv run agent-memory mcp --mode sse --port 9000 # Start MCP server (SSE mode)

### Database Operations
# Database/Redis operations
uv run agent-memory rebuild-index # Rebuild Redis search index
uv run agent-memory migrate-memories # Run memory migrations

### Background Tasks
# Background task management
uv run agent-memory task-worker # Start background task worker
# Schedule a specific task
uv run agent-memory schedule-task "agent_memory_server.long_term_memory.compact_long_term_memories"

### Running All Containers
# Docker development
docker-compose up # Start full stack (API, MCP, Redis)
docker-compose up redis # Start only Redis Stack
docker-compose down # Stop all services
```

### Committing Changes
IMPORTANT: This project uses `pre-commit`. You should run `pre-commit`
before committing:
```bash
Expand Down
359 changes: 0 additions & 359 deletions TASK_MEMORY.md

This file was deleted.

25 changes: 25 additions & 0 deletions agent-memory-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,31 @@ results = await client.search_long_term_memory(
)
```

## Recency-Aware Search

```python
from agent_memory_client.models import RecencyConfig

# Search with recency-aware ranking
recency_config = RecencyConfig(
recency_boost=True,
semantic_weight=0.8, # Weight for semantic similarity
recency_weight=0.2, # Weight for recency score
freshness_weight=0.6, # Weight for freshness component
novelty_weight=0.4, # Weight for novelty/age component
half_life_last_access_days=7, # Last accessed decay half-life
half_life_created_days=30, # Creation date decay half-life
server_side_recency=True # Use server-side optimization
)

results = await client.search_long_term_memory(
text="project updates",
recency=recency_config,
limit=10
)

```

## Error Handling

```python
Expand Down
36 changes: 35 additions & 1 deletion agent-memory-client/agent_memory_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
MemoryRecordResults,
MemoryTypeEnum,
ModelNameLiteral,
RecencyConfig,
SessionListResponse,
WorkingMemory,
WorkingMemoryResponse,
Expand Down Expand Up @@ -572,6 +573,7 @@ async def search_long_term_memory(
user_id: UserId | dict[str, Any] | None = None,
distance_threshold: float | None = None,
memory_type: MemoryType | dict[str, Any] | None = None,
recency: RecencyConfig | None = None,
limit: int = 10,
offset: int = 0,
optimize_query: bool = True,
Expand Down Expand Up @@ -671,6 +673,29 @@ async def search_long_term_memory(
if distance_threshold is not None:
payload["distance_threshold"] = distance_threshold

# Add recency config if provided
if recency is not None:
if recency.recency_boost is not None:
payload["recency_boost"] = recency.recency_boost
if recency.semantic_weight is not None:
payload["recency_semantic_weight"] = recency.semantic_weight
if recency.recency_weight is not None:
payload["recency_recency_weight"] = recency.recency_weight
if recency.freshness_weight is not None:
payload["recency_freshness_weight"] = recency.freshness_weight
if recency.novelty_weight is not None:
payload["recency_novelty_weight"] = recency.novelty_weight
if recency.half_life_last_access_days is not None:
payload["recency_half_life_last_access_days"] = (
recency.half_life_last_access_days
)
if recency.half_life_created_days is not None:
payload["recency_half_life_created_days"] = (
recency.half_life_created_days
)
if recency.server_side_recency is not None:
payload["server_side_recency"] = recency.server_side_recency

# Add optimize_query as query parameter
params = {"optimize_query": str(optimize_query).lower()}

Expand All @@ -681,7 +706,16 @@ async def search_long_term_memory(
params=params,
)
response.raise_for_status()
return MemoryRecordResults(**response.json())
data = response.json()
# Some tests may stub json() as an async function; handle awaitable
try:
import inspect

if inspect.isawaitable(data):
data = await data
except Exception:
pass
return MemoryRecordResults(**data)
except httpx.HTTPStatusError as e:
self._handle_http_error(e.response)
raise
Expand Down
31 changes: 31 additions & 0 deletions agent-memory-client/agent_memory_client/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,37 @@ class MemoryRecordResult(MemoryRecord):
dist: float


class RecencyConfig(BaseModel):
"""Client-side configuration for recency-aware ranking options."""

recency_boost: bool | None = Field(
default=None, description="Enable recency-aware re-ranking"
)
semantic_weight: float | None = Field(
default=None, description="Weight for semantic similarity"
)
recency_weight: float | None = Field(
default=None, description="Weight for recency score"
)
freshness_weight: float | None = Field(
default=None, description="Weight for freshness component"
)
novelty_weight: float | None = Field(
default=None, description="Weight for novelty/age component"
)

half_life_last_access_days: float | None = Field(
default=None, description="Half-life (days) for last_accessed decay"
)
half_life_created_days: float | None = Field(
default=None, description="Half-life (days) for created_at decay"
)
server_side_recency: bool | None = Field(
default=None,
description="If true, attempt server-side recency ranking (Redis-only)",
)


class MemoryRecordResults(BaseModel):
"""Results from memory search operations"""

Expand Down
42 changes: 42 additions & 0 deletions agent-memory-client/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
MemoryRecordResult,
MemoryRecordResults,
MemoryTypeEnum,
RecencyConfig,
WorkingMemoryResponse,
)

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


class TestRecencyConfig:
@pytest.mark.asyncio
async def test_recency_config_descriptive_parameters(self, enhanced_test_client):
"""Test that RecencyConfig descriptive parameters are properly sent to API."""
with patch.object(enhanced_test_client._client, "post") as mock_post:
mock_response = AsyncMock()
mock_response.raise_for_status.return_value = None
mock_response.json.return_value = MemoryRecordResults(
total=0, memories=[], next_offset=None
).model_dump()
mock_post.return_value = mock_response

rc = RecencyConfig(
recency_boost=True,
semantic_weight=0.8,
recency_weight=0.2,
freshness_weight=0.6,
novelty_weight=0.4,
half_life_last_access_days=7,
half_life_created_days=30,
server_side_recency=True,
)

await enhanced_test_client.search_long_term_memory(
text="search query", recency=rc, limit=5
)

# Verify payload contains descriptive parameter names
args, kwargs = mock_post.call_args
assert args[0] == "/v1/long-term-memory/search"
body = kwargs["json"]
assert body["recency_boost"] is True
assert body["recency_semantic_weight"] == 0.8
assert body["recency_recency_weight"] == 0.2
assert body["recency_freshness_weight"] == 0.6
assert body["recency_novelty_weight"] == 0.4
assert body["recency_half_life_last_access_days"] == 7
assert body["recency_half_life_created_days"] == 30
assert body["server_side_recency"] is True


class TestClientSideValidation:
"""Tests for client-side validation methods."""

Expand Down
Loading