| """ |
| Temporal Knowledge Graph β directed graph capturing sequential events. |
| |
| Tracks user traits, behavioral trends, and chronologies. |
| Edge weights decay with time and strengthen with interaction frequency. |
| """ |
| from datetime import datetime, timezone |
| from typing import Dict, List, Optional, Tuple |
| from .models import MemoryItem, MemoryTier, TemporalEdge, RetrievalResult |
| from .retriever import retrieve |
| import math |
|
|
|
|
| _DECAY_HALF_LIFE_DAYS = 30.0 |
|
|
|
|
| def _time_decay(last_updated: datetime) -> float: |
| """Exponential decay: weight halves every DECAY_HALF_LIFE_DAYS days.""" |
| delta = (datetime.now(timezone.utc) - last_updated).total_seconds() |
| days = delta / 86400.0 |
| return math.exp(-math.log(2) * days / _DECAY_HALF_LIFE_DAYS) |
|
|
|
|
| class TemporalKnowledgeGraph: |
| def __init__(self): |
| self._nodes: Dict[str, MemoryItem] = {} |
| self._edges: List[TemporalEdge] = [] |
|
|
| |
|
|
| def add_event(self, item: MemoryItem, follows_id: Optional[str] = None) -> MemoryItem: |
| item.tier = MemoryTier.TEMPORAL |
| self._nodes[item.item_id] = item |
| if follows_id and follows_id in self._nodes: |
| self._edges.append(TemporalEdge( |
| source_id=follows_id, |
| target_id=item.item_id, |
| relation="precedes", |
| weight=1.0, |
| )) |
| return item |
|
|
| def all(self) -> List[MemoryItem]: |
| return list(self._nodes.values()) |
|
|
| |
|
|
| def get_edges(self, node_id: str) -> List[TemporalEdge]: |
| return [e for e in self._edges if e.source_id == node_id] |
|
|
| def reinforce_edge(self, source_id: str, target_id: str, delta: float = 0.1) -> None: |
| for edge in self._edges: |
| if edge.source_id == source_id and edge.target_id == target_id: |
| edge.weight = min(1.0, edge.weight + delta) |
| edge.last_updated = datetime.now(timezone.utc) |
|
|
| def decay_all_edges(self) -> None: |
| """Apply time-based decay to all edge weights.""" |
| for edge in self._edges: |
| edge.weight *= _time_decay(edge.last_updated) |
|
|
| |
|
|
| def retrieve(self, query: str, top_k: int = 5) -> List[RetrievalResult]: |
| return retrieve(query, self.all(), top_k=top_k) |
|
|
| |
|
|
| def timeline(self) -> List[MemoryItem]: |
| return sorted(self._nodes.values(), key=lambda n: n.created_at) |
|
|