| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| from __future__ import annotations |
| import asyncio |
| import numpy as np |
| import time |
| from pathlib import Path |
| from typing import List, Dict, Optional |
| from dataclasses import dataclass, field |
| import json |
|
|
|
|
| @dataclass |
| class EmotionEvent: |
| """Single emotion event in user history.""" |
| timestamp: float |
| primary_idx: int |
| secondary_idx: int |
| weight: float = 1.0 |
|
|
|
|
| @dataclass |
| class UserCloud: |
| """ |
| Temporal emotional fingerprint with exponential decay. |
| |
| Maintains: |
| - History of emotion events |
| - Decayed fingerprint (100D vector) |
| - Decay half-life (default 24 hours) |
| |
| The fingerprint is recomputed on-the-fly with decay applied. |
| """ |
|
|
| events: List[EmotionEvent] = field(default_factory=list) |
| half_life_hours: float = 24.0 |
| max_history: int = 1000 |
|
|
| @property |
| def tau(self) -> float: |
| """Decay constant (in seconds).""" |
| return self.half_life_hours * 3600 / np.log(2) |
|
|
| def add_event( |
| self, |
| primary_idx: int, |
| secondary_idx: int, |
| weight: float = 1.0, |
| timestamp: Optional[float] = None, |
| ) -> None: |
| """ |
| Add an emotion event to history. |
| |
| Args: |
| primary_idx: primary emotion index (0-99) |
| secondary_idx: secondary emotion index (0-99) |
| weight: event importance (default 1.0) |
| timestamp: Unix timestamp (default: now) |
| """ |
| if timestamp is None: |
| timestamp = time.time() |
|
|
| event = EmotionEvent( |
| timestamp=timestamp, |
| primary_idx=primary_idx, |
| secondary_idx=secondary_idx, |
| weight=weight, |
| ) |
|
|
| self.events.append(event) |
|
|
| |
| if len(self.events) > self.max_history: |
| self.events = self.events[-self.max_history:] |
|
|
| def get_fingerprint(self, current_time: Optional[float] = None) -> np.ndarray: |
| """ |
| Compute current emotional fingerprint with temporal decay. |
| |
| Returns: |
| (100,) vector of decayed emotion exposures |
| """ |
| if current_time is None: |
| current_time = time.time() |
|
|
| fingerprint = np.zeros(100, dtype=np.float32) |
|
|
| for event in self.events: |
| |
| dt = current_time - event.timestamp |
|
|
| |
| decay = np.exp(-dt / self.tau) |
|
|
| |
| fingerprint[event.primary_idx] += event.weight * decay * 0.7 |
| fingerprint[event.secondary_idx] += event.weight * decay * 0.3 |
|
|
| |
| if fingerprint.max() > 0: |
| fingerprint = fingerprint / fingerprint.max() |
|
|
| return fingerprint |
|
|
| def get_recent_emotions( |
| self, |
| hours: float = 24.0, |
| current_time: Optional[float] = None, |
| ) -> List[EmotionEvent]: |
| """Get events from last N hours.""" |
| if current_time is None: |
| current_time = time.time() |
|
|
| cutoff = current_time - (hours * 3600) |
| return [e for e in self.events if e.timestamp >= cutoff] |
|
|
| def get_dominant_emotions( |
| self, |
| top_k: int = 5, |
| current_time: Optional[float] = None, |
| ) -> List[tuple]: |
| """ |
| Get top-k dominant emotions from fingerprint. |
| |
| Returns: |
| List of (emotion_idx, strength) tuples |
| """ |
| fingerprint = self.get_fingerprint(current_time) |
| top_indices = np.argsort(fingerprint)[-top_k:][::-1] |
| return [(int(idx), float(fingerprint[idx])) for idx in top_indices] |
|
|
| def save(self, path: Path) -> None: |
| """Save user cloud to JSON file.""" |
| data = { |
| "events": [ |
| { |
| "timestamp": e.timestamp, |
| "primary_idx": e.primary_idx, |
| "secondary_idx": e.secondary_idx, |
| "weight": e.weight, |
| } |
| for e in self.events |
| ], |
| "half_life_hours": self.half_life_hours, |
| "max_history": self.max_history, |
| } |
|
|
| with open(path, "w") as f: |
| json.dump(data, f, indent=2) |
|
|
| print(f"[user_cloud] saved {len(self.events)} events to {path}") |
|
|
| @classmethod |
| def load(cls, path: Path) -> "UserCloud": |
| """Load user cloud from JSON file.""" |
| with open(path, "r") as f: |
| data = json.load(f) |
|
|
| events = [ |
| EmotionEvent( |
| timestamp=e["timestamp"], |
| primary_idx=e["primary_idx"], |
| secondary_idx=e["secondary_idx"], |
| weight=e.get("weight", 1.0), |
| ) |
| for e in data["events"] |
| ] |
|
|
| cloud = cls( |
| events=events, |
| half_life_hours=data.get("half_life_hours", 24.0), |
| max_history=data.get("max_history", 1000), |
| ) |
|
|
| print(f"[user_cloud] loaded {len(events)} events from {path}") |
| return cloud |
|
|
| def stats(self) -> Dict: |
| """Return statistics about user cloud.""" |
| current_time = time.time() |
| fingerprint = self.get_fingerprint(current_time) |
|
|
| recent_24h = len(self.get_recent_emotions(24.0, current_time)) |
| recent_7d = len(self.get_recent_emotions(24.0 * 7, current_time)) |
|
|
| return { |
| "total_events": len(self.events), |
| "events_24h": recent_24h, |
| "events_7d": recent_7d, |
| "fingerprint_max": float(fingerprint.max()), |
| "fingerprint_mean": float(fingerprint.mean()), |
| "fingerprint_nonzero": int((fingerprint > 0).sum()), |
| "half_life_hours": self.half_life_hours, |
| } |
|
|
|
|
| class AsyncUserCloud: |
| """ |
| Async wrapper for UserCloud with field lock discipline. |
| |
| Based on HAZE's async pattern - achieves coherence through |
| explicit operation ordering and atomicity. |
| |
| "The asyncio.Lock doesn't add information—it adds discipline." |
| """ |
| |
| def __init__(self, cloud: UserCloud): |
| self._sync = cloud |
| self._lock = asyncio.Lock() |
| |
| @classmethod |
| def create(cls, half_life_hours: float = 24.0) -> "AsyncUserCloud": |
| """Create new async user cloud.""" |
| cloud = UserCloud(half_life_hours=half_life_hours) |
| return cls(cloud) |
| |
| @classmethod |
| def load(cls, path: Path) -> "AsyncUserCloud": |
| """Load from file.""" |
| cloud = UserCloud.load(path) |
| return cls(cloud) |
| |
| async def add_event( |
| self, |
| primary_idx: int, |
| secondary_idx: int, |
| weight: float = 1.0, |
| timestamp: Optional[float] = None, |
| ) -> None: |
| """Add event with lock protection.""" |
| async with self._lock: |
| self._sync.add_event(primary_idx, secondary_idx, weight, timestamp) |
| |
| async def get_fingerprint(self, current_time: Optional[float] = None) -> np.ndarray: |
| """Get fingerprint (read-only, but lock for consistency).""" |
| async with self._lock: |
| return self._sync.get_fingerprint(current_time) |
| |
| async def get_dominant_emotions( |
| self, |
| top_k: int = 5, |
| current_time: Optional[float] = None, |
| ) -> List[tuple]: |
| """Get dominant emotions.""" |
| async with self._lock: |
| return self._sync.get_dominant_emotions(top_k, current_time) |
| |
| async def save(self, path: Path) -> None: |
| """Save with lock protection.""" |
| async with self._lock: |
| self._sync.save(path) |
| |
| async def stats(self) -> Dict: |
| """Get stats.""" |
| async with self._lock: |
| return self._sync.stats() |
|
|
|
|
| if __name__ == "__main__": |
| from .anchors import get_all_anchors |
|
|
| print("=" * 60) |
| print(" CLOUD v3.0 — User Cloud (Temporal Fingerprint)") |
| print("=" * 60) |
| print() |
|
|
| |
| cloud = UserCloud(half_life_hours=24.0) |
| print(f"Initialized user cloud (half-life={cloud.half_life_hours}h)") |
| print() |
|
|
| |
| print("Simulating emotion events:") |
| current_time = time.time() |
|
|
| |
| events_to_add = [ |
| (0, 5, -48), |
| (20, 22, -24), |
| (38, 40, -12), |
| (55, 58, -6), |
| (70, 72, -1), |
| ] |
|
|
| anchors = get_all_anchors() |
|
|
| for primary, secondary, hours_ago in events_to_add: |
| timestamp = current_time + (hours_ago * 3600) |
| cloud.add_event(primary, secondary, timestamp=timestamp) |
| print(f" {hours_ago:+3d}h: {anchors[primary]} + {anchors[secondary]}") |
| print() |
|
|
| |
| print("Current emotional fingerprint:") |
| fingerprint = cloud.get_fingerprint(current_time) |
| print(f" Shape: {fingerprint.shape}") |
| print(f" Max: {fingerprint.max():.3f}") |
| print(f" Mean: {fingerprint.mean():.3f}") |
| print(f" Nonzero: {(fingerprint > 0).sum()}/100") |
| print() |
|
|
| |
| print("Top 5 dominant emotions:") |
| for idx, strength in cloud.get_dominant_emotions(5, current_time): |
| bar = "█" * int(strength * 40) |
| print(f" {anchors[idx]:15s}: {strength:.3f} {bar}") |
| print() |
|
|
| |
| print("Decay effect over time:") |
| for hours in [1, 6, 12, 24, 48, 72]: |
| past_time = current_time - (hours * 3600) |
| fp = cloud.get_fingerprint(past_time) |
| print(f" {hours:3d}h ago: max={fp.max():.3f}, nonzero={int((fp > 0).sum())}") |
| print() |
|
|
| |
| print("Testing save/load:") |
| path = Path("./cloud_data.json") |
| cloud.save(path) |
|
|
| cloud2 = UserCloud.load(path) |
| fp2 = cloud2.get_fingerprint(current_time) |
|
|
| match = np.allclose(fingerprint, fp2) |
| print(f" Save/load {'✓' if match else '✗'}") |
| print() |
|
|
| |
| print("User cloud statistics:") |
| for k, v in cloud.stats().items(): |
| print(f" {k}: {v}") |
| print() |
|
|
| print("=" * 60) |
| print(" Temporal fingerprint operational. Memory with decay.") |
| print("=" * 60) |
|
|