haze / cloud /user_cloud.py
ataeff's picture
Upload 13 files
3279f65 verified
#!/usr/bin/env python3
# user_cloud.py — Temporal Emotional Fingerprint
#
# Tracks user's emotional history with exponential decay.
# Recent emotions matter more (24h half-life).
#
# The "user cloud" is a 100D vector where each dimension
# represents cumulative exposure to that emotion anchor.
#
# Decay formula:
# weight(t) = exp(-t / tau)
# where tau = 24 hours, t = time since event
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 # Unix timestamp
primary_idx: int # Index of primary emotion (0-99)
secondary_idx: int # Index of secondary emotion (0-99)
weight: float = 1.0 # Event weight (default 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 # 24h half-life
max_history: int = 1000 # Keep last N events
@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)
# Prune old events if history too long
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:
# Time since event (in seconds)
dt = current_time - event.timestamp
# Exponential decay: exp(-dt / tau)
decay = np.exp(-dt / self.tau)
# Add decayed weight to fingerprint
fingerprint[event.primary_idx] += event.weight * decay * 0.7
fingerprint[event.secondary_idx] += event.weight * decay * 0.3
# Normalize to [0, 1] range
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()
# Initialize empty cloud
cloud = UserCloud(half_life_hours=24.0)
print(f"Initialized user cloud (half-life={cloud.half_life_hours}h)")
print()
# Simulate emotion events over time
print("Simulating emotion events:")
current_time = time.time()
# Add events at different times
events_to_add = [
(0, 5, -48), # FEAR event 48h ago
(20, 22, -24), # LOVE event 24h ago
(38, 40, -12), # RAGE event 12h ago
(55, 58, -6), # VOID event 6h ago
(70, 72, -1), # FLOW event 1h ago
]
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()
# Get fingerprint
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()
# Show dominant emotions
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()
# Show decay effect
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()
# Test save/load
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()
# Stats
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)