Skip to content

Commit 6b44d7e

Browse files
committed
style: formatting updates from pre-commit
1 parent da35c4e commit 6b44d7e

File tree

16 files changed

+1111
-15
lines changed

16 files changed

+1111
-15
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

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
) -> MemoryRecordResults:
@@ -669,13 +671,45 @@ async def search_long_term_memory(
669671
if distance_threshold is not None:
670672
payload["distance_threshold"] = distance_threshold
671673

674+
# Add recency config if provided
675+
if recency is not None:
676+
if recency.recency_boost is not None:
677+
payload["recency_boost"] = recency.recency_boost
678+
if recency.w_sem is not None:
679+
payload["recency_w_sem"] = recency.w_sem
680+
if recency.w_recency is not None:
681+
payload["recency_w_recency"] = recency.w_recency
682+
if recency.wf is not None:
683+
payload["recency_wf"] = recency.wf
684+
if recency.wa is not None:
685+
payload["recency_wa"] = recency.wa
686+
if recency.half_life_last_access_days is not None:
687+
payload["recency_half_life_last_access_days"] = (
688+
recency.half_life_last_access_days
689+
)
690+
if recency.half_life_created_days is not None:
691+
payload["recency_half_life_created_days"] = (
692+
recency.half_life_created_days
693+
)
694+
if recency.server_side_recency is not None:
695+
payload["server_side_recency"] = recency.server_side_recency
696+
672697
try:
673698
response = await self._client.post(
674699
"/v1/long-term-memory/search",
675700
json=payload,
676701
)
677702
response.raise_for_status()
678-
return MemoryRecordResults(**response.json())
703+
data = response.json()
704+
# Some tests may stub json() as an async function; handle awaitable
705+
try:
706+
import inspect
707+
708+
if inspect.isawaitable(data):
709+
data = await data
710+
except Exception:
711+
pass
712+
return MemoryRecordResults(**data)
679713
except httpx.HTTPStatusError as e:
680714
self._handle_http_error(e.response)
681715
raise

agent-memory-client/agent_memory_client/models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,30 @@ 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+
w_sem: float | None = Field(default=None, description="Weight for semantic score")
254+
w_recency: float | None = Field(
255+
default=None, description="Weight for recency composite"
256+
)
257+
wf: float | None = Field(default=None, description="Weight for freshness")
258+
wa: float | None = Field(default=None, description="Weight for age/novelty")
259+
half_life_last_access_days: float | None = Field(
260+
default=None, description="Half-life (days) for last_accessed decay"
261+
)
262+
half_life_created_days: float | None = Field(
263+
default=None, description="Half-life (days) for created_at decay"
264+
)
265+
server_side_recency: bool | None = Field(
266+
default=None,
267+
description="If true, attempt server-side recency ranking (Redis-only)",
268+
)
269+
270+
247271
class MemoryRecordResults(BaseModel):
248272
"""Results from memory search operations"""
249273

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_payload(self, enhanced_test_client):
305+
"""Ensure RecencyConfig fields are forwarded in the search payload."""
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+
w_sem=0.7,
317+
w_recency=0.3,
318+
wf=0.6,
319+
wa=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="q", recency=rc, limit=5
327+
)
328+
329+
# Verify payload contained recency fields
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_w_sem"] == 0.7
335+
assert body["recency_w_recency"] == 0.3
336+
assert body["recency_wf"] == 0.6
337+
assert body["recency_wa"] == 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

agent_memory_server/api.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,32 @@
3434
router = APIRouter()
3535

3636

37+
@router.post("/v1/long-term-memory/forget")
38+
async def forget_endpoint(
39+
policy: dict,
40+
namespace: str | None = None,
41+
user_id: str | None = None,
42+
session_id: str | None = None,
43+
limit: int = 1000,
44+
dry_run: bool = True,
45+
pinned_ids: list[str] | None = None,
46+
current_user: UserInfo = Depends(get_current_user),
47+
):
48+
"""Run a forgetting pass with the provided policy. Returns summary data.
49+
50+
This is an admin-style endpoint; auth is enforced by the standard dependency.
51+
"""
52+
return await long_term_memory.forget_long_term_memories(
53+
policy,
54+
namespace=namespace,
55+
user_id=user_id,
56+
session_id=session_id,
57+
limit=limit,
58+
dry_run=dry_run,
59+
pinned_ids=pinned_ids,
60+
)
61+
62+
3763
def _get_effective_token_limit(
3864
model_name: ModelNameLiteral | None,
3965
context_window_max: int | None,
@@ -525,7 +551,81 @@ async def search_long_term_memory(
525551
logger.debug(f"Long-term search kwargs: {kwargs}")
526552

527553
# Pass text and filter objects to the search function (no redis needed for vectorstore adapter)
528-
return await long_term_memory.search_long_term_memories(**kwargs)
554+
# Server-side recency rerank toggle (Redis-only path); defaults to False
555+
server_side_recency = (
556+
payload.server_side_recency
557+
if payload.server_side_recency is not None
558+
else False
559+
)
560+
if server_side_recency:
561+
recency_params = {
562+
"w_sem": payload.recency_w_sem
563+
if payload.recency_w_sem is not None
564+
else 0.8,
565+
"w_recency": payload.recency_w_recency
566+
if payload.recency_w_recency is not None
567+
else 0.2,
568+
"wf": payload.recency_wf if payload.recency_wf is not None else 0.6,
569+
"wa": payload.recency_wa if payload.recency_wa is not None else 0.4,
570+
# map half-life to smoothing constants server-side if needed
571+
"half_life_last_access_days": payload.recency_half_life_last_access_days
572+
if payload.recency_half_life_last_access_days is not None
573+
else 7.0,
574+
"half_life_created_days": payload.recency_half_life_created_days
575+
if payload.recency_half_life_created_days is not None
576+
else 30.0,
577+
}
578+
kwargs["server_side_recency"] = True
579+
kwargs["recency_params"] = recency_params
580+
return await long_term_memory.search_long_term_memories(**kwargs)
581+
582+
raw_results = await long_term_memory.search_long_term_memories(**kwargs)
583+
584+
# Recency-aware re-ranking of results (configurable)
585+
try:
586+
from datetime import UTC, datetime as _dt
587+
588+
# Decide whether to apply recency boost
589+
recency_boost = (
590+
payload.recency_boost if payload.recency_boost is not None else True
591+
)
592+
if not recency_boost or not raw_results.memories:
593+
return raw_results
594+
595+
now = _dt.now(UTC)
596+
recency_params = {
597+
"w_sem": payload.recency_w_sem
598+
if payload.recency_w_sem is not None
599+
else 0.8,
600+
"w_recency": payload.recency_w_recency
601+
if payload.recency_w_recency is not None
602+
else 0.2,
603+
"wf": payload.recency_wf if payload.recency_wf is not None else 0.6,
604+
"wa": payload.recency_wa if payload.recency_wa is not None else 0.4,
605+
"half_life_last_access_days": (
606+
payload.recency_half_life_last_access_days
607+
if payload.recency_half_life_last_access_days is not None
608+
else 7.0
609+
),
610+
"half_life_created_days": (
611+
payload.recency_half_life_created_days
612+
if payload.recency_half_life_created_days is not None
613+
else 30.0
614+
),
615+
}
616+
ranked = long_term_memory.rerank_with_recency(
617+
raw_results.memories, now=now, params=recency_params
618+
)
619+
# Update last_accessed in background with rate limiting
620+
ids = [m.id for m in ranked if m.id]
621+
if ids:
622+
background_tasks = get_background_tasks()
623+
await background_tasks.add_task(long_term_memory.update_last_accessed, ids)
624+
625+
raw_results.memories = ranked
626+
return raw_results
627+
except Exception:
628+
return raw_results
529629

530630

531631
@router.delete("/v1/long-term-memory", response_model=AckResponse)

agent_memory_server/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ class Settings(BaseSettings):
129129
default_mcp_user_id: str | None = None
130130
default_mcp_namespace: str | None = None
131131

132+
# Forgetting settings
133+
forgetting_enabled: bool = False
134+
forgetting_every_minutes: int = 60
135+
forgetting_max_age_days: float | None = None
136+
forgetting_max_inactive_days: float | None = None
137+
# Keep only top N most recent (by recency score) when budget is set
138+
forgetting_budget_keep_top_n: int | None = None
139+
132140
class Config:
133141
env_file = ".env"
134142
env_file_encoding = "utf-8"

agent_memory_server/docket_tasks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
compact_long_term_memories,
1313
delete_long_term_memories,
1414
extract_memory_structure,
15+
forget_long_term_memories,
1516
index_long_term_memories,
17+
periodic_forget_long_term_memories,
1618
promote_working_memory_to_long_term,
1719
)
1820
from agent_memory_server.summarization import summarize_session
@@ -30,6 +32,8 @@
3032
extract_discrete_memories,
3133
promote_working_memory_to_long_term,
3234
delete_long_term_memories,
35+
forget_long_term_memories,
36+
periodic_forget_long_term_memories,
3337
]
3438

3539

0 commit comments

Comments
 (0)