agmem 0.2.0__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.
- {agmem-0.2.0.dist-info → agmem-0.3.0.dist-info}/METADATA +338 -26
- {agmem-0.2.0.dist-info → agmem-0.3.0.dist-info}/RECORD +32 -16
- memvcs/__init__.py +1 -1
- memvcs/cli.py +1 -1
- memvcs/coordinator/server.py +18 -2
- memvcs/core/agents.py +411 -0
- memvcs/core/archaeology.py +410 -0
- memvcs/core/collaboration.py +435 -0
- memvcs/core/compliance.py +427 -0
- memvcs/core/compression_metrics.py +248 -0
- memvcs/core/confidence.py +379 -0
- memvcs/core/daemon.py +735 -0
- memvcs/core/delta.py +45 -23
- memvcs/core/distiller.py +3 -12
- memvcs/core/fast_similarity.py +404 -0
- memvcs/core/federated.py +13 -2
- memvcs/core/gardener.py +8 -68
- memvcs/core/pack.py +1 -1
- memvcs/core/privacy_validator.py +187 -0
- memvcs/core/private_search.py +327 -0
- memvcs/core/protocol_builder.py +198 -0
- memvcs/core/search_index.py +538 -0
- memvcs/core/semantic_graph.py +388 -0
- memvcs/core/session.py +520 -0
- memvcs/core/timetravel.py +430 -0
- memvcs/integrations/mcp_server.py +775 -4
- memvcs/integrations/web_ui/server.py +424 -0
- memvcs/integrations/web_ui/websocket.py +223 -0
- {agmem-0.2.0.dist-info → agmem-0.3.0.dist-info}/WHEEL +0 -0
- {agmem-0.2.0.dist-info → agmem-0.3.0.dist-info}/entry_points.txt +0 -0
- {agmem-0.2.0.dist-info → agmem-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.2.0.dist-info → agmem-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
}
|