Rohan03 commited on
Commit
9b02e81
·
verified ·
1 Parent(s): 7498b52

V2 merge: purpose_agent/memory.py

Browse files
Files changed (1) hide show
  1. purpose_agent/memory.py +301 -0
purpose_agent/memory.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ memory.py — Typed, versioned, scoped, reversible memory system.
3
+
4
+ Replaces the flat heuristic library with a structured memory store where
5
+ every memory has a kind, status, scope, trust score, and full provenance.
6
+
7
+ Memory lifecycle:
8
+ candidate → quarantined (immune scan) → promoted (replay-tested) → archived
9
+ → rejected (if scan/test fails)
10
+
11
+ Memory kinds:
12
+ purpose_contract — the user's stated goal and constraints
13
+ user_preference — learned user-specific preferences ("always cite sources")
14
+ skill_card — reusable procedure extracted from successful trajectories
15
+ episodic_case — a specific (state, action, outcome) triple worth remembering
16
+ failure_pattern — a pattern that led to failure (negative heuristic)
17
+ critic_calibration — learned adjustments to the Purpose Function's scoring
18
+ tool_policy — usage constraints and tips for specific tools
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import logging
24
+ import math
25
+ import time
26
+ import uuid
27
+ from dataclasses import dataclass, field
28
+ from enum import Enum
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ from purpose_agent.v2_types import MemoryScope
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class MemoryKind(Enum):
38
+ PURPOSE_CONTRACT = "purpose_contract"
39
+ USER_PREFERENCE = "user_preference"
40
+ SKILL_CARD = "skill_card"
41
+ EPISODIC_CASE = "episodic_case"
42
+ FAILURE_PATTERN = "failure_pattern"
43
+ CRITIC_CALIBRATION = "critic_calibration"
44
+ TOOL_POLICY = "tool_policy"
45
+
46
+
47
+ class MemoryStatus(Enum):
48
+ CANDIDATE = "candidate"
49
+ QUARANTINED = "quarantined"
50
+ PROMOTED = "promoted"
51
+ REJECTED = "rejected"
52
+ ARCHIVED = "archived"
53
+
54
+
55
+ @dataclass
56
+ class MemoryCard:
57
+ """
58
+ A single unit of agent memory. Every field is tracked for provenance.
59
+ """
60
+ id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
61
+ kind: MemoryKind = MemoryKind.SKILL_CARD
62
+ status: MemoryStatus = MemoryStatus.CANDIDATE
63
+ scope: MemoryScope = field(default_factory=MemoryScope)
64
+
65
+ # Content
66
+ content: str = "" # The actual knowledge (human readable)
67
+ pattern: str = "" # When does this apply?
68
+ strategy: str = "" # What to do?
69
+ steps: list[str] = field(default_factory=list)
70
+
71
+ # Trust & utility
72
+ trust_score: float = 0.5 # 0=untrusted, 1=fully trusted
73
+ utility_score: float = 0.5 # how useful is this when retrieved?
74
+ times_retrieved: int = 0
75
+ times_helped: int = 0
76
+ times_hurt: int = 0
77
+
78
+ # Provenance
79
+ source_trace_id: str = ""
80
+ source_step: int = 0
81
+ created_at: float = field(default_factory=time.time)
82
+ created_by: str = "" # agent name or "human" or "system"
83
+ version: int = 1
84
+ parent_id: str = "" # if this is a revision of another memory
85
+ rejection_reason: str = ""
86
+ immune_scan_result: dict[str, Any] = field(default_factory=dict)
87
+
88
+ # Embedding for retrieval
89
+ embedding: list[float] | None = None
90
+
91
+ def update_utility(self, helped: bool, alpha: float = 0.1) -> None:
92
+ """Monte Carlo utility update: U_new = U + α(reward - U)."""
93
+ self.times_retrieved += 1
94
+ if helped:
95
+ self.times_helped += 1
96
+ self.utility_score += alpha * (1.0 - self.utility_score)
97
+ else:
98
+ self.times_hurt += 1
99
+ self.utility_score += alpha * (0.0 - self.utility_score)
100
+ self.utility_score = max(0.0, min(1.0, self.utility_score))
101
+
102
+ @property
103
+ def empirical_help_rate(self) -> float:
104
+ if self.times_retrieved == 0:
105
+ return 0.5
106
+ return self.times_helped / self.times_retrieved
107
+
108
+ def to_dict(self) -> dict[str, Any]:
109
+ return {
110
+ "id": self.id,
111
+ "kind": self.kind.value,
112
+ "status": self.status.value,
113
+ "scope": {
114
+ "agent_roles": self.scope.agent_roles,
115
+ "tool_names": self.scope.tool_names,
116
+ "task_categories": self.scope.task_categories,
117
+ "user_id": self.scope.user_id,
118
+ },
119
+ "content": self.content,
120
+ "pattern": self.pattern,
121
+ "strategy": self.strategy,
122
+ "steps": self.steps,
123
+ "trust_score": self.trust_score,
124
+ "utility_score": self.utility_score,
125
+ "times_retrieved": self.times_retrieved,
126
+ "times_helped": self.times_helped,
127
+ "times_hurt": self.times_hurt,
128
+ "source_trace_id": self.source_trace_id,
129
+ "created_at": self.created_at,
130
+ "created_by": self.created_by,
131
+ "version": self.version,
132
+ "parent_id": self.parent_id,
133
+ "status_detail": self.rejection_reason,
134
+ "immune_scan": self.immune_scan_result,
135
+ }
136
+
137
+ @classmethod
138
+ def from_dict(cls, d: dict) -> "MemoryCard":
139
+ scope_d = d.get("scope", {})
140
+ return cls(
141
+ id=d.get("id", uuid.uuid4().hex[:12]),
142
+ kind=MemoryKind(d.get("kind", "skill_card")),
143
+ status=MemoryStatus(d.get("status", "candidate")),
144
+ scope=MemoryScope(
145
+ agent_roles=scope_d.get("agent_roles", []),
146
+ tool_names=scope_d.get("tool_names", []),
147
+ task_categories=scope_d.get("task_categories", []),
148
+ user_id=scope_d.get("user_id", ""),
149
+ ),
150
+ content=d.get("content", ""),
151
+ pattern=d.get("pattern", ""),
152
+ strategy=d.get("strategy", ""),
153
+ steps=d.get("steps", []),
154
+ trust_score=d.get("trust_score", 0.5),
155
+ utility_score=d.get("utility_score", 0.5),
156
+ times_retrieved=d.get("times_retrieved", 0),
157
+ times_helped=d.get("times_helped", 0),
158
+ times_hurt=d.get("times_hurt", 0),
159
+ source_trace_id=d.get("source_trace_id", ""),
160
+ created_at=d.get("created_at", time.time()),
161
+ created_by=d.get("created_by", ""),
162
+ version=d.get("version", 1),
163
+ parent_id=d.get("parent_id", ""),
164
+ rejection_reason=d.get("status_detail", ""),
165
+ immune_scan_result=d.get("immune_scan", {}),
166
+ )
167
+
168
+
169
+ class MemoryStore:
170
+ """
171
+ Persistent, queryable store of MemoryCards.
172
+
173
+ Supports:
174
+ - Add/retrieve/update/reject memories
175
+ - Filter by kind, status, scope
176
+ - Ranked retrieval (relevance × trust × utility)
177
+ - Persistence to JSON
178
+ """
179
+
180
+ def __init__(self, persistence_path: str | None = None):
181
+ self._cards: dict[str, MemoryCard] = {}
182
+ self._path = Path(persistence_path) if persistence_path else None
183
+ if self._path and self._path.exists():
184
+ self._load()
185
+
186
+ def add(self, card: MemoryCard) -> MemoryCard:
187
+ """Add a memory card. Returns the card (with id assigned)."""
188
+ self._cards[card.id] = card
189
+ self._save()
190
+ return card
191
+
192
+ def get(self, card_id: str) -> MemoryCard | None:
193
+ return self._cards.get(card_id)
194
+
195
+ def update_status(self, card_id: str, status: MemoryStatus, reason: str = "") -> None:
196
+ card = self._cards.get(card_id)
197
+ if card:
198
+ card.status = status
199
+ if reason:
200
+ card.rejection_reason = reason
201
+ self._save()
202
+
203
+ def retrieve(
204
+ self,
205
+ query_text: str = "",
206
+ scope: MemoryScope | None = None,
207
+ kinds: list[MemoryKind] | None = None,
208
+ statuses: list[MemoryStatus] | None = None,
209
+ top_k: int = 10,
210
+ ) -> list[MemoryCard]:
211
+ """
212
+ Retrieve memories ranked by composite score.
213
+ Default: only promoted memories.
214
+ """
215
+ statuses = statuses or [MemoryStatus.PROMOTED]
216
+ candidates = []
217
+ query_emb = self._embed(query_text) if query_text else None
218
+
219
+ for card in self._cards.values():
220
+ if card.status not in statuses:
221
+ continue
222
+ if kinds and card.kind not in kinds:
223
+ continue
224
+ if scope and not card.scope.matches(scope):
225
+ continue
226
+
227
+ # Composite score: relevance × trust × utility
228
+ relevance = 0.5
229
+ if query_emb and card.embedding:
230
+ relevance = self._cosine(query_emb, card.embedding)
231
+ elif query_emb:
232
+ card.embedding = self._embed(card.content or card.pattern)
233
+ relevance = self._cosine(query_emb, card.embedding)
234
+
235
+ score = 0.4 * relevance + 0.3 * card.trust_score + 0.3 * card.utility_score
236
+ candidates.append((score, card))
237
+
238
+ candidates.sort(key=lambda x: -x[0])
239
+ return [c for _, c in candidates[:top_k]]
240
+
241
+ def get_by_status(self, status: MemoryStatus) -> list[MemoryCard]:
242
+ return [c for c in self._cards.values() if c.status == status]
243
+
244
+ def get_all(self) -> list[MemoryCard]:
245
+ return list(self._cards.values())
246
+
247
+ @property
248
+ def size(self) -> int:
249
+ return len(self._cards)
250
+
251
+ def stats(self) -> dict[str, Any]:
252
+ by_status = {}
253
+ by_kind = {}
254
+ for c in self._cards.values():
255
+ by_status[c.status.value] = by_status.get(c.status.value, 0) + 1
256
+ by_kind[c.kind.value] = by_kind.get(c.kind.value, 0) + 1
257
+ return {"total": self.size, "by_status": by_status, "by_kind": by_kind}
258
+
259
+ # --- Persistence ---
260
+
261
+ def _save(self) -> None:
262
+ if not self._path:
263
+ return
264
+ self._path.parent.mkdir(parents=True, exist_ok=True)
265
+ data = [c.to_dict() for c in self._cards.values()]
266
+ with open(self._path, "w") as f:
267
+ json.dump(data, f, indent=2, default=str)
268
+
269
+ def _load(self) -> None:
270
+ if not self._path or not self._path.exists():
271
+ return
272
+ try:
273
+ with open(self._path) as f:
274
+ data = json.load(f)
275
+ for d in data:
276
+ card = MemoryCard.from_dict(d)
277
+ self._cards[card.id] = card
278
+ logger.info(f"MemoryStore: loaded {len(self._cards)} cards")
279
+ except Exception as e:
280
+ logger.error(f"MemoryStore: load failed: {e}")
281
+
282
+ # --- Embedding (lightweight, swappable) ---
283
+
284
+ @staticmethod
285
+ def _embed(text: str) -> list[float]:
286
+ dim = 128
287
+ vec = [0.0] * dim
288
+ for i in range(len(text) - 2):
289
+ h = hash(text[i:i+3].lower()) % dim
290
+ vec[h] += 1.0
291
+ mag = math.sqrt(sum(x*x for x in vec))
292
+ return [x/mag for x in vec] if mag > 0 else vec
293
+
294
+ @staticmethod
295
+ def _cosine(a: list[float], b: list[float]) -> float:
296
+ if not a or not b or len(a) != len(b):
297
+ return 0.0
298
+ dot = sum(x*y for x, y in zip(a, b))
299
+ ma = math.sqrt(sum(x*x for x in a))
300
+ mb = math.sqrt(sum(x*x for x in b))
301
+ return dot / (ma * mb) if ma > 0 and mb > 0 else 0.0