agmem 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,410 @@
1
+ """
2
+ Memory Archaeology - Deep history exploration and analysis tools.
3
+
4
+ This module provides:
5
+ - Historical context reconstruction
6
+ - Memory evolution tracking
7
+ - Forgotten knowledge discovery
8
+ - Pattern analysis across time
9
+ """
10
+
11
+ import json
12
+ import re
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime, timedelta, timezone
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional, Set, Tuple
17
+
18
+
19
+ @dataclass
20
+ class MemoryEvolution:
21
+ """Tracks how a memory file evolved over time."""
22
+
23
+ path: str
24
+ first_seen: str
25
+ last_modified: str
26
+ version_count: int
27
+ commits: List[str]
28
+ size_history: List[Tuple[str, int]] # (timestamp, size)
29
+
30
+ def to_dict(self) -> Dict[str, Any]:
31
+ return {
32
+ "path": self.path,
33
+ "first_seen": self.first_seen,
34
+ "last_modified": self.last_modified,
35
+ "version_count": self.version_count,
36
+ "commits": self.commits,
37
+ "size_history": self.size_history,
38
+ }
39
+
40
+
41
+ @dataclass
42
+ class ForgottenMemory:
43
+ """A memory that hasn't been accessed recently."""
44
+
45
+ path: str
46
+ last_accessed: str
47
+ days_since_access: int
48
+ content_preview: str
49
+ memory_type: str
50
+ relevance_score: float = 0.0
51
+
52
+ def to_dict(self) -> Dict[str, Any]:
53
+ return {
54
+ "path": self.path,
55
+ "last_accessed": self.last_accessed,
56
+ "days_since_access": self.days_since_access,
57
+ "content_preview": self.content_preview,
58
+ "memory_type": self.memory_type,
59
+ "relevance_score": self.relevance_score,
60
+ }
61
+
62
+
63
+ @dataclass
64
+ class TemporalPattern:
65
+ """A pattern in memory activity over time."""
66
+
67
+ pattern_type: str # "burst", "periodic", "declining", "growing"
68
+ description: str
69
+ files_involved: List[str]
70
+ time_range: Tuple[str, str]
71
+ confidence: float
72
+
73
+
74
+ class HistoryExplorer:
75
+ """Explores memory history across commits."""
76
+
77
+ def __init__(self, repo_root: Path):
78
+ self.repo_root = Path(repo_root)
79
+
80
+ def get_file_history(self, relative_path: str, max_commits: int = 50) -> List[Dict[str, Any]]:
81
+ """Get commit history for a specific file."""
82
+ try:
83
+ from memvcs.core.repository import Repository
84
+
85
+ repo = Repository(self.repo_root)
86
+ commits = repo.get_log(max_count=max_commits)
87
+
88
+ file_commits = []
89
+ for commit_info in commits:
90
+ # Check if file was in this commit's tree
91
+ # This is a simplified check - full implementation would walk the tree
92
+ file_commits.append(
93
+ {
94
+ "commit": commit_info["short_hash"],
95
+ "message": commit_info["message"],
96
+ "timestamp": commit_info.get("timestamp", ""),
97
+ "author": commit_info.get("author", ""),
98
+ }
99
+ )
100
+
101
+ return file_commits[:max_commits]
102
+ except Exception:
103
+ return []
104
+
105
+ def get_memory_evolution(self, relative_path: str) -> Optional[MemoryEvolution]:
106
+ """Track how a memory file evolved."""
107
+ try:
108
+ from memvcs.core.repository import Repository
109
+
110
+ repo = Repository(self.repo_root)
111
+ commits = repo.get_log(max_count=100)
112
+
113
+ if not commits:
114
+ return None
115
+
116
+ first_seen = commits[-1].get("timestamp", "")
117
+ last_modified = commits[0].get("timestamp", "")
118
+ commit_hashes = [c["short_hash"] for c in commits]
119
+
120
+ return MemoryEvolution(
121
+ path=relative_path,
122
+ first_seen=first_seen,
123
+ last_modified=last_modified,
124
+ version_count=len(commits),
125
+ commits=commit_hashes,
126
+ size_history=[], # Would require content at each commit
127
+ )
128
+ except Exception:
129
+ return None
130
+
131
+ def compare_versions(self, path: str, commit1: str, commit2: str) -> Dict[str, Any]:
132
+ """Compare two versions of a file."""
133
+ try:
134
+ from memvcs.core.repository import Repository
135
+ from memvcs.core.diff import DiffEngine
136
+
137
+ repo = Repository(self.repo_root)
138
+ engine = DiffEngine(repo.object_store)
139
+
140
+ diff = engine.diff_commits(commit1, commit2)
141
+
142
+ return {
143
+ "path": path,
144
+ "from_commit": commit1,
145
+ "to_commit": commit2,
146
+ "has_changes": len(diff.files) > 0,
147
+ "files_changed": len(diff.files),
148
+ }
149
+ except Exception as e:
150
+ return {"error": str(e)}
151
+
152
+
153
+ class ForgottenKnowledgeFinder:
154
+ """Discovers forgotten or under-utilized memories."""
155
+
156
+ def __init__(self, repo_root: Path, access_log_path: Optional[Path] = None):
157
+ self.repo_root = Path(repo_root)
158
+ self.mem_dir = self.repo_root / ".mem"
159
+ self.access_log = access_log_path or (self.mem_dir / "access.log")
160
+
161
+ def _load_access_times(self) -> Dict[str, str]:
162
+ """Load last access times from log."""
163
+ access_times = {}
164
+ if self.access_log.exists():
165
+ try:
166
+ for line in self.access_log.read_text().strip().split("\n"):
167
+ if line:
168
+ parts = line.split("|")
169
+ if len(parts) >= 2:
170
+ access_times[parts[1]] = parts[0]
171
+ except Exception:
172
+ pass
173
+ return access_times
174
+
175
+ def find_forgotten(self, days_threshold: int = 30, limit: int = 20) -> List[ForgottenMemory]:
176
+ """Find memories not accessed in the given time period."""
177
+ from memvcs.core.repository import Repository
178
+
179
+ try:
180
+ repo = Repository(self.repo_root)
181
+ current_dir = repo.current_dir
182
+
183
+ access_times = self._load_access_times()
184
+ now = datetime.now(timezone.utc)
185
+ threshold = now - timedelta(days=days_threshold)
186
+
187
+ forgotten = []
188
+ for filepath in current_dir.rglob("*"):
189
+ if not filepath.is_file():
190
+ continue
191
+
192
+ rel_path = str(filepath.relative_to(current_dir))
193
+
194
+ # Determine last access
195
+ if rel_path in access_times:
196
+ last_access = access_times[rel_path]
197
+ else:
198
+ # Use modification time as fallback
199
+ mtime = filepath.stat().st_mtime
200
+ last_access = datetime.fromtimestamp(mtime, tz=timezone.utc).isoformat()
201
+
202
+ try:
203
+ last_dt = datetime.fromisoformat(last_access.replace("Z", "+00:00"))
204
+ if last_dt < threshold:
205
+ days_ago = (now - last_dt).days
206
+ content = filepath.read_text(encoding="utf-8", errors="replace")[:200]
207
+ memory_type = self._infer_memory_type(filepath)
208
+
209
+ forgotten.append(
210
+ ForgottenMemory(
211
+ path=rel_path,
212
+ last_accessed=last_access,
213
+ days_since_access=days_ago,
214
+ content_preview=content,
215
+ memory_type=memory_type,
216
+ )
217
+ )
218
+ except Exception:
219
+ pass
220
+
221
+ # Sort by days since access (oldest first)
222
+ forgotten.sort(key=lambda x: x.days_since_access, reverse=True)
223
+ return forgotten[:limit]
224
+
225
+ except Exception:
226
+ return []
227
+
228
+ def _infer_memory_type(self, filepath: Path) -> str:
229
+ """Infer memory type from path."""
230
+ parts = filepath.parts
231
+ for mt in ["episodic", "semantic", "procedural"]:
232
+ if mt in parts:
233
+ return mt
234
+ return "unknown"
235
+
236
+ def rediscover_relevant(self, query: str, days_threshold: int = 30) -> List[ForgottenMemory]:
237
+ """Find forgotten memories relevant to a query."""
238
+ forgotten = self.find_forgotten(days_threshold=days_threshold, limit=100)
239
+
240
+ # Simple relevance scoring based on query terms
241
+ query_terms = set(query.lower().split())
242
+
243
+ for memory in forgotten:
244
+ content_lower = memory.content_preview.lower()
245
+ matches = sum(1 for term in query_terms if term in content_lower)
246
+ memory.relevance_score = matches / max(1, len(query_terms))
247
+
248
+ # Sort by relevance
249
+ forgotten.sort(key=lambda x: x.relevance_score, reverse=True)
250
+ return [m for m in forgotten if m.relevance_score > 0][:20]
251
+
252
+
253
+ class PatternAnalyzer:
254
+ """Analyzes temporal patterns in memory activity."""
255
+
256
+ def __init__(self, repo_root: Path):
257
+ self.repo_root = Path(repo_root)
258
+
259
+ def analyze_activity_patterns(self, days: int = 90) -> List[TemporalPattern]:
260
+ """Analyze patterns in memory activity."""
261
+ from memvcs.core.repository import Repository
262
+
263
+ patterns = []
264
+
265
+ try:
266
+ repo = Repository(self.repo_root)
267
+ commits = repo.get_log(max_count=500)
268
+
269
+ if len(commits) < 5:
270
+ return []
271
+
272
+ # Group commits by day
273
+ by_day: Dict[str, int] = {}
274
+ for commit in commits:
275
+ ts = commit.get("timestamp", "")[:10]
276
+ if ts:
277
+ by_day[ts] = by_day.get(ts, 0) + 1
278
+
279
+ # Detect patterns
280
+ sorted_days = sorted(by_day.keys())
281
+
282
+ if sorted_days:
283
+ # Check for bursts (days with >3x average)
284
+ avg_commits = sum(by_day.values()) / len(by_day)
285
+ burst_days = [d for d, c in by_day.items() if c > avg_commits * 3]
286
+ if burst_days:
287
+ patterns.append(
288
+ TemporalPattern(
289
+ pattern_type="burst",
290
+ description=f"High activity bursts on {len(burst_days)} days",
291
+ files_involved=[],
292
+ time_range=(burst_days[0], burst_days[-1]),
293
+ confidence=0.8,
294
+ )
295
+ )
296
+
297
+ # Check for declining activity
298
+ recent_30 = [
299
+ c for d, c in by_day.items() if d >= sorted_days[-30] if len(sorted_days) >= 30
300
+ ]
301
+ older_30 = [
302
+ c for d, c in by_day.items() if d < sorted_days[-30] if len(sorted_days) >= 60
303
+ ]
304
+ if recent_30 and older_30:
305
+ recent_avg = sum(recent_30) / len(recent_30)
306
+ older_avg = sum(older_30) / len(older_30)
307
+ if recent_avg < older_avg * 0.5:
308
+ patterns.append(
309
+ TemporalPattern(
310
+ pattern_type="declining",
311
+ description="Activity has declined significantly",
312
+ files_involved=[],
313
+ time_range=(sorted_days[0], sorted_days[-1]),
314
+ confidence=0.7,
315
+ )
316
+ )
317
+
318
+ except Exception:
319
+ pass
320
+
321
+ return patterns
322
+
323
+ def get_memory_hotspots(self, days: int = 30) -> List[Dict[str, Any]]:
324
+ """Find most frequently modified memories."""
325
+ from memvcs.core.repository import Repository
326
+
327
+ try:
328
+ repo = Repository(self.repo_root)
329
+ commits = repo.get_log(max_count=100)
330
+
331
+ # This is a simplified version - full implementation would
332
+ # track which files changed in each commit
333
+ file_activity: Dict[str, int] = {}
334
+
335
+ # Count commits as proxy for activity
336
+ return [
337
+ {"path": path, "activity_count": count}
338
+ for path, count in sorted(file_activity.items(), key=lambda x: -x[1])[:10]
339
+ ]
340
+ except Exception:
341
+ return []
342
+
343
+
344
+ class ContextReconstructor:
345
+ """Reconstructs historical context around memories."""
346
+
347
+ def __init__(self, repo_root: Path):
348
+ self.repo_root = Path(repo_root)
349
+
350
+ def reconstruct_context(
351
+ self, path: str, target_date: str, window_days: int = 7
352
+ ) -> Dict[str, Any]:
353
+ """Reconstruct what was happening around a memory at a point in time."""
354
+ from memvcs.core.repository import Repository
355
+
356
+ try:
357
+ repo = Repository(self.repo_root)
358
+ commits = repo.get_log(max_count=500)
359
+
360
+ # Find commits around target date
361
+ target_dt = datetime.fromisoformat(target_date.replace("Z", "+00:00"))
362
+ window_start = target_dt - timedelta(days=window_days)
363
+ window_end = target_dt + timedelta(days=window_days)
364
+
365
+ nearby_commits = []
366
+ for commit in commits:
367
+ ts = commit.get("timestamp", "")
368
+ if ts:
369
+ try:
370
+ commit_dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
371
+ if window_start <= commit_dt <= window_end:
372
+ nearby_commits.append(commit)
373
+ except Exception:
374
+ pass
375
+
376
+ return {
377
+ "target_path": path,
378
+ "target_date": target_date,
379
+ "window_days": window_days,
380
+ "commits_in_window": len(nearby_commits),
381
+ "commits": nearby_commits[:20],
382
+ "summary": f"Found {len(nearby_commits)} commits within {window_days} days of {target_date[:10]}",
383
+ }
384
+ except Exception as e:
385
+ return {"error": str(e)}
386
+
387
+
388
+ # --- Dashboard Helper ---
389
+
390
+
391
+ def get_archaeology_dashboard(repo_root: Path) -> Dict[str, Any]:
392
+ """Get data for memory archaeology dashboard."""
393
+ pattern_analyzer = PatternAnalyzer(repo_root)
394
+ forgotten_finder = ForgottenKnowledgeFinder(repo_root)
395
+
396
+ forgotten = forgotten_finder.find_forgotten(days_threshold=30, limit=10)
397
+ patterns = pattern_analyzer.analyze_activity_patterns(days=90)
398
+
399
+ return {
400
+ "forgotten_memories": [f.to_dict() for f in forgotten],
401
+ "forgotten_count": len(forgotten),
402
+ "activity_patterns": [
403
+ {
404
+ "type": p.pattern_type,
405
+ "description": p.description,
406
+ "confidence": p.confidence,
407
+ }
408
+ for p in patterns
409
+ ],
410
+ }