feat: add discover() for serendipitous entry exploration
Probabilistic entry selection weighted by vitality decay. Fading entries surface more often, vibrant ones less. Touches selected entries to boost their vitality. Optional topic filter.
This commit is contained in:
@@ -1308,3 +1308,88 @@ class MnemosyneArchive:
|
||||
|
||||
self._save()
|
||||
return total_links
|
||||
# ─── Discovery ──────────────────────────────────────────────
|
||||
|
||||
def discover(
|
||||
self,
|
||||
count: int = 5,
|
||||
prefer_fading: bool = True,
|
||||
topic: Optional[str] = None,
|
||||
) -> list[dict]:
|
||||
"""Serendipitous entry discovery — surface forgotten knowledge.
|
||||
|
||||
Selects entries probabilistically, weighting toward fading (low vitality)
|
||||
entries when prefer_fading=True, or toward vibrant entries when False.
|
||||
Optionally filter by topic.
|
||||
|
||||
Touches selected entries to boost their vitality, preventing the same
|
||||
entries from being repeatedly surfaced.
|
||||
|
||||
Args:
|
||||
count: Number of entries to discover.
|
||||
prefer_fading: If True, weight toward neglected entries. If False,
|
||||
weight toward vibrant entries.
|
||||
topic: Optional topic filter — only discover entries with this tag.
|
||||
|
||||
Returns:
|
||||
List of dicts with keys: entry_id, title, content_preview, topics,
|
||||
vitality, age_days, last_accessed
|
||||
"""
|
||||
import random
|
||||
|
||||
candidates = list(self._entries.values())
|
||||
|
||||
# Filter by topic if specified
|
||||
if topic:
|
||||
topic_lower = topic.lower()
|
||||
candidates = [
|
||||
e for e in candidates
|
||||
if topic_lower in [t.lower() for t in e.topics]
|
||||
]
|
||||
|
||||
if not candidates:
|
||||
return []
|
||||
|
||||
# Compute vitality for each candidate
|
||||
scored = []
|
||||
for entry in candidates:
|
||||
v = self._compute_vitality(entry)
|
||||
scored.append((entry, v))
|
||||
|
||||
# Build selection weights
|
||||
if prefer_fading:
|
||||
# Lower vitality = higher weight. Invert and normalize.
|
||||
weights = [max(0.01, 1.0 - v) for _, v in scored]
|
||||
else:
|
||||
# Higher vitality = higher weight
|
||||
weights = [max(0.01, v) for _, v in scored]
|
||||
|
||||
# Sample without replacement
|
||||
k = min(count, len(scored))
|
||||
selected_indices = random.choices(range(len(scored)), weights=weights, k=k)
|
||||
# Deduplicate while preserving order
|
||||
seen = set()
|
||||
unique_indices = []
|
||||
for idx in selected_indices:
|
||||
if idx not in seen:
|
||||
seen.add(idx)
|
||||
unique_indices.append(idx)
|
||||
|
||||
results = []
|
||||
for idx in unique_indices:
|
||||
entry, v = scored[idx]
|
||||
# Touch to boost vitality
|
||||
self.touch(entry.id)
|
||||
created = self._parse_dt(entry.created_at)
|
||||
age_days = (datetime.now(timezone.utc) - created).days
|
||||
results.append({
|
||||
"entry_id": entry.id,
|
||||
"title": entry.title,
|
||||
"content_preview": entry.content[:200] + "..." if len(entry.content) > 200 else entry.content,
|
||||
"topics": entry.topics,
|
||||
"vitality": round(v, 4),
|
||||
"age_days": age_days,
|
||||
"last_accessed": entry.last_accessed,
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user