| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | 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) |
| |
|