File size: 15,987 Bytes
66916eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
"""
TensorStore Agent Memory β€” Google TensorStore-inspired Multi-Dimensional Agent Memory

Concepts from Google Neural Mapping:
- TensorStore: N-dimensional array storage for petabytes (C++/Python)
- Neuroglancer: Multi-resolution zoomable data viewer
- SegCLR: Self-supervised embedding learning

Applied to Agent Memory:
- Store all agent interactions as embedding vectors in a tensor
- Multi-resolution retrieval (recent, week, month, all-time)
- Semantic similarity search (not grep)
- Auto-clustering of related memories
- Zero external dependencies β€” pure numpy + built-in json

Usage:
    from tensorstore_memory import AgentMemoryTensor
    mem = AgentMemoryTensor(dimensions=384)
    mem.store("rushd", "task completed: trade signal BTC", embedding=[...])
    results = mem.query("what trades did we do?", top_k=5)
"""

import json, time, math, os, hashlib
from pathlib import Path
from collections import defaultdict, OrderedDict
from datetime import datetime, timedelta
import threading

try:
    import numpy as np
except ImportError:
    np = None

# ─── Configuration ───────────────────────────────────────
MEMORY_DIR = Path(os.environ.get("TENSORSTORE_DIR", "/tmp/agent-tensorstore"))
DEFAULT_DIM = 384  # embedding dimension
MAX_RESOLUTIONS = 4  # zoom levels: recent, daily, weekly, all-time

# ─── Simple embedding (no external deps) ──────────────────
def simple_embed(text: str, dim: int = DEFAULT_DIM) -> list[float]:
    """Lightweight text embedding using character n-gram hashing.
    For production, plug in any embedding model (sentence-transformers, etc.)"""
    if np is None:
        # Fallback pure Python embedding
        vec = [0.0] * dim
        for i, ch in enumerate(text):
            h = hash(f"{i}:{ch}") % dim
            vec[h] += 1.0 / (i + 1)
        # Normalize
        norm = math.sqrt(sum(v*v for v in vec)) or 1
        return [v/norm for v in vec]
    else:
        # Use numpy for faster hashing
        vec = np.zeros(dim, dtype=np.float32)
        for i, ch in enumerate(text):
            h = abs(hash(f"{i}:{ch}")) % dim
            vec[h] += 1.0 / (i + 1)
        norm = np.linalg.norm(vec) or 1.0
        return (vec / norm).tolist()

def cosine_similarity(a, b):
    """Cosine similarity between two vectors."""
    if np is not None:
        a, b = np.array(a), np.array(b)
        return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8))
    dot = sum(x*y for x,y in zip(a,b))
    na = math.sqrt(sum(x*x for x in a)) + 1e-8
    nb = math.sqrt(sum(x*x for x in b)) + 1e-8
    return dot / (na * nb)


# ─── LRU Cache for hot memories ───────────────────────────
class LRUCache:
    def __init__(self, maxsize: int = 1000):
        self.cache = OrderedDict()
        self.maxsize = maxsize
        self._lock = threading.Lock()
    
    def get(self, key):
        with self._lock:
            if key in self.cache:
                self.cache.move_to_end(key)
                return self.cache[key]
            return None
    
    def put(self, key, value):
        with self._lock:
            if key in self.cache:
                self.cache.move_to_end(key)
            self.cache[key] = value
            while len(self.cache) > self.maxsize:
                self.cache.popitem(last=False)


# ─── Core: Agent Memory Tensor ────────────────────────────
class AgentMemoryTensor:
    """Multi-resolution memory tensor for agent interactions.
    Inspired by Google TensorStore's n-dimensional array model."""
    
    def __init__(self, dimensions: int = DEFAULT_DIM, cache_size: int = 1000):
        self.dim = dimensions
        self.memories: list[dict] = []  # [{"agent","text","embedding","ts","tags"}]
        self.agents: dict[str, dict] = {}
        self.cache = LRUCache(cache_size)
        self._lock = threading.Lock()
        self._dirty = False
        
        # Resolution layers (like Neuroglancer zoom levels)
        self.resolutions = {
            "recent": {"max_age_hours": 1, "memories": []},
            "daily": {"max_age_hours": 24, "memories": []},
            "weekly": {"max_age_hours": 168, "memories": []},
            "all_time": {"max_age_hours": float("inf"), "memories": []},
        }
        
        MEMORY_DIR.mkdir(parents=True, exist_ok=True)
        self._load()
    
    # ─── Store ─────────────────────────────────────────────
    def store(self, agent_id: str, text: str, 
              embedding: list[float] = None, 
              tags: list[str] = None,
              metadata: dict = None) -> str:
        """Store a memory entry. Returns memory_id."""
        if embedding is None:
            embedding = simple_embed(text, self.dim)
        
        mem_id = hashlib.sha256(f"{agent_id}:{text}:{time.time()}".encode()).hexdigest()[:16]
        
        entry = {
            "id": mem_id,
            "agent": agent_id,
            "text": text[:500],  # truncate long texts
            "embedding": embedding,
            "ts": time.time(),
            "tags": tags or [],
            "metadata": metadata or {},
        }
        
        with self._lock:
            self.memories.append(entry)
            if agent_id not in self.agents:
                self.agents[agent_id] = {"count": 0, "first_seen": time.time(), "last_seen": time.time()}
            self.agents[agent_id]["count"] += 1
            self.agents[agent_id]["last_seen"] = time.time()
            
            # Update resolutions
            now = time.time()
            for res_name, res_data in self.resolutions.items():
                max_age = res_data["max_age_hours"] * 3600
                if max_age == float("inf"):
                    res_data["memories"].append(mem_id)
                else:
                    # Keep only memories within time window
                    res_data["memories"] = [m for m in res_data["memories"] 
                                           if any(mm["id"] == m and now - mm["ts"] <= max_age 
                                                 for mm in self.memories[-100:])]
                    res_data["memories"].append(mem_id)
            
            self._dirty = True
        
        self.cache.put(mem_id, entry)
        return mem_id
    
    # ─── Query by text (semantic search) ───────────────────
    def query(self, query_text: str, top_k: int = 5, 
              agent_filter: str = None, tag_filter: str = None,
              resolution: str = "all_time") -> list[dict]:
        """Semantic search across memories."""
        query_emb = simple_embed(query_text, self.dim)
        
        # Use cached result if available
        cache_key = f"q:{query_text[:50]}:{top_k}:{agent_filter}:{tag_filter}:{resolution}"
        cached = self.cache.get(cache_key)
        if cached:
            return cached
        
        with self._lock:
            # Filter by resolution
            if resolution in self.resolutions:
                valid_ids = set(self.resolutions[resolution]["memories"][-1000:])
                candidates = [m for m in self.memories[-5000:] if m["id"] in valid_ids]
            else:
                candidates = self.memories[-5000:]
            
            # Filter by agent/tag
            if agent_filter:
                candidates = [m for m in candidates if m["agent"] == agent_filter]
            if tag_filter:
                candidates = [m for m in candidates if tag_filter in m.get("tags", [])]
        
        # Score and rank
        scored = []
        for mem in candidates:
            sim = cosine_similarity(query_emb, mem["embedding"])
            # Boost recent memories
            recency = 1.0 / (1.0 + (time.time() - mem["ts"]) / 86400)  # days ago
            score = sim * 0.7 + recency * 0.3
            scored.append((score, mem))
        
        scored.sort(key=lambda x: x[0], reverse=True)
        results = []
        for score, mem in scored[:top_k]:
            results.append({
                "id": mem["id"],
                "agent": mem["agent"],
                "text": mem["text"],
                "score": round(score, 4),
                "similarity": round(cosine_similarity(query_emb, mem["embedding"]), 4),
                "timestamp": datetime.fromtimestamp(mem["ts"]).isoformat(),
                "tags": mem["tags"],
                "metadata": mem.get("metadata", {}),
            })
        
        self.cache.put(cache_key, results)
        return results
    
    # ─── Get by agent (timeline) ───────────────────────────
    def agent_timeline(self, agent_id: str, limit: int = 20) -> list[dict]:
        """Get recent memories for a specific agent."""
        with self._lock:
            agent_mems = [m for m in self.memories[-limit*10:] if m["agent"] == agent_id]
        return [{
            "id": m["id"],
            "text": m["text"],
            "timestamp": datetime.fromtimestamp(m["ts"]).isoformat(),
            "tags": m.get("tags", []),
        } for m in agent_mems[-limit:]]
    
    # ─── Similar agents (like SegCLR cell type clustering) ──
    def similar_agents(self, agent_id: str, top_k: int = 5) -> list[dict]:
        """Find agents with similar behavior patterns."""
        if agent_id not in self.agents:
            return []
        
        # Build agent centroids from their memory embeddings
        centroids = {}
        with self._lock:
            for aid in self.agents:
                agent_mems = [m for m in self.memories[-1000:] if m["agent"] == aid]
                if agent_mems:
                    if np is not None:
                        emb_matrix = np.array([m["embedding"] for m in agent_mems])
                        centroids[aid] = emb_matrix.mean(axis=0).tolist()
                    else:
                        centroid = [0.0] * self.dim
                        for m in agent_mems:
                            for i, v in enumerate(m["embedding"]):
                                centroid[i] += v
                        n = len(agent_mems)
                        centroids[aid] = [v/n for v in centroid]
        
        target = centroids.get(agent_id)
        if not target:
            return []
        
        similarities = []
        for aid, centroid in centroids.items():
            if aid != agent_id:
                sim = cosine_similarity(target, centroid)
                similarities.append({"agent": aid, "similarity": round(sim, 4)})
        
        similarities.sort(key=lambda x: x["similarity"], reverse=True)
        return similarities[:top_k]
    
    # ─── Stats ─────────────────────────────────────────────
    def stats(self) -> dict:
        """Memory statistics like TensorStore's metadata inspection."""
        with self._lock:
            total = len(self.memories)
            agents_count = len(self.agents)
            
            # Memory size by agent
            by_agent = {aid: data["count"] for aid, data in self.agents.items()}
            top_agents = sorted(by_agent.items(), key=lambda x: x[1], reverse=True)[:10]
            
            # Memory age distribution
            now = time.time()
            ages = {"<1h": 0, "1-24h": 0, "1-7d": 0, ">7d": 0}
            for m in self.memories:
                age_hours = (now - m["ts"]) / 3600
                if age_hours < 1: ages["<1h"] += 1
                elif age_hours < 24: ages["1-24h"] += 1
                elif age_hours < 168: ages["1-7d"] += 1
                else: ages[">7d"] += 1
            
            return {
                "total_memories": total,
                "total_agents": agents_count,
                "dimensions": self.dim,
                "resolution_layers": len(self.resolutions),
                "top_agents": dict(top_agents),
                "age_distribution": ages,
                "cache_size": len(self.cache.cache),
                "memory_size_kb": round(total * self.dim * 4 / 1024, 1),  # float32 estimate
            }
    
    # ─── Persistence ───────────────────────────────────────
    def _load(self):
        path = MEMORY_DIR / "memory.json"
        if path.exists():
            try:
                with open(path) as f:
                    data = json.load(f)
                self.memories = data.get("memories", [])
                self.agents = data.get("agents", {})
            except:
                pass
    
    def save(self):
        path = MEMORY_DIR / "memory.json"
        with self._lock:
            # Keep last 10000 memories to avoid bloat
            data = {
                "memories": self.memories[-10000:],
                "agents": self.agents,
            }
        with open(path, "w") as f:
            json.dump(data, f, ensure_ascii=False)
        self._dirty = False
    
    def auto_save(self, interval_seconds: int = 60):
        """Background auto-save thread."""
        def _loop():
            while True:
                time.sleep(interval_seconds)
                if self._dirty:
                    self.save()
        t = threading.Thread(target=_loop, daemon=True)
        t.start()


# ─── CLI Demo ─────────────────────────────────────────────
if __name__ == "__main__":
    print("🧠 TensorStore Agent Memory β€” Google TensorStore-inspired Multi-Resolution Memory")
    print(f"   Dimensions: {DEFAULT_DIM}  |  Directory: {MEMORY_DIR}")
    
    mem = AgentMemoryTensor(dimensions=DEFAULT_DIM)
    
    # Demo: simulate agent memories
    agents = ["rushd", "wafa", "awf", "dragon", "hermes", "musa", "zeus", "haytham"]
    tasks = [
        "routed task to awf for trading signal",
        "verified output from dragon agent",
        "executed BTC/USDT trade with 2% profit",
        "memory search completed for hayula papers",
        "skill code_review invoked on PR #42",
        "Arabic text generation for blog post",
        "error timeout on connection to M2",
        "created new agent connectome snapshot",
    ]
    
    print(f"\nπŸ“ Storing {len(agents) * 5} memories...")
    import random
    for i in range(len(agents) * 5):
        agent = random.choice(agents)
        task = random.choice(tasks)
        tags = random.sample(["trade", "code", "memory", "route", "error"], k=random.randint(1, 3))
        mem.store(agent, task, tags=tags)
    
    mem.save()
    
    # Stats
    s = mem.stats()
    print(f"\nπŸ“Š Stats:")
    print(f"   Total: {s['total_memories']} memories across {s['total_agents']} agents")
    print(f"   Size: ~{s['memory_size_kb']} KB")
    print(f"   Cache: {s['cache_size']} entries")
    print(f"   Age: {s['age_distribution']}")
    
    # Query
    q = "what trading activity happened?"
    print(f"\nπŸ” Query: '{q}'")
    results = mem.query(q, top_k=3)
    for r in results:
        print(f"   [{r['score']:.3f}] {r['agent']}: {r['text'][:60]}")
    
    # Similar agents
    print(f"\n🧬 Agents similar to 'awf':")
    similar = mem.similar_agents("awf", top_k=3)
    for s in similar:
        print(f"   {s['agent']}: similarity={s['similarity']}")
    
    # Multi-resolution
    print(f"\nπŸ”¬ Resolutions:")
    for name, data in mem.resolutions.items():
        print(f"   {name}: {len(data['memories'])} memories (max_age={data['max_age_hours']}h)")
    
    print(f"\nβœ… TensorStore Agent Memory ready!")
    print(f"   Memory file: {MEMORY_DIR}/memory.json")