plug-play-memory 0.2.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.
- plug_play_memory/__init__.py +23 -0
- plug_play_memory/audit.py +209 -0
- plug_play_memory/auth.py +155 -0
- plug_play_memory/cli.py +413 -0
- plug_play_memory/consolidation.py +172 -0
- plug_play_memory/emitter.py +337 -0
- plug_play_memory/events.py +36 -0
- plug_play_memory/judge.py +461 -0
- plug_play_memory/policies/builtin/_base.yaml +18 -0
- plug_play_memory/policies/builtin/coding.yaml +53 -0
- plug_play_memory/policies/builtin/financial.yaml +50 -0
- plug_play_memory/policies/builtin/study.yaml +50 -0
- plug_play_memory/policy.py +353 -0
- plug_play_memory/server.py +374 -0
- plug_play_memory/storage.py +495 -0
- plug_play_memory-0.2.0.dist-info/METADATA +487 -0
- plug_play_memory-0.2.0.dist-info/RECORD +21 -0
- plug_play_memory-0.2.0.dist-info/WHEEL +5 -0
- plug_play_memory-0.2.0.dist-info/entry_points.txt +2 -0
- plug_play_memory-0.2.0.dist-info/licenses/LICENSE +201 -0
- plug_play_memory-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event emitter — the SDK agents call to record meaningful actions.
|
|
3
|
+
|
|
4
|
+
Each emit() goes through:
|
|
5
|
+
1. Judge evaluation (rules + heuristics + salience)
|
|
6
|
+
2. Policy gate (does this agent have permission to write_memory from this source?)
|
|
7
|
+
3. Deduplication against existing memory
|
|
8
|
+
4. MemoryStore.remember() with salience + tagged metadata
|
|
9
|
+
5. Audit log entry
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .events import AgentEvent
|
|
17
|
+
from .judge import Judge
|
|
18
|
+
from .policy import PolicyEngine
|
|
19
|
+
from .storage import MemoryStore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EventEmitter:
|
|
23
|
+
"""Agents call helpers on this; the system handles judging + policy + storage."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
store: MemoryStore,
|
|
28
|
+
policy: PolicyEngine,
|
|
29
|
+
judge: Judge,
|
|
30
|
+
agent_id: str,
|
|
31
|
+
agent_type: str,
|
|
32
|
+
):
|
|
33
|
+
self.store = store
|
|
34
|
+
self.policy = policy
|
|
35
|
+
self.judge = judge
|
|
36
|
+
self.agent_id = agent_id
|
|
37
|
+
self.agent_type = agent_type
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# Public API — agents call these
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
def emit(
|
|
44
|
+
self,
|
|
45
|
+
action: str,
|
|
46
|
+
inputs: dict[str, Any] | None = None,
|
|
47
|
+
outputs: dict[str, Any] | None = None,
|
|
48
|
+
outcome: str = "success",
|
|
49
|
+
raw: str = "",
|
|
50
|
+
):
|
|
51
|
+
"""Generic emit. Use this for actions without a dedicated helper."""
|
|
52
|
+
return self._emit(action, inputs or {}, outputs or {}, outcome, raw)
|
|
53
|
+
|
|
54
|
+
# ---- CODING helpers ----
|
|
55
|
+
|
|
56
|
+
def file_edited(self, path: str, diff_summary: str = "", lines_changed: int = 0, success: bool = True):
|
|
57
|
+
return self._emit(
|
|
58
|
+
action="file_edited",
|
|
59
|
+
inputs={"path": path, "lines_changed": lines_changed},
|
|
60
|
+
outputs={"diff_summary": diff_summary},
|
|
61
|
+
outcome="success" if success else "failure",
|
|
62
|
+
raw=diff_summary,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def git_pushed(self, branch: str, sha: str, repo: str, message: str = ""):
|
|
66
|
+
return self._emit(
|
|
67
|
+
action="git_pushed",
|
|
68
|
+
inputs={"branch": branch, "repo": repo},
|
|
69
|
+
outputs={"sha": sha, "message": message},
|
|
70
|
+
outcome="success",
|
|
71
|
+
raw=message,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def test_run(self, suite: str, passed: int, failed: int, failures: list[str] | None = None):
|
|
75
|
+
outcome = "success" if failed == 0 else ("partial" if passed > 0 else "failure")
|
|
76
|
+
return self._emit(
|
|
77
|
+
action="test_run",
|
|
78
|
+
inputs={"suite": suite, "passed": passed, "failed": failed},
|
|
79
|
+
outputs={"failures": failures or []},
|
|
80
|
+
outcome=outcome,
|
|
81
|
+
raw="; ".join(failures or []),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def decision_made(self, topic: str, choice: str, reason: str = ""):
|
|
85
|
+
return self._emit(
|
|
86
|
+
action="decision_made",
|
|
87
|
+
inputs={"topic": topic},
|
|
88
|
+
outputs={"choice": choice, "reason": reason},
|
|
89
|
+
outcome="success",
|
|
90
|
+
raw=f"Decided {choice} for {topic}. Reason: {reason}",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def error_encountered(self, message: str, context: dict[str, Any] | None = None):
|
|
94
|
+
return self._emit(
|
|
95
|
+
action="error_encountered",
|
|
96
|
+
inputs=context or {},
|
|
97
|
+
outputs={"error": message},
|
|
98
|
+
outcome="failure",
|
|
99
|
+
raw=message,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def deployed(self, target: str, version: str, success: bool, notes: str = ""):
|
|
103
|
+
return self._emit(
|
|
104
|
+
action="deployed",
|
|
105
|
+
inputs={"target": target, "version": version},
|
|
106
|
+
outputs={"success": success, "notes": notes},
|
|
107
|
+
outcome="success" if success else "failure",
|
|
108
|
+
raw=notes,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def migration_run(self, name: str, success: bool, error: str = ""):
|
|
112
|
+
return self._emit(
|
|
113
|
+
action="migration_run",
|
|
114
|
+
inputs={"migration": name},
|
|
115
|
+
outputs={"success": success, "error": error},
|
|
116
|
+
outcome="success" if success else "failure",
|
|
117
|
+
raw=error or f"Migration {name} completed",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# ---- STUDY helpers ----
|
|
121
|
+
|
|
122
|
+
def fact_observed(self, field_name: str, value: str, raw: str = ""):
|
|
123
|
+
# Use field_name as the dedup key so repeated observations
|
|
124
|
+
# of the same field update rather than accumulate.
|
|
125
|
+
from .judge import JudgeDecision
|
|
126
|
+
decision = self.judge.evaluate(AgentEvent(
|
|
127
|
+
agent_id=self.agent_id, agent_type=self.agent_type,
|
|
128
|
+
action="fact_observed", outcome="success",
|
|
129
|
+
inputs={"field": field_name},
|
|
130
|
+
outputs={"value": value},
|
|
131
|
+
raw=raw or f"{field_name}: {value}",
|
|
132
|
+
))
|
|
133
|
+
if decision.should_save and not decision.key:
|
|
134
|
+
decision.key = field_name
|
|
135
|
+
# Bypass _emit's re-judging and write directly
|
|
136
|
+
if not decision.should_save:
|
|
137
|
+
self.store.audit.log(
|
|
138
|
+
self.agent_id, "event_discarded",
|
|
139
|
+
{"action": "fact_observed", "reason": decision.discard_reason},
|
|
140
|
+
)
|
|
141
|
+
return None
|
|
142
|
+
# Apply same policy + dedup + write pipeline as _emit
|
|
143
|
+
return self._write_decision(decision, "fact_observed", {"field": field_name},
|
|
144
|
+
{"value": value}, "success", raw or f"{field_name}: {value}")
|
|
145
|
+
|
|
146
|
+
def question_asked(self, topic: str, question: str, difficulty: str = "normal"):
|
|
147
|
+
return self._emit(
|
|
148
|
+
action="question_asked",
|
|
149
|
+
inputs={"topic": topic, "difficulty": difficulty},
|
|
150
|
+
outputs={"question": question},
|
|
151
|
+
outcome="success",
|
|
152
|
+
raw=question,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def answer_attempted(
|
|
156
|
+
self,
|
|
157
|
+
topic: str,
|
|
158
|
+
correct: bool,
|
|
159
|
+
misconception: str = "",
|
|
160
|
+
question_id: str = "",
|
|
161
|
+
student_answer: str = "",
|
|
162
|
+
):
|
|
163
|
+
outcome = "success" if correct else "failure"
|
|
164
|
+
return self._emit(
|
|
165
|
+
action="answer_attempted",
|
|
166
|
+
inputs={"topic": topic, "question_id": question_id},
|
|
167
|
+
outputs={
|
|
168
|
+
"correct": correct,
|
|
169
|
+
"misconception": misconception,
|
|
170
|
+
"student_answer": student_answer,
|
|
171
|
+
},
|
|
172
|
+
outcome=outcome,
|
|
173
|
+
raw=f"Student answer: {student_answer}. Misconception: {misconception}" if misconception else f"Student answer: {student_answer}",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def concept_mastered(self, topic: str, mastery_level: float, attempts: int = 1):
|
|
177
|
+
return self._emit(
|
|
178
|
+
action="concept_mastered",
|
|
179
|
+
outcome="success",
|
|
180
|
+
inputs={"topic": topic, "attempts": attempts},
|
|
181
|
+
outputs={"mastery_level": mastery_level},
|
|
182
|
+
raw=f"Mastered {topic} at {mastery_level:.0%} after {attempts} attempts",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def knowledge_gap_found(self, topic: str, gap: str, severity: str = "high"):
|
|
186
|
+
return self._emit(
|
|
187
|
+
action="knowledge_gap_found",
|
|
188
|
+
outcome="failure",
|
|
189
|
+
inputs={"topic": topic, "severity": severity},
|
|
190
|
+
outputs={"gap": gap},
|
|
191
|
+
raw=gap,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def goal_set(self, description: str, deadline: str = "", priority: str = "normal"):
|
|
195
|
+
return self._emit(
|
|
196
|
+
action="goal_set",
|
|
197
|
+
outcome="success",
|
|
198
|
+
inputs={"deadline": deadline, "priority": priority},
|
|
199
|
+
outputs={"description": description},
|
|
200
|
+
raw=description,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# ---- FINANCIAL helpers ----
|
|
204
|
+
|
|
205
|
+
def make_payment(self, amount: float, recipient: str, success: bool, notes: str = ""):
|
|
206
|
+
return self._emit(
|
|
207
|
+
action="make_payment",
|
|
208
|
+
inputs={"amount": amount, "recipient": recipient},
|
|
209
|
+
outputs={"success": success, "notes": notes},
|
|
210
|
+
outcome="success" if success else "failure",
|
|
211
|
+
raw=notes or f"Payment of {amount} to {recipient}",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def set_budget_limit(self, category: str, limit: float, period: str = "monthly"):
|
|
215
|
+
return self._emit(
|
|
216
|
+
action="set_budget_limit",
|
|
217
|
+
outcome="success",
|
|
218
|
+
inputs={"category": category, "limit": limit, "period": period},
|
|
219
|
+
outputs={},
|
|
220
|
+
raw=f"Budget {category}: {limit} per {period}",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# ------------------------------------------------------------------
|
|
224
|
+
# Core emit logic — judge → policy → dedup → store
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def _emit(
|
|
228
|
+
self,
|
|
229
|
+
action: str,
|
|
230
|
+
inputs: dict[str, Any],
|
|
231
|
+
outputs: dict[str, Any],
|
|
232
|
+
outcome: str,
|
|
233
|
+
raw: str,
|
|
234
|
+
):
|
|
235
|
+
event = AgentEvent(
|
|
236
|
+
agent_id=self.agent_id,
|
|
237
|
+
agent_type=self.agent_type,
|
|
238
|
+
action=action,
|
|
239
|
+
inputs=inputs,
|
|
240
|
+
outputs=outputs,
|
|
241
|
+
outcome=outcome,
|
|
242
|
+
raw=raw,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Step 1: judge
|
|
246
|
+
decision = self.judge.evaluate(event)
|
|
247
|
+
|
|
248
|
+
if not decision.should_save:
|
|
249
|
+
self.store.audit.log(
|
|
250
|
+
self.agent_id, "event_discarded",
|
|
251
|
+
{
|
|
252
|
+
"action": action, "outcome": outcome,
|
|
253
|
+
"reason": decision.discard_reason,
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
return self._write_decision(decision, action, inputs, outputs, outcome, raw)
|
|
259
|
+
|
|
260
|
+
def _write_decision(
|
|
261
|
+
self,
|
|
262
|
+
decision,
|
|
263
|
+
action: str,
|
|
264
|
+
inputs: dict[str, Any],
|
|
265
|
+
outputs: dict[str, Any],
|
|
266
|
+
outcome: str,
|
|
267
|
+
raw: str,
|
|
268
|
+
):
|
|
269
|
+
"""Shared write path: policy gate → dedup → store → audit. Used by _emit and fact_observed."""
|
|
270
|
+
|
|
271
|
+
# Step 2: policy gate — agent must have write_memory permission for this category
|
|
272
|
+
# M5 fix: pass agent_type explicitly to avoid shared-state mutation
|
|
273
|
+
policy_decision = self.policy.check(
|
|
274
|
+
self.agent_id, "write_memory",
|
|
275
|
+
{
|
|
276
|
+
"source": "event_judge",
|
|
277
|
+
"action": action,
|
|
278
|
+
"category": decision.category,
|
|
279
|
+
"tier": decision.tier,
|
|
280
|
+
},
|
|
281
|
+
agent_type=self.agent_type,
|
|
282
|
+
)
|
|
283
|
+
# Log as permission_check so coverage_manifest picks it up
|
|
284
|
+
self.store.audit.log(
|
|
285
|
+
self.agent_id, "permission_check",
|
|
286
|
+
{
|
|
287
|
+
"action": "write_memory",
|
|
288
|
+
"context": {
|
|
289
|
+
"source": "event_judge",
|
|
290
|
+
"event_action": action,
|
|
291
|
+
"category": decision.category,
|
|
292
|
+
"tier": decision.tier,
|
|
293
|
+
},
|
|
294
|
+
"backend": "layered_yaml",
|
|
295
|
+
"reason": policy_decision.reason,
|
|
296
|
+
"layers": policy_decision.layers_evaluated,
|
|
297
|
+
},
|
|
298
|
+
allowed=policy_decision.allowed,
|
|
299
|
+
)
|
|
300
|
+
if not policy_decision.allowed:
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
# Step 3: dedup — skip if same key + same value already exists
|
|
304
|
+
if decision.key and self._is_duplicate(decision.key, decision.summary):
|
|
305
|
+
self.store.audit.log(
|
|
306
|
+
self.agent_id, "event_deduplicated",
|
|
307
|
+
{"action": action, "key": decision.key},
|
|
308
|
+
)
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
# Step 4: write with rich metadata
|
|
312
|
+
fact = self.store.remember(
|
|
313
|
+
agent_id=self.agent_id,
|
|
314
|
+
value=decision.summary,
|
|
315
|
+
tier=decision.tier,
|
|
316
|
+
key=decision.key,
|
|
317
|
+
importance=decision.importance,
|
|
318
|
+
salience_score=decision.salience_score,
|
|
319
|
+
tagged=decision.tagged,
|
|
320
|
+
metadata={
|
|
321
|
+
"source": "event_judge",
|
|
322
|
+
"agent_type": self.agent_type,
|
|
323
|
+
"action": action,
|
|
324
|
+
"category": decision.category,
|
|
325
|
+
"outcome": outcome,
|
|
326
|
+
"inputs": inputs,
|
|
327
|
+
"outputs": outputs,
|
|
328
|
+
},
|
|
329
|
+
)
|
|
330
|
+
return fact
|
|
331
|
+
|
|
332
|
+
def _is_duplicate(self, key: str, value: str) -> bool:
|
|
333
|
+
"""Check ALL tiers for duplicate (M3 fix — was core-only)."""
|
|
334
|
+
for f in self.store.all_facts(self.agent_id):
|
|
335
|
+
if f.key == key and f.value.lower() == value.lower():
|
|
336
|
+
return True
|
|
337
|
+
return False
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Structured events — the unit of memory input."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class AgentEvent:
|
|
13
|
+
"""A structured thing an agent did. This is what the judge evaluates."""
|
|
14
|
+
agent_id: str
|
|
15
|
+
agent_type: str # "coding" | "study" | "financial" | ...
|
|
16
|
+
action: str # "file_edited" | "git_pushed" | "answer_attempted" | ...
|
|
17
|
+
inputs: dict[str, Any] = field(default_factory=dict)
|
|
18
|
+
outputs: dict[str, Any] = field(default_factory=dict)
|
|
19
|
+
outcome: str = "success" # "success" | "failure" | "partial"
|
|
20
|
+
raw: str = ""
|
|
21
|
+
timestamp: float = field(default_factory=time.time)
|
|
22
|
+
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
23
|
+
|
|
24
|
+
def to_prompt(self) -> str:
|
|
25
|
+
lines = [
|
|
26
|
+
f"AGENT_TYPE: {self.agent_type}",
|
|
27
|
+
f"ACTION: {self.action}",
|
|
28
|
+
f"OUTCOME: {self.outcome}",
|
|
29
|
+
]
|
|
30
|
+
if self.inputs:
|
|
31
|
+
lines.append(f"INPUTS: {self.inputs}")
|
|
32
|
+
if self.outputs:
|
|
33
|
+
lines.append(f"OUTPUTS: {self.outputs}")
|
|
34
|
+
if self.raw:
|
|
35
|
+
lines.append(f"RAW: {self.raw[:300]}")
|
|
36
|
+
return "\n".join(lines)
|