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,379 @@
1
+ """
2
+ Confidence Scoring - Memory reliability with temporal decay.
3
+
4
+ This module provides:
5
+ - Confidence scoring for memories
6
+ - Temporal decay calculations
7
+ - Source reliability tracking
8
+ - Evidence chain analysis
9
+ """
10
+
11
+ import hashlib
12
+ import json
13
+ import math
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime, timedelta, timezone
16
+ from pathlib import Path
17
+ from typing import Any, Dict, List, Optional
18
+
19
+
20
+ @dataclass
21
+ class ConfidenceFactors:
22
+ """Factors that contribute to confidence score."""
23
+
24
+ source_reliability: float = 1.0 # 0.0 to 1.0
25
+ corroboration_count: int = 0 # Number of supporting sources
26
+ age_days: float = 0.0 # Age in days
27
+ access_frequency: int = 0 # How often accessed
28
+ last_verified: Optional[str] = None
29
+ contradiction_count: int = 0 # Number of conflicting sources
30
+
31
+
32
+ @dataclass
33
+ class ConfidenceScore:
34
+ """A confidence score for a memory."""
35
+
36
+ score: float # 0.0 to 1.0
37
+ factors: ConfidenceFactors
38
+ decay_rate: float # Daily decay rate
39
+ computed_at: str
40
+
41
+ def to_dict(self) -> Dict[str, Any]:
42
+ return {
43
+ "score": round(self.score, 3),
44
+ "factors": {
45
+ "source_reliability": self.factors.source_reliability,
46
+ "corroboration_count": self.factors.corroboration_count,
47
+ "age_days": round(self.factors.age_days, 1),
48
+ "access_frequency": self.factors.access_frequency,
49
+ "contradiction_count": self.factors.contradiction_count,
50
+ },
51
+ "decay_rate": self.decay_rate,
52
+ "computed_at": self.computed_at,
53
+ }
54
+
55
+
56
+ class DecayModel:
57
+ """Models confidence decay over time."""
58
+
59
+ # Decay models
60
+ EXPONENTIAL = "exponential"
61
+ LINEAR = "linear"
62
+ STEP = "step"
63
+
64
+ def __init__(self, model: str = EXPONENTIAL, half_life_days: float = 30.0):
65
+ self.model = model
66
+ self.half_life_days = half_life_days
67
+
68
+ def calculate_decay(self, age_days: float) -> float:
69
+ """Calculate decay factor (0.0 to 1.0) based on age."""
70
+ if age_days <= 0:
71
+ return 1.0
72
+
73
+ if self.model == self.EXPONENTIAL:
74
+ # Exponential decay with half-life
75
+ decay_constant = math.log(2) / self.half_life_days
76
+ return math.exp(-decay_constant * age_days)
77
+
78
+ elif self.model == self.LINEAR:
79
+ # Linear decay over 2x half-life
80
+ max_age = self.half_life_days * 2
81
+ return max(0.0, 1.0 - (age_days / max_age))
82
+
83
+ elif self.model == self.STEP:
84
+ # Step function at half-life
85
+ if age_days < self.half_life_days:
86
+ return 1.0
87
+ elif age_days < self.half_life_days * 2:
88
+ return 0.5
89
+ else:
90
+ return 0.2
91
+
92
+ return 1.0
93
+
94
+ def days_until_threshold(self, current_score: float, threshold: float) -> Optional[float]:
95
+ """Calculate days until score drops below threshold."""
96
+ if current_score <= threshold:
97
+ return 0.0
98
+
99
+ if self.model == self.EXPONENTIAL:
100
+ decay_constant = math.log(2) / self.half_life_days
101
+ return math.log(current_score / threshold) / decay_constant
102
+
103
+ elif self.model == self.LINEAR:
104
+ max_age = self.half_life_days * 2
105
+ return max_age * (current_score - threshold)
106
+
107
+ return None
108
+
109
+
110
+ class SourceTracker:
111
+ """Tracks source reliability."""
112
+
113
+ def __init__(self, mem_dir: Path):
114
+ self.mem_dir = Path(mem_dir)
115
+ self.sources_file = self.mem_dir / "sources.json"
116
+ self._sources: Dict[str, Dict[str, Any]] = {}
117
+ self._load()
118
+
119
+ def _load(self) -> None:
120
+ """Load sources from disk."""
121
+ if self.sources_file.exists():
122
+ try:
123
+ data = json.loads(self.sources_file.read_text())
124
+ self._sources = data.get("sources", {})
125
+ except Exception:
126
+ pass
127
+
128
+ def _save(self) -> None:
129
+ """Save sources to disk."""
130
+ self.mem_dir.mkdir(parents=True, exist_ok=True)
131
+ self.sources_file.write_text(json.dumps({"sources": self._sources}, indent=2))
132
+
133
+ def register_source(
134
+ self,
135
+ source_id: str,
136
+ name: str,
137
+ initial_reliability: float = 0.8,
138
+ source_type: str = "agent",
139
+ ) -> Dict[str, Any]:
140
+ """Register a new source."""
141
+ self._sources[source_id] = {
142
+ "name": name,
143
+ "reliability": initial_reliability,
144
+ "type": source_type,
145
+ "contributions": 0,
146
+ "verified_count": 0,
147
+ "error_count": 0,
148
+ "registered_at": datetime.now(timezone.utc).isoformat(),
149
+ }
150
+ self._save()
151
+ return self._sources[source_id]
152
+
153
+ def get_reliability(self, source_id: str) -> float:
154
+ """Get reliability score for a source."""
155
+ source = self._sources.get(source_id)
156
+ if not source:
157
+ return 0.5 # Unknown source default
158
+ return source.get("reliability", 0.5)
159
+
160
+ def update_reliability(self, source_id: str, delta: float) -> float:
161
+ """Update source reliability by delta."""
162
+ source = self._sources.get(source_id)
163
+ if source:
164
+ new_reliability = max(0.1, min(1.0, source["reliability"] + delta))
165
+ source["reliability"] = new_reliability
166
+ self._save()
167
+ return new_reliability
168
+ return 0.5
169
+
170
+ def record_verification(self, source_id: str, verified: bool) -> None:
171
+ """Record a verification result for a source."""
172
+ source = self._sources.get(source_id)
173
+ if source:
174
+ if verified:
175
+ source["verified_count"] = source.get("verified_count", 0) + 1
176
+ self.update_reliability(source_id, 0.01)
177
+ else:
178
+ source["error_count"] = source.get("error_count", 0) + 1
179
+ self.update_reliability(source_id, -0.05)
180
+
181
+ def get_all_sources(self) -> Dict[str, Dict[str, Any]]:
182
+ """Get all registered sources."""
183
+ return self._sources.copy()
184
+
185
+
186
+ class ConfidenceCalculator:
187
+ """Calculates confidence scores for memories."""
188
+
189
+ def __init__(
190
+ self,
191
+ mem_dir: Path,
192
+ decay_model: Optional[DecayModel] = None,
193
+ ):
194
+ self.mem_dir = Path(mem_dir)
195
+ self.decay_model = decay_model or DecayModel()
196
+ self.source_tracker = SourceTracker(mem_dir)
197
+ self.scores_file = self.mem_dir / "confidence_scores.json"
198
+ self._scores: Dict[str, Dict[str, Any]] = {}
199
+ self._load()
200
+
201
+ def _load(self) -> None:
202
+ """Load scores from disk."""
203
+ if self.scores_file.exists():
204
+ try:
205
+ data = json.loads(self.scores_file.read_text())
206
+ self._scores = data.get("scores", {})
207
+ except Exception:
208
+ pass
209
+
210
+ def _save(self) -> None:
211
+ """Save scores to disk."""
212
+ self.mem_dir.mkdir(parents=True, exist_ok=True)
213
+ self.scores_file.write_text(json.dumps({"scores": self._scores}, indent=2))
214
+
215
+ def calculate_score(
216
+ self,
217
+ path: str,
218
+ source_id: Optional[str] = None,
219
+ created_at: Optional[str] = None,
220
+ ) -> ConfidenceScore:
221
+ """Calculate confidence score for a memory."""
222
+ # Get source reliability
223
+ source_reliability = 0.8
224
+ if source_id:
225
+ source_reliability = self.source_tracker.get_reliability(source_id)
226
+
227
+ # Calculate age
228
+ age_days = 0.0
229
+ if created_at:
230
+ try:
231
+ created_dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
232
+ age_days = (datetime.now(timezone.utc) - created_dt).total_seconds() / 86400
233
+ except Exception:
234
+ pass
235
+
236
+ # Get existing score data for corroboration/contradiction
237
+ existing = self._scores.get(path, {})
238
+ corroboration_count = existing.get("corroboration_count", 0)
239
+ contradiction_count = existing.get("contradiction_count", 0)
240
+ access_frequency = existing.get("access_count", 0)
241
+
242
+ factors = ConfidenceFactors(
243
+ source_reliability=source_reliability,
244
+ corroboration_count=corroboration_count,
245
+ age_days=age_days,
246
+ access_frequency=access_frequency,
247
+ contradiction_count=contradiction_count,
248
+ )
249
+
250
+ # Calculate base score
251
+ base_score = source_reliability
252
+
253
+ # Apply corroboration boost
254
+ corroboration_boost = min(0.2, corroboration_count * 0.05)
255
+ base_score = min(1.0, base_score + corroboration_boost)
256
+
257
+ # Apply contradiction penalty
258
+ contradiction_penalty = min(0.3, contradiction_count * 0.1)
259
+ base_score = max(0.0, base_score - contradiction_penalty)
260
+
261
+ # Apply time decay
262
+ decay_factor = self.decay_model.calculate_decay(age_days)
263
+ final_score = base_score * decay_factor
264
+
265
+ score = ConfidenceScore(
266
+ score=final_score,
267
+ factors=factors,
268
+ decay_rate=math.log(2) / self.decay_model.half_life_days,
269
+ computed_at=datetime.now(timezone.utc).isoformat(),
270
+ )
271
+
272
+ # Store score
273
+ self._scores[path] = {
274
+ **factors.__dict__,
275
+ "score": final_score,
276
+ "computed_at": score.computed_at,
277
+ }
278
+ self._save()
279
+
280
+ return score
281
+
282
+ def get_score(self, path: str) -> Optional[ConfidenceScore]:
283
+ """Get stored confidence score."""
284
+ stored = self._scores.get(path)
285
+ if not stored:
286
+ return None
287
+
288
+ factors = ConfidenceFactors(
289
+ source_reliability=stored.get("source_reliability", 0.8),
290
+ corroboration_count=stored.get("corroboration_count", 0),
291
+ age_days=stored.get("age_days", 0),
292
+ access_frequency=stored.get("access_frequency", 0),
293
+ contradiction_count=stored.get("contradiction_count", 0),
294
+ )
295
+
296
+ return ConfidenceScore(
297
+ score=stored.get("score", 0.5),
298
+ factors=factors,
299
+ decay_rate=stored.get("decay_rate", 0.023),
300
+ computed_at=stored.get("computed_at", ""),
301
+ )
302
+
303
+ def add_corroboration(self, path: str) -> None:
304
+ """Add corroborating evidence for a memory."""
305
+ if path in self._scores:
306
+ self._scores[path]["corroboration_count"] = (
307
+ self._scores[path].get("corroboration_count", 0) + 1
308
+ )
309
+ self._save()
310
+
311
+ def add_contradiction(self, path: str) -> None:
312
+ """Add contradicting evidence for a memory."""
313
+ if path in self._scores:
314
+ self._scores[path]["contradiction_count"] = (
315
+ self._scores[path].get("contradiction_count", 0) + 1
316
+ )
317
+ self._save()
318
+
319
+ def record_access(self, path: str) -> None:
320
+ """Record an access to a memory."""
321
+ if path not in self._scores:
322
+ self._scores[path] = {}
323
+ self._scores[path]["access_count"] = self._scores[path].get("access_count", 0) + 1
324
+ self._save()
325
+
326
+ def get_low_confidence_memories(self, threshold: float = 0.5) -> List[Dict[str, Any]]:
327
+ """Get memories with low confidence scores."""
328
+ low_confidence = []
329
+ for path, data in self._scores.items():
330
+ if data.get("score", 1.0) < threshold:
331
+ low_confidence.append(
332
+ {
333
+ "path": path,
334
+ "score": data.get("score"),
335
+ "age_days": data.get("age_days", 0),
336
+ }
337
+ )
338
+
339
+ return sorted(low_confidence, key=lambda x: x["score"])
340
+
341
+ def get_expiring_soon(self, days: int = 7, threshold: float = 0.5) -> List[Dict[str, Any]]:
342
+ """Get memories that will fall below threshold soon."""
343
+ expiring = []
344
+ for path, data in self._scores.items():
345
+ current_score = data.get("score", 1.0)
346
+ if current_score > threshold:
347
+ days_until = self.decay_model.days_until_threshold(current_score, threshold)
348
+ if days_until and days_until <= days:
349
+ expiring.append(
350
+ {
351
+ "path": path,
352
+ "current_score": current_score,
353
+ "days_until_threshold": round(days_until, 1),
354
+ }
355
+ )
356
+
357
+ return sorted(expiring, key=lambda x: x["days_until_threshold"])
358
+
359
+
360
+ # --- Dashboard Helper ---
361
+
362
+
363
+ def get_confidence_dashboard(mem_dir: Path) -> Dict[str, Any]:
364
+ """Get data for confidence scoring dashboard."""
365
+ calculator = ConfidenceCalculator(mem_dir)
366
+ source_tracker = SourceTracker(mem_dir)
367
+
368
+ low_confidence = calculator.get_low_confidence_memories(threshold=0.5)
369
+ expiring = calculator.get_expiring_soon(days=7, threshold=0.5)
370
+ sources = source_tracker.get_all_sources()
371
+
372
+ return {
373
+ "low_confidence_count": len(low_confidence),
374
+ "low_confidence_memories": low_confidence[:10],
375
+ "expiring_soon_count": len(expiring),
376
+ "expiring_soon": expiring[:10],
377
+ "sources": list(sources.values()),
378
+ "source_count": len(sources),
379
+ }