memex-python 0.13.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.
- memex/__init__.py +336 -0
- memex/_time.py +26 -0
- memex/_uuid.py +62 -0
- memex/bulk.py +138 -0
- memex/commands.py +75 -0
- memex/envelope.py +69 -0
- memex/errors.py +51 -0
- memex/factories.py +97 -0
- memex/graph.py +30 -0
- memex/integrity.py +317 -0
- memex/intent.py +318 -0
- memex/models.py +271 -0
- memex/query.py +435 -0
- memex/reducer.py +151 -0
- memex/replay.py +144 -0
- memex/retrieval.py +266 -0
- memex/schemas.py +67 -0
- memex/serialization.py +47 -0
- memex/stats.py +71 -0
- memex/store.py +222 -0
- memex/task.py +361 -0
- memex/transplant.py +480 -0
- memex_python-0.13.0.dist-info/METADATA +150 -0
- memex_python-0.13.0.dist-info/RECORD +26 -0
- memex_python-0.13.0.dist-info/WHEEL +4 -0
- memex_python-0.13.0.dist-info/licenses/LICENSE +190 -0
memex/__init__.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""memex — a typed, immutable, provenance-tracked memory graph for AI agents.
|
|
2
|
+
|
|
3
|
+
Pydantic port of ``@ai2070/memex``. Public surface grows phase by phase; this
|
|
4
|
+
barrel re-exports everything implemented so far.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ._uuid import safe_extract_timestamp, uuid7
|
|
10
|
+
from .bulk import (
|
|
11
|
+
ItemTransform,
|
|
12
|
+
ScoreAdjustment,
|
|
13
|
+
apply_many,
|
|
14
|
+
bulk_adjust_scores,
|
|
15
|
+
decay_importance,
|
|
16
|
+
)
|
|
17
|
+
from .commands import (
|
|
18
|
+
EdgeCreate,
|
|
19
|
+
EdgeRetract,
|
|
20
|
+
EdgeUpdate,
|
|
21
|
+
MemoryCommand,
|
|
22
|
+
MemoryCommandAdapter,
|
|
23
|
+
MemoryCreate,
|
|
24
|
+
MemoryRetract,
|
|
25
|
+
MemoryUpdate,
|
|
26
|
+
)
|
|
27
|
+
from .envelope import wrap_edge_state_event, wrap_lifecycle_event, wrap_state_event
|
|
28
|
+
from .errors import (
|
|
29
|
+
DuplicateEdgeError,
|
|
30
|
+
DuplicateMemoryError,
|
|
31
|
+
EdgeNotFoundError,
|
|
32
|
+
InvalidTimestampError,
|
|
33
|
+
MemexError,
|
|
34
|
+
MemoryNotFoundError,
|
|
35
|
+
)
|
|
36
|
+
from .factories import create_edge, create_event_envelope, create_memory_item
|
|
37
|
+
from .graph import GraphState, clone_graph_state, create_graph_state
|
|
38
|
+
from .integrity import (
|
|
39
|
+
CascadeResult,
|
|
40
|
+
Contradiction,
|
|
41
|
+
StaleItem,
|
|
42
|
+
cascade_retract,
|
|
43
|
+
get_alias_group,
|
|
44
|
+
get_aliases,
|
|
45
|
+
get_contradictions,
|
|
46
|
+
get_dependents,
|
|
47
|
+
get_items_by_budget,
|
|
48
|
+
get_stale_items,
|
|
49
|
+
mark_alias,
|
|
50
|
+
mark_contradiction,
|
|
51
|
+
resolve_contradiction,
|
|
52
|
+
)
|
|
53
|
+
from .intent import (
|
|
54
|
+
DuplicateIntentError,
|
|
55
|
+
Intent,
|
|
56
|
+
IntentCommand,
|
|
57
|
+
IntentFilter,
|
|
58
|
+
IntentLifecycleEvent,
|
|
59
|
+
IntentNotFoundError,
|
|
60
|
+
IntentResult,
|
|
61
|
+
IntentState,
|
|
62
|
+
IntentStatus,
|
|
63
|
+
InvalidIntentTransitionError,
|
|
64
|
+
apply_intent_command,
|
|
65
|
+
create_intent,
|
|
66
|
+
create_intent_state,
|
|
67
|
+
get_child_intents,
|
|
68
|
+
get_intent_by_id,
|
|
69
|
+
get_intents,
|
|
70
|
+
)
|
|
71
|
+
from .models import (
|
|
72
|
+
CreatedFilter,
|
|
73
|
+
DecayConfig,
|
|
74
|
+
DecayFilter,
|
|
75
|
+
DecayInterval,
|
|
76
|
+
DecayType,
|
|
77
|
+
Edge,
|
|
78
|
+
EdgeFilter,
|
|
79
|
+
EventEnvelope,
|
|
80
|
+
KnownEdgeKind,
|
|
81
|
+
KnownMemoryKind,
|
|
82
|
+
KnownNamespace,
|
|
83
|
+
KnownSourceKind,
|
|
84
|
+
LifecycleEventType,
|
|
85
|
+
MemoryFilter,
|
|
86
|
+
MemoryItem,
|
|
87
|
+
MemoryLifecycleEvent,
|
|
88
|
+
ParentsFilter,
|
|
89
|
+
QueryOptions,
|
|
90
|
+
Range,
|
|
91
|
+
ScoredItem,
|
|
92
|
+
ScoreRanges,
|
|
93
|
+
ScoreWeights,
|
|
94
|
+
SortField,
|
|
95
|
+
SortOption,
|
|
96
|
+
)
|
|
97
|
+
from .query import (
|
|
98
|
+
ScoredQueryOptions,
|
|
99
|
+
compute_decay_multiplier,
|
|
100
|
+
compute_score,
|
|
101
|
+
extract_timestamp,
|
|
102
|
+
get_children,
|
|
103
|
+
get_edge_by_id,
|
|
104
|
+
get_edges,
|
|
105
|
+
get_item_by_id,
|
|
106
|
+
get_items,
|
|
107
|
+
get_parents,
|
|
108
|
+
get_related_items,
|
|
109
|
+
get_scored_items,
|
|
110
|
+
matches_filter,
|
|
111
|
+
)
|
|
112
|
+
from .reducer import CommandResult, apply_command, merge_edge, merge_item
|
|
113
|
+
from .replay import (
|
|
114
|
+
ReplayFailure,
|
|
115
|
+
ReplayResult,
|
|
116
|
+
replay_commands,
|
|
117
|
+
replay_from_envelopes,
|
|
118
|
+
)
|
|
119
|
+
from .retrieval import (
|
|
120
|
+
DiversityOptions,
|
|
121
|
+
SupportNode,
|
|
122
|
+
apply_diversity,
|
|
123
|
+
filter_contradictions,
|
|
124
|
+
get_support_set,
|
|
125
|
+
get_support_tree,
|
|
126
|
+
smart_retrieve,
|
|
127
|
+
surface_contradictions,
|
|
128
|
+
)
|
|
129
|
+
from .serialization import (
|
|
130
|
+
SerializedGraphState,
|
|
131
|
+
from_json,
|
|
132
|
+
parse,
|
|
133
|
+
stringify,
|
|
134
|
+
to_json,
|
|
135
|
+
)
|
|
136
|
+
from .stats import EdgeStats, GraphStats, ItemStats, get_stats
|
|
137
|
+
from .store import MemexStore
|
|
138
|
+
from .task import (
|
|
139
|
+
DuplicateTaskError,
|
|
140
|
+
InvalidTaskTransitionError,
|
|
141
|
+
Task,
|
|
142
|
+
TaskCommand,
|
|
143
|
+
TaskFilter,
|
|
144
|
+
TaskLifecycleEvent,
|
|
145
|
+
TaskNotFoundError,
|
|
146
|
+
TaskResult,
|
|
147
|
+
TaskState,
|
|
148
|
+
TaskStatus,
|
|
149
|
+
apply_task_command,
|
|
150
|
+
create_task,
|
|
151
|
+
create_task_state,
|
|
152
|
+
get_child_tasks,
|
|
153
|
+
get_task_by_id,
|
|
154
|
+
get_tasks,
|
|
155
|
+
get_tasks_by_intent,
|
|
156
|
+
)
|
|
157
|
+
from .transplant import (
|
|
158
|
+
ExportOptions,
|
|
159
|
+
ImportBucket,
|
|
160
|
+
ImportOptions,
|
|
161
|
+
ImportReport,
|
|
162
|
+
ImportResult,
|
|
163
|
+
MemexExport,
|
|
164
|
+
export_slice,
|
|
165
|
+
import_slice,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
__all__ = [
|
|
169
|
+
# uuid
|
|
170
|
+
"uuid7",
|
|
171
|
+
"safe_extract_timestamp",
|
|
172
|
+
# graph state
|
|
173
|
+
"GraphState",
|
|
174
|
+
"create_graph_state",
|
|
175
|
+
"clone_graph_state",
|
|
176
|
+
# reducer
|
|
177
|
+
"apply_command",
|
|
178
|
+
"CommandResult",
|
|
179
|
+
"merge_item",
|
|
180
|
+
"merge_edge",
|
|
181
|
+
# query
|
|
182
|
+
"get_items",
|
|
183
|
+
"get_scored_items",
|
|
184
|
+
"get_edges",
|
|
185
|
+
"get_item_by_id",
|
|
186
|
+
"get_edge_by_id",
|
|
187
|
+
"get_related_items",
|
|
188
|
+
"get_parents",
|
|
189
|
+
"get_children",
|
|
190
|
+
"matches_filter",
|
|
191
|
+
"compute_score",
|
|
192
|
+
"compute_decay_multiplier",
|
|
193
|
+
"extract_timestamp",
|
|
194
|
+
"ScoredQueryOptions",
|
|
195
|
+
# retrieval
|
|
196
|
+
"get_support_tree",
|
|
197
|
+
"get_support_set",
|
|
198
|
+
"filter_contradictions",
|
|
199
|
+
"surface_contradictions",
|
|
200
|
+
"apply_diversity",
|
|
201
|
+
"smart_retrieve",
|
|
202
|
+
"SupportNode",
|
|
203
|
+
"DiversityOptions",
|
|
204
|
+
# integrity
|
|
205
|
+
"get_contradictions",
|
|
206
|
+
"mark_contradiction",
|
|
207
|
+
"resolve_contradiction",
|
|
208
|
+
"get_stale_items",
|
|
209
|
+
"get_dependents",
|
|
210
|
+
"cascade_retract",
|
|
211
|
+
"mark_alias",
|
|
212
|
+
"get_aliases",
|
|
213
|
+
"get_alias_group",
|
|
214
|
+
"get_items_by_budget",
|
|
215
|
+
"Contradiction",
|
|
216
|
+
"StaleItem",
|
|
217
|
+
"CascadeResult",
|
|
218
|
+
# bulk
|
|
219
|
+
"apply_many",
|
|
220
|
+
"bulk_adjust_scores",
|
|
221
|
+
"decay_importance",
|
|
222
|
+
"ScoreAdjustment",
|
|
223
|
+
"ItemTransform",
|
|
224
|
+
# replay
|
|
225
|
+
"replay_commands",
|
|
226
|
+
"replay_from_envelopes",
|
|
227
|
+
"ReplayFailure",
|
|
228
|
+
"ReplayResult",
|
|
229
|
+
# envelope
|
|
230
|
+
"wrap_lifecycle_event",
|
|
231
|
+
"wrap_state_event",
|
|
232
|
+
"wrap_edge_state_event",
|
|
233
|
+
# serialization
|
|
234
|
+
"to_json",
|
|
235
|
+
"from_json",
|
|
236
|
+
"stringify",
|
|
237
|
+
"parse",
|
|
238
|
+
"SerializedGraphState",
|
|
239
|
+
# stats
|
|
240
|
+
"get_stats",
|
|
241
|
+
"GraphStats",
|
|
242
|
+
"ItemStats",
|
|
243
|
+
"EdgeStats",
|
|
244
|
+
# intent graph
|
|
245
|
+
"create_intent_state",
|
|
246
|
+
"create_intent",
|
|
247
|
+
"apply_intent_command",
|
|
248
|
+
"get_intents",
|
|
249
|
+
"get_intent_by_id",
|
|
250
|
+
"get_child_intents",
|
|
251
|
+
"Intent",
|
|
252
|
+
"IntentState",
|
|
253
|
+
"IntentStatus",
|
|
254
|
+
"IntentCommand",
|
|
255
|
+
"IntentFilter",
|
|
256
|
+
"IntentLifecycleEvent",
|
|
257
|
+
"IntentResult",
|
|
258
|
+
"IntentNotFoundError",
|
|
259
|
+
"DuplicateIntentError",
|
|
260
|
+
"InvalidIntentTransitionError",
|
|
261
|
+
# task graph
|
|
262
|
+
"create_task_state",
|
|
263
|
+
"create_task",
|
|
264
|
+
"apply_task_command",
|
|
265
|
+
"get_tasks",
|
|
266
|
+
"get_task_by_id",
|
|
267
|
+
"get_tasks_by_intent",
|
|
268
|
+
"get_child_tasks",
|
|
269
|
+
"Task",
|
|
270
|
+
"TaskState",
|
|
271
|
+
"TaskStatus",
|
|
272
|
+
"TaskCommand",
|
|
273
|
+
"TaskFilter",
|
|
274
|
+
"TaskLifecycleEvent",
|
|
275
|
+
"TaskResult",
|
|
276
|
+
"TaskNotFoundError",
|
|
277
|
+
"DuplicateTaskError",
|
|
278
|
+
"InvalidTaskTransitionError",
|
|
279
|
+
# transplant
|
|
280
|
+
"export_slice",
|
|
281
|
+
"import_slice",
|
|
282
|
+
"MemexExport",
|
|
283
|
+
"ExportOptions",
|
|
284
|
+
"ImportOptions",
|
|
285
|
+
"ImportReport",
|
|
286
|
+
"ImportBucket",
|
|
287
|
+
"ImportResult",
|
|
288
|
+
# facade
|
|
289
|
+
"MemexStore",
|
|
290
|
+
# factories
|
|
291
|
+
"create_memory_item",
|
|
292
|
+
"create_edge",
|
|
293
|
+
"create_event_envelope",
|
|
294
|
+
# models
|
|
295
|
+
"MemoryItem",
|
|
296
|
+
"Edge",
|
|
297
|
+
"EventEnvelope",
|
|
298
|
+
"DecayConfig",
|
|
299
|
+
"ScoreWeights",
|
|
300
|
+
"ScoredItem",
|
|
301
|
+
"MemoryFilter",
|
|
302
|
+
"EdgeFilter",
|
|
303
|
+
"SortOption",
|
|
304
|
+
"QueryOptions",
|
|
305
|
+
"Range",
|
|
306
|
+
"ScoreRanges",
|
|
307
|
+
"ParentsFilter",
|
|
308
|
+
"DecayFilter",
|
|
309
|
+
"CreatedFilter",
|
|
310
|
+
"MemoryLifecycleEvent",
|
|
311
|
+
# type aliases
|
|
312
|
+
"KnownMemoryKind",
|
|
313
|
+
"KnownSourceKind",
|
|
314
|
+
"KnownEdgeKind",
|
|
315
|
+
"KnownNamespace",
|
|
316
|
+
"LifecycleEventType",
|
|
317
|
+
"SortField",
|
|
318
|
+
"DecayInterval",
|
|
319
|
+
"DecayType",
|
|
320
|
+
# commands
|
|
321
|
+
"MemoryCommand",
|
|
322
|
+
"MemoryCommandAdapter",
|
|
323
|
+
"MemoryCreate",
|
|
324
|
+
"MemoryUpdate",
|
|
325
|
+
"MemoryRetract",
|
|
326
|
+
"EdgeCreate",
|
|
327
|
+
"EdgeUpdate",
|
|
328
|
+
"EdgeRetract",
|
|
329
|
+
# errors
|
|
330
|
+
"MemexError",
|
|
331
|
+
"MemoryNotFoundError",
|
|
332
|
+
"EdgeNotFoundError",
|
|
333
|
+
"DuplicateMemoryError",
|
|
334
|
+
"DuplicateEdgeError",
|
|
335
|
+
"InvalidTimestampError",
|
|
336
|
+
]
|
memex/_time.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Internal clock helpers.
|
|
2
|
+
|
|
3
|
+
Isolated in one module so tests can monkeypatch ``now_ms`` / ``now_iso``
|
|
4
|
+
deterministically (the analog of stubbing ``Date.now`` / ``Date.toISOString``).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def now_ms() -> int:
|
|
14
|
+
"""Current unix time in milliseconds (mirrors JS ``Date.now()``)."""
|
|
15
|
+
return int(time.time() * 1000)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def now_iso() -> str:
|
|
19
|
+
"""Current UTC time as ISO-8601 with millisecond precision and a ``Z`` suffix.
|
|
20
|
+
|
|
21
|
+
Matches JavaScript ``new Date().toISOString()`` byte-for-byte
|
|
22
|
+
(e.g. ``"2024-06-22T13:45:30.123Z"``), which the strict replay parser
|
|
23
|
+
in :mod:`memex.replay` accepts.
|
|
24
|
+
"""
|
|
25
|
+
dt = datetime.now(timezone.utc)
|
|
26
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{dt.microsecond // 1000:03d}Z"
|
memex/_uuid.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""UUIDv7 generation and timestamp extraction.
|
|
2
|
+
|
|
3
|
+
Mirrors the single runtime dependency of the TypeScript library (``uuidv7``)
|
|
4
|
+
with a tiny internal implementation so the only third-party dependency is
|
|
5
|
+
``pydantic``. UUIDv7 (RFC 9562) encodes a 48-bit big-endian millisecond
|
|
6
|
+
timestamp in its first six bytes; we decode exactly those bytes the same way
|
|
7
|
+
the TS ``safeExtractTimestamp`` does.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from uuid import UUID
|
|
14
|
+
|
|
15
|
+
from . import _time
|
|
16
|
+
|
|
17
|
+
__all__ = ["uuid7", "safe_extract_timestamp"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def uuid7(ms: int | None = None) -> str:
|
|
21
|
+
"""Generate a UUIDv7 string for ``ms`` (defaults to the current time)."""
|
|
22
|
+
if ms is None:
|
|
23
|
+
ms = _time.now_ms()
|
|
24
|
+
ts = ms & ((1 << 48) - 1)
|
|
25
|
+
rand = os.urandom(10)
|
|
26
|
+
|
|
27
|
+
b = bytearray(16)
|
|
28
|
+
b[0] = (ts >> 40) & 0xFF
|
|
29
|
+
b[1] = (ts >> 32) & 0xFF
|
|
30
|
+
b[2] = (ts >> 24) & 0xFF
|
|
31
|
+
b[3] = (ts >> 16) & 0xFF
|
|
32
|
+
b[4] = (ts >> 8) & 0xFF
|
|
33
|
+
b[5] = ts & 0xFF
|
|
34
|
+
b[6] = 0x70 | (rand[0] & 0x0F) # version 7 in the high nibble
|
|
35
|
+
b[7] = rand[1]
|
|
36
|
+
b[8] = 0x80 | (rand[2] & 0x3F) # RFC 4122 variant (0b10) in the top bits
|
|
37
|
+
b[9:16] = rand[3:10]
|
|
38
|
+
return str(UUID(bytes=bytes(b)))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def safe_extract_timestamp(value: str) -> int | None:
|
|
42
|
+
"""Decode the millisecond timestamp from a UUIDv7 id.
|
|
43
|
+
|
|
44
|
+
Returns ``None`` for anything that is not a valid version-7 UUID, or whose
|
|
45
|
+
encoded timestamp is non-positive — matching the TS helper's tolerance.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
parsed = UUID(value)
|
|
49
|
+
except (ValueError, AttributeError, TypeError):
|
|
50
|
+
return None
|
|
51
|
+
if parsed.version != 7:
|
|
52
|
+
return None
|
|
53
|
+
b = parsed.bytes
|
|
54
|
+
ts = (
|
|
55
|
+
(b[0] << 40)
|
|
56
|
+
| (b[1] << 32)
|
|
57
|
+
| (b[2] << 24)
|
|
58
|
+
| (b[3] << 16)
|
|
59
|
+
| (b[4] << 8)
|
|
60
|
+
| b[5]
|
|
61
|
+
)
|
|
62
|
+
return ts if ts > 0 else None
|
memex/bulk.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Bulk operations: single-pass transforms, score adjustments, importance decay."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from . import _time
|
|
11
|
+
from .graph import GraphState
|
|
12
|
+
from .models import Edge, MemoryFilter, MemoryItem, MemoryLifecycleEvent, QueryOptions
|
|
13
|
+
from .query import get_items
|
|
14
|
+
from .reducer import CommandResult, merge_item
|
|
15
|
+
|
|
16
|
+
__all__ = ["ScoreAdjustment", "ItemTransform", "apply_many", "bulk_adjust_scores", "decay_importance"]
|
|
17
|
+
|
|
18
|
+
ItemTransform = Callable[[MemoryItem], "dict[str, Any] | None"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ScoreAdjustment(BaseModel):
|
|
22
|
+
authority: float | None = None
|
|
23
|
+
conviction: float | None = None
|
|
24
|
+
importance: float | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _clamp(value: float) -> float:
|
|
28
|
+
return max(0.0, min(1.0, value))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def apply_many(
|
|
32
|
+
state: GraphState,
|
|
33
|
+
filter: MemoryFilter | dict[str, Any] | None,
|
|
34
|
+
transform: ItemTransform,
|
|
35
|
+
author: str,
|
|
36
|
+
reason: str | None = None,
|
|
37
|
+
options: QueryOptions | dict[str, Any] | None = None,
|
|
38
|
+
) -> CommandResult:
|
|
39
|
+
"""Apply ``transform`` to every matching item in a single pass.
|
|
40
|
+
|
|
41
|
+
``transform`` returns ``None`` to retract (also cleaning up incident edges),
|
|
42
|
+
an empty dict to skip, or a partial to update. The items dict is cloned once;
|
|
43
|
+
the edges dict and a reverse index are built lazily on the first retract.
|
|
44
|
+
"""
|
|
45
|
+
matched = get_items(state, filter, options)
|
|
46
|
+
if not matched:
|
|
47
|
+
return CommandResult(state, [])
|
|
48
|
+
|
|
49
|
+
items = dict(state.items)
|
|
50
|
+
edges: dict[str, Edge] | None = None
|
|
51
|
+
edges_by_endpoint: dict[str, list[str]] | None = None
|
|
52
|
+
all_events: list[MemoryLifecycleEvent] = []
|
|
53
|
+
changed = False
|
|
54
|
+
|
|
55
|
+
for item in matched:
|
|
56
|
+
if item.id not in items:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
partial = transform(item)
|
|
60
|
+
|
|
61
|
+
if partial is None:
|
|
62
|
+
del items[item.id]
|
|
63
|
+
all_events.append(
|
|
64
|
+
MemoryLifecycleEvent(type="memory.retracted", item=item, cause_type="memory.retract")
|
|
65
|
+
)
|
|
66
|
+
changed = True
|
|
67
|
+
if state.edges:
|
|
68
|
+
if edges is None:
|
|
69
|
+
edges = dict(state.edges)
|
|
70
|
+
if edges_by_endpoint is None:
|
|
71
|
+
edges_by_endpoint = {}
|
|
72
|
+
for edge_id, edge in state.edges.items():
|
|
73
|
+
edges_by_endpoint.setdefault(edge.from_, []).append(edge_id)
|
|
74
|
+
if edge.from_ != edge.to:
|
|
75
|
+
edges_by_endpoint.setdefault(edge.to, []).append(edge_id)
|
|
76
|
+
incident_ids = edges_by_endpoint.get(item.id)
|
|
77
|
+
if incident_ids:
|
|
78
|
+
for edge_id in incident_ids:
|
|
79
|
+
incident_edge = edges.get(edge_id)
|
|
80
|
+
if incident_edge is None:
|
|
81
|
+
continue # already cleaned by a prior retract
|
|
82
|
+
del edges[edge_id]
|
|
83
|
+
all_events.append(
|
|
84
|
+
MemoryLifecycleEvent(type="edge.retracted", edge=incident_edge, cause_type="memory.retract")
|
|
85
|
+
)
|
|
86
|
+
elif len(partial) > 0:
|
|
87
|
+
merged = merge_item(item, partial)
|
|
88
|
+
items[item.id] = merged
|
|
89
|
+
all_events.append(
|
|
90
|
+
MemoryLifecycleEvent(type="memory.updated", item=merged, cause_type="memory.update")
|
|
91
|
+
)
|
|
92
|
+
changed = True
|
|
93
|
+
|
|
94
|
+
if not changed:
|
|
95
|
+
return CommandResult(state, [])
|
|
96
|
+
|
|
97
|
+
return CommandResult(GraphState(items, edges if edges is not None else state.edges), all_events)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def bulk_adjust_scores(
|
|
101
|
+
state: GraphState,
|
|
102
|
+
criteria: MemoryFilter | dict[str, Any],
|
|
103
|
+
delta: ScoreAdjustment | dict[str, Any],
|
|
104
|
+
author: str,
|
|
105
|
+
reason: str | None = None,
|
|
106
|
+
) -> CommandResult:
|
|
107
|
+
d = delta if isinstance(delta, ScoreAdjustment) else ScoreAdjustment.model_validate(delta)
|
|
108
|
+
|
|
109
|
+
def transform(item: MemoryItem) -> dict[str, Any]:
|
|
110
|
+
partial: dict[str, Any] = {}
|
|
111
|
+
if d.authority is not None:
|
|
112
|
+
partial["authority"] = _clamp(item.authority + d.authority)
|
|
113
|
+
if d.conviction is not None:
|
|
114
|
+
partial["conviction"] = _clamp((item.conviction or 0) + d.conviction)
|
|
115
|
+
if d.importance is not None:
|
|
116
|
+
partial["importance"] = _clamp((item.importance or 0) + d.importance)
|
|
117
|
+
return partial
|
|
118
|
+
|
|
119
|
+
return apply_many(state, criteria, transform, author, reason)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def decay_importance(
|
|
123
|
+
state: GraphState,
|
|
124
|
+
older_than_ms: int,
|
|
125
|
+
factor: float,
|
|
126
|
+
author: str,
|
|
127
|
+
reason: str | None = None,
|
|
128
|
+
) -> CommandResult:
|
|
129
|
+
"""Decay importance on items created before a cutoff time."""
|
|
130
|
+
cutoff = _time.now_ms() - older_than_ms
|
|
131
|
+
|
|
132
|
+
def transform(item: MemoryItem) -> dict[str, Any]:
|
|
133
|
+
current = item.importance if item.importance is not None else 0
|
|
134
|
+
if current == 0:
|
|
135
|
+
return {}
|
|
136
|
+
return {"importance": _clamp(current * factor)}
|
|
137
|
+
|
|
138
|
+
return apply_many(state, {"created": {"before": cutoff}}, transform, author, reason or "time-based importance decay")
|
memex/commands.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Memory commands as a Pydantic discriminated union.
|
|
2
|
+
|
|
3
|
+
This single module replaces the entire ``schemas.ts`` / Zod layer: in Pydantic
|
|
4
|
+
the command models *are* the schema. ``apply_command`` accepts either a command
|
|
5
|
+
model instance or a plain dict (validated through ``MemoryCommandAdapter``),
|
|
6
|
+
which keeps the TS object-literal call style portable.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Annotated, Any, Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field, TypeAdapter
|
|
14
|
+
|
|
15
|
+
from .models import Edge, MemoryItem
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"MemoryCreate",
|
|
19
|
+
"MemoryUpdate",
|
|
20
|
+
"MemoryRetract",
|
|
21
|
+
"EdgeCreate",
|
|
22
|
+
"EdgeUpdate",
|
|
23
|
+
"EdgeRetract",
|
|
24
|
+
"MemoryCommand",
|
|
25
|
+
"MemoryCommandAdapter",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MemoryCreate(BaseModel):
|
|
30
|
+
type: Literal["memory.create"] = "memory.create"
|
|
31
|
+
item: MemoryItem
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MemoryUpdate(BaseModel):
|
|
35
|
+
type: Literal["memory.update"] = "memory.update"
|
|
36
|
+
item_id: str
|
|
37
|
+
partial: dict[str, Any]
|
|
38
|
+
author: str
|
|
39
|
+
reason: str | None = None
|
|
40
|
+
basis: dict[str, Any] | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MemoryRetract(BaseModel):
|
|
44
|
+
type: Literal["memory.retract"] = "memory.retract"
|
|
45
|
+
item_id: str
|
|
46
|
+
author: str
|
|
47
|
+
reason: str | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class EdgeCreate(BaseModel):
|
|
51
|
+
type: Literal["edge.create"] = "edge.create"
|
|
52
|
+
edge: Edge
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class EdgeUpdate(BaseModel):
|
|
56
|
+
type: Literal["edge.update"] = "edge.update"
|
|
57
|
+
edge_id: str
|
|
58
|
+
partial: dict[str, Any]
|
|
59
|
+
author: str
|
|
60
|
+
reason: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class EdgeRetract(BaseModel):
|
|
64
|
+
type: Literal["edge.retract"] = "edge.retract"
|
|
65
|
+
edge_id: str
|
|
66
|
+
author: str
|
|
67
|
+
reason: str | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
MemoryCommand = Annotated[
|
|
71
|
+
MemoryCreate | MemoryUpdate | MemoryRetract | EdgeCreate | EdgeUpdate | EdgeRetract,
|
|
72
|
+
Field(discriminator="type"),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
MemoryCommandAdapter: TypeAdapter[MemoryCommand] = TypeAdapter(MemoryCommand)
|
memex/envelope.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Helpers that wrap lifecycle events / state snapshots into event envelopes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from . import _time
|
|
8
|
+
from ._uuid import uuid7
|
|
9
|
+
from .models import Edge, EventEnvelope, MemoryItem, MemoryLifecycleEvent
|
|
10
|
+
|
|
11
|
+
__all__ = ["wrap_lifecycle_event", "wrap_state_event", "wrap_edge_state_event"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _event_fields(event: MemoryLifecycleEvent) -> dict[str, Any]:
|
|
15
|
+
# Mirror the TS object spread: only keys that are set (item/edge/cause_type
|
|
16
|
+
# may be absent). Nested item/edge are kept as models until serialized.
|
|
17
|
+
fields: dict[str, Any] = {"namespace": event.namespace, "type": event.type}
|
|
18
|
+
if event.item is not None:
|
|
19
|
+
fields["item"] = event.item
|
|
20
|
+
if event.edge is not None:
|
|
21
|
+
fields["edge"] = event.edge
|
|
22
|
+
if event.cause_type is not None:
|
|
23
|
+
fields["cause_type"] = event.cause_type
|
|
24
|
+
return fields
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def wrap_lifecycle_event(
|
|
28
|
+
event: MemoryLifecycleEvent,
|
|
29
|
+
cause_id: str,
|
|
30
|
+
trace_id: str | None = None,
|
|
31
|
+
) -> EventEnvelope[dict[str, Any]]:
|
|
32
|
+
return EventEnvelope(
|
|
33
|
+
id=uuid7(),
|
|
34
|
+
namespace="memory",
|
|
35
|
+
type=event.type,
|
|
36
|
+
ts=_time.now_iso(),
|
|
37
|
+
trace_id=trace_id,
|
|
38
|
+
payload={**_event_fields(event), "cause_id": cause_id},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def wrap_state_event(
|
|
43
|
+
item: MemoryItem,
|
|
44
|
+
cause_id: str,
|
|
45
|
+
trace_id: str | None = None,
|
|
46
|
+
) -> EventEnvelope[dict[str, Any]]:
|
|
47
|
+
return EventEnvelope(
|
|
48
|
+
id=uuid7(),
|
|
49
|
+
namespace="memory",
|
|
50
|
+
type="state.memory",
|
|
51
|
+
ts=_time.now_iso(),
|
|
52
|
+
trace_id=trace_id,
|
|
53
|
+
payload={"item": item, "cause_id": cause_id},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def wrap_edge_state_event(
|
|
58
|
+
edge: Edge,
|
|
59
|
+
cause_id: str,
|
|
60
|
+
trace_id: str | None = None,
|
|
61
|
+
) -> EventEnvelope[dict[str, Any]]:
|
|
62
|
+
return EventEnvelope(
|
|
63
|
+
id=uuid7(),
|
|
64
|
+
namespace="memory",
|
|
65
|
+
type="state.edge",
|
|
66
|
+
ts=_time.now_iso(),
|
|
67
|
+
trace_id=trace_id,
|
|
68
|
+
payload={"edge": edge, "cause_id": cause_id},
|
|
69
|
+
)
|