loop-memory 0.4.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.
Files changed (84) hide show
  1. loop_memory/__init__.py +62 -0
  2. loop_memory/backends/__init__.py +13 -0
  3. loop_memory/backends/embedding.py +82 -0
  4. loop_memory/backends/sentence_embedder.py +30 -0
  5. loop_memory/backends/vector_store.py +139 -0
  6. loop_memory/cli/__init__.py +0 -0
  7. loop_memory/cli/_common.py +68 -0
  8. loop_memory/cli/commands/__init__.py +13 -0
  9. loop_memory/cli/commands/cognitive.py +205 -0
  10. loop_memory/cli/commands/diag.py +346 -0
  11. loop_memory/cli/commands/graph.py +21 -0
  12. loop_memory/cli/commands/hooks.py +212 -0
  13. loop_memory/cli/commands/read.py +362 -0
  14. loop_memory/cli/commands/serve.py +147 -0
  15. loop_memory/cli/commands/write.py +138 -0
  16. loop_memory/cli/main.py +115 -0
  17. loop_memory/engine/__init__.py +0 -0
  18. loop_memory/engine/loop.py +247 -0
  19. loop_memory/engine/reflect.py +89 -0
  20. loop_memory/examples/__init__.py +0 -0
  21. loop_memory/examples/demo.py +39 -0
  22. loop_memory/export/__init__.py +39 -0
  23. loop_memory/export/memory_md.py +629 -0
  24. loop_memory/graph/__init__.py +0 -0
  25. loop_memory/graph/build.py +259 -0
  26. loop_memory/graph/extract.py +197 -0
  27. loop_memory/ingest/__init__.py +0 -0
  28. loop_memory/ingest/loader.py +782 -0
  29. loop_memory/ingest/pipeline.py +458 -0
  30. loop_memory/jobs/__init__.py +0 -0
  31. loop_memory/jobs/cognitive.py +353 -0
  32. loop_memory/jobs/compact.py +371 -0
  33. loop_memory/jobs/consolidate.py +95 -0
  34. loop_memory/jobs/contradiction.py +281 -0
  35. loop_memory/jobs/evolution.py +2021 -0
  36. loop_memory/jobs/graph.py +395 -0
  37. loop_memory/jobs/llm_compact_pass.py +24 -0
  38. loop_memory/jobs/llm_consolidate.py +980 -0
  39. loop_memory/jobs/scheduler.py +495 -0
  40. loop_memory/llm/__init__.py +0 -0
  41. loop_memory/llm/base.py +80 -0
  42. loop_memory/llm/openai_adapter.py +31 -0
  43. loop_memory/llm/providers.py +517 -0
  44. loop_memory/mcp/__init__.py +804 -0
  45. loop_memory/memory/__init__.py +0 -0
  46. loop_memory/memory/types.py +199 -0
  47. loop_memory/privacy/__init__.py +22 -0
  48. loop_memory/privacy/private.py +46 -0
  49. loop_memory/privacy/redact.py +188 -0
  50. loop_memory/py.typed +0 -0
  51. loop_memory/sdk.py +875 -0
  52. loop_memory/sdk_extensions.py +384 -0
  53. loop_memory/security/__init__.py +20 -0
  54. loop_memory/security/secrets.py +464 -0
  55. loop_memory/serve/__init__.py +0 -0
  56. loop_memory/serve/app.py +506 -0
  57. loop_memory/serve/handlers.py +316 -0
  58. loop_memory/serve/routes/_shared.py +59 -0
  59. loop_memory/serve/routes/admin.py +970 -0
  60. loop_memory/serve/routes/cognitive.py +64 -0
  61. loop_memory/serve/routes/export.py +65 -0
  62. loop_memory/serve/routes/graph.py +101 -0
  63. loop_memory/serve/routes/insights.py +702 -0
  64. loop_memory/serve/routes/memories.py +435 -0
  65. loop_memory/serve/routes/sessions.py +75 -0
  66. loop_memory/serve/routes/system.py +493 -0
  67. loop_memory/serve/routes/wiki.py +812 -0
  68. loop_memory/serve/static/__init__.py +0 -0
  69. loop_memory/serve/static/index.html +15 -0
  70. loop_memory/serve/watcher.py +451 -0
  71. loop_memory/storage/__init__.py +5 -0
  72. loop_memory/storage/retrieval.py +365 -0
  73. loop_memory/storage/sqlite_store.py +3627 -0
  74. loop_memory/wiki/__init__.py +41 -0
  75. loop_memory/wiki/backfill.py +143 -0
  76. loop_memory/wiki/classifier.py +238 -0
  77. loop_memory/wiki/prompts.py +295 -0
  78. loop_memory/wiki/scope.py +227 -0
  79. loop_memory-0.4.0.dist-info/METADATA +627 -0
  80. loop_memory-0.4.0.dist-info/RECORD +84 -0
  81. loop_memory-0.4.0.dist-info/WHEEL +5 -0
  82. loop_memory-0.4.0.dist-info/entry_points.txt +2 -0
  83. loop_memory-0.4.0.dist-info/licenses/LICENSE +21 -0
  84. loop_memory-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,384 @@
1
+ """Extra surface on top of ``loop_memory.sdk.MemoryClient``.
2
+
3
+ The base ``MemoryClient`` covers the 4-verb contract (remember /
4
+ recall / feedback / forget). The Universal Agent Memory
5
+ contract adds four more first-class operations an Agent needs:
6
+
7
+ * **namespace** — `for_user(...)` / `for_agent(...)` sugar that
8
+ returns a proxy whose every call auto-stamps the triple, so
9
+ multi-tenant code reads like English instead of plumbing.
10
+
11
+ * **graph** — push high-signal relations (Mem0's differentiator),
12
+ query the subgraph for a free-text query, and rebuild the
13
+ entity-mention index.
14
+
15
+ * **cognitive** — run a single ``cognitive_sleep`` sweep, dry-run
16
+ by default, and read the audit trail.
17
+
18
+ * **export / import / fork** — write a white-box ``MEMORY.md``
19
+ bundle, re-hydrate it, or snapshot a branch of the wiki.
20
+
21
+ These are mixed in via the ``MemoryClientExt`` class — the SDK
22
+ exposes a single ``MemoryClient`` symbol that inherits from both
23
+ ``MemoryClient`` and ``MemoryClientExt`` so the call sites stay
24
+ unchanged.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import os
31
+ import urllib.error
32
+ import urllib.parse
33
+ import urllib.request
34
+ from dataclasses import dataclass, field
35
+ from typing import Any, Iterable
36
+
37
+
38
+ # Re-export the symbols the caller will need.
39
+ __all__ = [
40
+ "MemoryClientExt",
41
+ "MemoryNamespace",
42
+ "GraphHit",
43
+ "SubgraphView",
44
+ "CognitiveActionView",
45
+ "CognitiveReportView",
46
+ "ExportView",
47
+ "ImportView",
48
+ ]
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Dataclasses
53
+ # ---------------------------------------------------------------------------
54
+
55
+
56
+ @dataclass
57
+ class GraphHit:
58
+ kind: str
59
+ src: str
60
+ dst: str
61
+ weight: float
62
+ evidence_id: str | None = None
63
+
64
+ @classmethod
65
+ def from_dict(cls, d: dict[str, Any]) -> GraphHit:
66
+ return cls(
67
+ kind=d.get("kind", "co_occurs_with"),
68
+ src=d.get("src") or d.get("name") or "",
69
+ dst=d.get("dst") or "",
70
+ weight=float(d.get("weight", 0) or 0),
71
+ evidence_id=d.get("evidence_id"),
72
+ )
73
+
74
+
75
+ @dataclass
76
+ class SubgraphView:
77
+ nodes: list[dict[str, Any]] = field(default_factory=list)
78
+ edges: list[GraphHit] = field(default_factory=list)
79
+ memory_ids: list[str] = field(default_factory=list)
80
+ node_count: int = 0
81
+ edge_count: int = 0
82
+
83
+ @classmethod
84
+ def from_dict(cls, d: dict[str, Any]) -> SubgraphView:
85
+ edges = [GraphHit.from_dict(e) for e in d.get("edges", [])]
86
+ return cls(
87
+ nodes=list(d.get("nodes", [])),
88
+ edges=edges,
89
+ memory_ids=list(d.get("memory_ids", [])),
90
+ node_count=int(d.get("node_count", len(d.get("nodes", [])))),
91
+ edge_count=int(d.get("edge_count", len(edges))),
92
+ )
93
+
94
+
95
+ @dataclass
96
+ class CognitiveActionView:
97
+ kind: str
98
+ target_kind: str
99
+ target_id: str
100
+ target_text: str
101
+ reason: str
102
+ score: float
103
+ action: str
104
+ payload: dict = field(default_factory=dict)
105
+
106
+ @classmethod
107
+ def from_dict(cls, d: dict[str, Any]) -> CognitiveActionView:
108
+ return cls(
109
+ kind=d.get("kind", ""),
110
+ target_kind=d.get("target_kind", ""),
111
+ target_id=d.get("target_id") or "",
112
+ target_text=d.get("target_text") or "",
113
+ reason=d.get("reason") or "",
114
+ score=float(d.get("score", 0) or 0),
115
+ action=d.get("action", "suggest"),
116
+ payload=dict(d.get("payload") or {}),
117
+ )
118
+
119
+
120
+ @dataclass
121
+ class CognitiveReportView:
122
+ actions: list[CognitiveActionView] = field(default_factory=list)
123
+ counts: dict[str, int] = field(default_factory=dict)
124
+ applied: bool = False
125
+ elapsed_ms: float = 0.0
126
+ total: int = 0
127
+
128
+ @classmethod
129
+ def from_dict(cls, d: dict[str, Any]) -> CognitiveReportView:
130
+ return cls(
131
+ actions=[CognitiveActionView.from_dict(a) for a in d.get("actions", [])],
132
+ counts=dict(d.get("counts", {})),
133
+ applied=bool(d.get("applied", False)),
134
+ elapsed_ms=float(d.get("elapsed_ms", 0) or 0),
135
+ total=int(d.get("total", 0)),
136
+ )
137
+
138
+
139
+ @dataclass
140
+ class ExportView:
141
+ out_dir: str
142
+ memory_md_path: str
143
+ pages: list[str] = field(default_factory=list)
144
+ memories: int = 0
145
+ graph_entities: int = 0
146
+ graph_relations: int = 0
147
+ sessions: int = 0
148
+ elapsed_ms: float = 0.0
149
+
150
+ @classmethod
151
+ def from_dict(cls, d: dict[str, Any]) -> ExportView:
152
+ return cls(
153
+ out_dir=d.get("out_dir", ""),
154
+ memory_md_path=d.get("memory_md_path", ""),
155
+ pages=list(d.get("pages", [])),
156
+ memories=int(d.get("memories", 0) or 0),
157
+ graph_entities=int(d.get("graph_entities", 0) or 0),
158
+ graph_relations=int(d.get("graph_relations", 0) or 0),
159
+ sessions=int(d.get("sessions", 0) or 0),
160
+ elapsed_ms=float(d.get("elapsed_ms", 0) or 0),
161
+ )
162
+
163
+
164
+ @dataclass
165
+ class ImportView:
166
+ pages_upserted: int = 0
167
+ memories_upserted: int = 0
168
+ entities_upserted: int = 0
169
+ relations_upserted: int = 0
170
+ sessions_upserted: int = 0
171
+ elapsed_ms: float = 0.0
172
+ bundle_path: str = ""
173
+
174
+ @classmethod
175
+ def from_dict(cls, d: dict[str, Any]) -> ImportView:
176
+ return cls(**{k: d.get(k, getattr(cls(), k)) for k in cls.__dataclass_fields__})
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Namespace proxy
181
+ # ---------------------------------------------------------------------------
182
+
183
+
184
+ class MemoryNamespace:
185
+ """A thin proxy returned by ``MemoryClient.for_user(...)`` /
186
+ ``for_agent(...)`` that auto-stamps the namespace triple on
187
+ every call.
188
+
189
+ Read ``client.remember(...)`` once and then ``ns.remember(...)``
190
+ in every call site — no need to repeat ``user_id=`` and
191
+ ``external_id=`` plumbing. The proxy is itself a context
192
+ manager so ``with client.for_user("alice") as alice_ns:`` is
193
+ a documented pattern even though it doesn't own any
194
+ resources.
195
+ """
196
+
197
+ def __init__(self, parent, *, agent_id: str | None = None,
198
+ user_id: str | None = None) -> None:
199
+ self._parent = parent
200
+ self._agent_id = agent_id
201
+ self._user_id = user_id
202
+
203
+ @property
204
+ def agent_id(self) -> str | None:
205
+ return self._agent_id
206
+
207
+ @property
208
+ def user_id(self) -> str | None:
209
+ return self._user_id
210
+
211
+ def __enter__(self) -> MemoryNamespace:
212
+ return self
213
+
214
+ def __exit__(self, *exc) -> None:
215
+ return None
216
+
217
+ # ----- 4-verb contract -----------------------------------------
218
+
219
+ def remember(self, text: str, **kwargs: Any) -> Any:
220
+ kwargs.setdefault("agent_id", self._agent_id)
221
+ kwargs.setdefault("user_id", self._user_id)
222
+ return self._parent.remember(text, **kwargs)
223
+
224
+ def recall(self, query: str, **kwargs: Any) -> Any:
225
+ kwargs.setdefault("agent_id", self._agent_id)
226
+ kwargs.setdefault("user_id", self._user_id)
227
+ return self._parent.recall(query, **kwargs)
228
+
229
+ def feedback(self, **kwargs: Any) -> bool:
230
+ kwargs.setdefault("agent_id", self._agent_id)
231
+ kwargs.setdefault("user_id", self._user_id)
232
+ return self._parent.feedback(**kwargs)
233
+
234
+ def forget(self, **kwargs: Any) -> int:
235
+ kwargs.setdefault("agent_id", self._agent_id)
236
+ kwargs.setdefault("user_id", self._user_id)
237
+ return self._parent.forget(**kwargs)
238
+
239
+ def list(self, **kwargs: Any) -> list:
240
+ kwargs.setdefault("agent_id", self._agent_id)
241
+ kwargs.setdefault("user_id", self._user_id)
242
+ return self._parent.list(**kwargs)
243
+
244
+ # ----- extra ops ----------------------------------------------
245
+
246
+ def subgraph(self, query: str, **kwargs: Any) -> SubgraphView:
247
+ return self._parent.subgraph(query, **kwargs)
248
+
249
+ def remember_edge(self, src: str, dst: str, *, kind: str = "relates_to",
250
+ weight: float = 0.5) -> GraphHit:
251
+ return self._parent.remember_edge(src, dst, kind=kind, weight=weight)
252
+
253
+ def cognitive_sleep(self, **kwargs: Any) -> CognitiveReportView:
254
+ return self._parent.cognitive_sleep(**kwargs)
255
+
256
+ def audit(self, **kwargs: Any) -> list[CognitiveActionView]:
257
+ return self._parent.audit(**kwargs)
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # Mixin
262
+ # ---------------------------------------------------------------------------
263
+
264
+
265
+ class MemoryClientExt:
266
+ """Mixin that adds namespace / graph / cognitive / export to any
267
+ MemoryClient. The base class is a stub — concrete behaviour
268
+ lives in the in-process and HTTP backends.
269
+ """
270
+
271
+ # ---- namespace sugar -------------------------------------------
272
+
273
+ def for_user(self, user_id: str, *, agent_id: str | None = None) -> MemoryNamespace:
274
+ a = agent_id if agent_id is not None else getattr(self, "_default_agent_id", None)
275
+ return MemoryNamespace(self, agent_id=a, user_id=user_id)
276
+
277
+ def for_agent(self, agent_id: str, *, user_id: str | None = None) -> MemoryNamespace:
278
+ u = user_id if user_id is not None else getattr(self, "_default_user_id", None)
279
+ return MemoryNamespace(self, agent_id=agent_id, user_id=u)
280
+
281
+ # ---- graph ----------------------------------------------------
282
+
283
+ def remember_edge(self, src: str, dst: str, *,
284
+ kind: str = "relates_to", weight: float = 0.5,
285
+ evidence_id: str | None = None) -> GraphHit:
286
+ raise NotImplementedError
287
+
288
+ def subgraph(self, query: str, *, max_nodes: int = 32,
289
+ max_edges: int = 64) -> SubgraphView:
290
+ raise NotImplementedError
291
+
292
+ def rebuild_graph(self) -> int:
293
+ """Re-extract entities from every memory and rebuild the
294
+ ``entity_mentions`` table. Returns the number of mentions
295
+ written."""
296
+ raise NotImplementedError
297
+
298
+ def recall_adaptive(self, query: str, *, limit: int = 8, **kwargs) -> Any:
299
+ """Recall with the 3D AdaptiveScore + graph boost enabled.
300
+
301
+ Falls back to plain ``recall`` on stores that don't support
302
+ adaptive scoring yet, so the call site is forward-compatible.
303
+ """
304
+ raise NotImplementedError
305
+
306
+ # ---- cognitive ------------------------------------------------
307
+
308
+ def cognitive_sleep(self, *, apply: bool = False, **kwargs) -> CognitiveReportView:
309
+ raise NotImplementedError
310
+
311
+ def audit(self, *, kind: str | None = None, action: str | None = None,
312
+ limit: int = 200) -> list[CognitiveActionView]:
313
+ raise NotImplementedError
314
+
315
+ def revert_audit(self, audit_id: str) -> bool:
316
+ """Mark an audit row as ``reverted`` (does not actually
317
+ re-insert the deleted memory — it just records the
318
+ decision so a future re-import of a bundle can restore
319
+ it)."""
320
+ raise NotImplementedError
321
+
322
+ # ---- export / import / fork -----------------------------------
323
+
324
+ def export(self, out_dir: str, *, agent_id: str | None = None,
325
+ user_id: str | None = None, scope: str = "global",
326
+ min_importance: float = 0.0) -> ExportView:
327
+ raise NotImplementedError
328
+
329
+ def import_bundle(self, in_dir: str, *, agent_id: str | None = None,
330
+ user_id: str | None = None, dry_run: bool = False) -> ImportView:
331
+ raise NotImplementedError
332
+
333
+ def fork(self, branch_tag: str | None = None) -> dict[str, Any]:
334
+ raise NotImplementedError
335
+
336
+
337
+ # ---------------------------------------------------------------------------
338
+ # Helpers shared by both backends
339
+ # ---------------------------------------------------------------------------
340
+
341
+
342
+ def http_post_json(url: str, body: dict, *, timeout: float = 10.0) -> dict:
343
+ data = json.dumps(body).encode("utf-8")
344
+ req = urllib.request.Request(
345
+ url, data=data, method="POST",
346
+ headers={"Content-Type": "application/json", "Accept": "application/json"},
347
+ )
348
+ try:
349
+ with urllib.request.urlopen(req, timeout=timeout) as r:
350
+ return json.loads(r.read().decode("utf-8") or "{}")
351
+ except urllib.error.HTTPError as e:
352
+ raw = e.read().decode("utf-8", errors="ignore")
353
+ raise MemoryClientError(f"POST {url} → {e.code}: {raw[:300]}") from e
354
+ except urllib.error.URLError as e:
355
+ raise MemoryClientError(f"cannot reach {url}: {e}") from e
356
+
357
+
358
+ def http_get_json(url: str, *, timeout: float = 10.0) -> dict:
359
+ req = urllib.request.Request(url, headers={"Accept": "application/json"})
360
+ try:
361
+ with urllib.request.urlopen(req, timeout=timeout) as r:
362
+ return json.loads(r.read().decode("utf-8") or "{}")
363
+ except urllib.error.HTTPError as e:
364
+ raw = e.read().decode("utf-8", errors="ignore")
365
+ raise MemoryClientError(f"GET {url} → {e.code}: {raw[:300]}") from e
366
+ except urllib.error.URLError as e:
367
+ raise MemoryClientError(f"cannot reach {url}: {e}") from e
368
+
369
+
370
+ def http_delete_json(url: str, *, timeout: float = 10.0) -> dict:
371
+ req = urllib.request.Request(url, method="DELETE",
372
+ headers={"Accept": "application/json"})
373
+ try:
374
+ with urllib.request.urlopen(req, timeout=timeout) as r:
375
+ return json.loads(r.read().decode("utf-8") or "{}")
376
+ except urllib.error.HTTPError as e:
377
+ raw = e.read().decode("utf-8", errors="ignore")
378
+ raise MemoryClientError(f"DELETE {url} → {e.code}: {raw[:300]}") from e
379
+ except urllib.error.URLError as e:
380
+ raise MemoryClientError(f"cannot reach {url}: {e}") from e
381
+
382
+
383
+ class MemoryClientError(RuntimeError):
384
+ """Raised when the HTTP backend cannot serve a request."""
@@ -0,0 +1,20 @@
1
+ """Cross-platform secret storage. See ``secrets.py`` for details."""
2
+ from .secrets import (
3
+ account_for,
4
+ backend_display_name,
5
+ backend_name,
6
+ delete_secret,
7
+ get_secret,
8
+ has_secret,
9
+ set_secret,
10
+ )
11
+
12
+ __all__ = [
13
+ "account_for",
14
+ "backend_display_name",
15
+ "backend_name",
16
+ "delete_secret",
17
+ "get_secret",
18
+ "has_secret",
19
+ "set_secret",
20
+ ]