11import logging
22import httpx
3+ from datetime import datetime , timezone
34from typing import Any
45
56from mcp_server_qdrant .qdrant import Entry
@@ -59,7 +60,7 @@ async def rerank(self, query: str, entries: list[Entry], top_k: int | None = Non
5960 # Map reranked results back to original entries
6061 # API returns: {results: [{index: N, relevance_score: X}, ...]}
6162 reranked_entries = []
62- for item in data ["results" ][: top_k ]: # Force limit to top_k
63+ for item in data ["results" ]: # Process all results, will re-sort and trim
6364 index = item .get ("index" )
6465 score = item .get ("relevance_score" ) or item .get ("score" )
6566
@@ -71,12 +72,22 @@ async def rerank(self, query: str, entries: list[Entry], top_k: int | None = Non
7172 entry = entries [index ]
7273 if entry .metadata is None :
7374 entry .metadata = {}
74- entry .metadata ["rerank_score" ] = score
75+
76+ # Apply TTL decay for expired working memories
77+ adjusted_score = self ._apply_ttl_decay (entry , score )
78+
79+ entry .metadata ["rerank_score" ] = adjusted_score
7580 entry .metadata ["reranked" ] = True
7681
7782 reranked_entries .append (entry )
7883
79- logger .info (f"Reranked { len (entries )} → { len (reranked_entries )} results" )
84+ # Re-sort by adjusted score (TTL decay may have changed order)
85+ reranked_entries .sort (key = lambda e : e .metadata .get ("rerank_score" , 0 ), reverse = True )
86+
87+ # Trim to top_k after re-sorting
88+ reranked_entries = reranked_entries [:top_k ]
89+
90+ logger .info (f"Reranked { len (entries )} → { len (reranked_entries )} results (with TTL decay)" )
8091 return reranked_entries
8192
8293 except httpx .HTTPError as e :
@@ -86,6 +97,45 @@ async def rerank(self, query: str, entries: list[Entry], top_k: int | None = Non
8697 logger .error (f"Reranker error: { e } " )
8798 raise RuntimeError (f"Reranker failed: { e } " )
8899
100+ def _apply_ttl_decay (self , entry : Entry , score : float ) -> float :
101+ """
102+ Apply decay multiplier to scores for expired or near-expired working memory.
103+
104+ Long-term memories and unclassified entries pass through unchanged.
105+ Working memories get decayed based on TTL expiration status.
106+ """
107+ metadata = entry .metadata or {}
108+ memory_type = metadata .get ("memory_type" )
109+ ttl_days = metadata .get ("ttl_days" )
110+ timestamp = metadata .get ("timestamp" )
111+
112+ # Only decay working memory with valid TTL info
113+ if memory_type != "working" or not ttl_days or not timestamp :
114+ return score
115+
116+ try :
117+ # Parse timestamp (handle ISO format with Z or +00:00)
118+ ts = timestamp .replace ("Z" , "+00:00" ) if isinstance (timestamp , str ) else str (timestamp )
119+ created = datetime .fromisoformat (ts )
120+ age_days = (datetime .now (timezone .utc ) - created ).days
121+
122+ if age_days > ttl_days :
123+ # Expired - heavy decay
124+ decay = 0.3
125+ logger .debug (f"TTL expired ({ age_days } d > { ttl_days } d): score { score :.3f} → { score * decay :.3f} " )
126+ return score * decay
127+ elif age_days > ttl_days * 0.8 :
128+ # Near expiry (>80% of TTL) - mild decay
129+ decay = 0.7
130+ logger .debug (f"TTL near expiry ({ age_days } d / { ttl_days } d): score { score :.3f} → { score * decay :.3f} " )
131+ return score * decay
132+
133+ return score
134+
135+ except (ValueError , TypeError ) as e :
136+ logger .warning (f"Failed to parse TTL metadata: { e } " )
137+ return score
138+
89139 async def close (self ):
90140 """Close HTTP client."""
91141 await self .client .aclose ()
0 commit comments