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.
- loop_memory/__init__.py +62 -0
- loop_memory/backends/__init__.py +13 -0
- loop_memory/backends/embedding.py +82 -0
- loop_memory/backends/sentence_embedder.py +30 -0
- loop_memory/backends/vector_store.py +139 -0
- loop_memory/cli/__init__.py +0 -0
- loop_memory/cli/_common.py +68 -0
- loop_memory/cli/commands/__init__.py +13 -0
- loop_memory/cli/commands/cognitive.py +205 -0
- loop_memory/cli/commands/diag.py +346 -0
- loop_memory/cli/commands/graph.py +21 -0
- loop_memory/cli/commands/hooks.py +212 -0
- loop_memory/cli/commands/read.py +362 -0
- loop_memory/cli/commands/serve.py +147 -0
- loop_memory/cli/commands/write.py +138 -0
- loop_memory/cli/main.py +115 -0
- loop_memory/engine/__init__.py +0 -0
- loop_memory/engine/loop.py +247 -0
- loop_memory/engine/reflect.py +89 -0
- loop_memory/examples/__init__.py +0 -0
- loop_memory/examples/demo.py +39 -0
- loop_memory/export/__init__.py +39 -0
- loop_memory/export/memory_md.py +629 -0
- loop_memory/graph/__init__.py +0 -0
- loop_memory/graph/build.py +259 -0
- loop_memory/graph/extract.py +197 -0
- loop_memory/ingest/__init__.py +0 -0
- loop_memory/ingest/loader.py +782 -0
- loop_memory/ingest/pipeline.py +458 -0
- loop_memory/jobs/__init__.py +0 -0
- loop_memory/jobs/cognitive.py +353 -0
- loop_memory/jobs/compact.py +371 -0
- loop_memory/jobs/consolidate.py +95 -0
- loop_memory/jobs/contradiction.py +281 -0
- loop_memory/jobs/evolution.py +2021 -0
- loop_memory/jobs/graph.py +395 -0
- loop_memory/jobs/llm_compact_pass.py +24 -0
- loop_memory/jobs/llm_consolidate.py +980 -0
- loop_memory/jobs/scheduler.py +495 -0
- loop_memory/llm/__init__.py +0 -0
- loop_memory/llm/base.py +80 -0
- loop_memory/llm/openai_adapter.py +31 -0
- loop_memory/llm/providers.py +517 -0
- loop_memory/mcp/__init__.py +804 -0
- loop_memory/memory/__init__.py +0 -0
- loop_memory/memory/types.py +199 -0
- loop_memory/privacy/__init__.py +22 -0
- loop_memory/privacy/private.py +46 -0
- loop_memory/privacy/redact.py +188 -0
- loop_memory/py.typed +0 -0
- loop_memory/sdk.py +875 -0
- loop_memory/sdk_extensions.py +384 -0
- loop_memory/security/__init__.py +20 -0
- loop_memory/security/secrets.py +464 -0
- loop_memory/serve/__init__.py +0 -0
- loop_memory/serve/app.py +506 -0
- loop_memory/serve/handlers.py +316 -0
- loop_memory/serve/routes/_shared.py +59 -0
- loop_memory/serve/routes/admin.py +970 -0
- loop_memory/serve/routes/cognitive.py +64 -0
- loop_memory/serve/routes/export.py +65 -0
- loop_memory/serve/routes/graph.py +101 -0
- loop_memory/serve/routes/insights.py +702 -0
- loop_memory/serve/routes/memories.py +435 -0
- loop_memory/serve/routes/sessions.py +75 -0
- loop_memory/serve/routes/system.py +493 -0
- loop_memory/serve/routes/wiki.py +812 -0
- loop_memory/serve/static/__init__.py +0 -0
- loop_memory/serve/static/index.html +15 -0
- loop_memory/serve/watcher.py +451 -0
- loop_memory/storage/__init__.py +5 -0
- loop_memory/storage/retrieval.py +365 -0
- loop_memory/storage/sqlite_store.py +3627 -0
- loop_memory/wiki/__init__.py +41 -0
- loop_memory/wiki/backfill.py +143 -0
- loop_memory/wiki/classifier.py +238 -0
- loop_memory/wiki/prompts.py +295 -0
- loop_memory/wiki/scope.py +227 -0
- loop_memory-0.4.0.dist-info/METADATA +627 -0
- loop_memory-0.4.0.dist-info/RECORD +84 -0
- loop_memory-0.4.0.dist-info/WHEEL +5 -0
- loop_memory-0.4.0.dist-info/entry_points.txt +2 -0
- loop_memory-0.4.0.dist-info/licenses/LICENSE +21 -0
- loop_memory-0.4.0.dist-info/top_level.txt +1 -0
loop_memory/sdk.py
ADDED
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
"""Universal Agent Memory SDK.
|
|
2
|
+
|
|
3
|
+
This is the protocol-agnostic surface every Agent (Codex, Claude,
|
|
4
|
+
Hermes, OpenClaw, LangChain, AutoGPT, a custom internal bot, …) uses
|
|
5
|
+
to remember facts, recall context, give feedback, and forget.
|
|
6
|
+
|
|
7
|
+
The SDK deliberately has *zero* third-party dependencies so it
|
|
8
|
+
ships with ``loop-memory`` itself. Two backends are supported:
|
|
9
|
+
|
|
10
|
+
* **In-process** — wraps a :class:`MemoryStore` directly. Use this
|
|
11
|
+
when your agent runs in the same Python process as loop-memory
|
|
12
|
+
(long-running daemon, embedded tool, tests).
|
|
13
|
+
* **HTTP** — talks to a running ``loop-memory serve`` instance over
|
|
14
|
+
``http://127.0.0.1:7767``. Use this from any other language that
|
|
15
|
+
can speak JSON over HTTP, or from another process. Zero deps:
|
|
16
|
+
uses :mod:`urllib` from the stdlib.
|
|
17
|
+
|
|
18
|
+
The public surface is intentionally small and stable:
|
|
19
|
+
|
|
20
|
+
>>> client = MemoryClient.memory(store)
|
|
21
|
+
>>> client.remember("user prefers dark mode", kind="preference",
|
|
22
|
+
... tags=["ui"], external_id="pref-dark")
|
|
23
|
+
>>> client.recall("dark mode", limit=5)
|
|
24
|
+
>>> client.feedback(external_id="pref-dark", value="up")
|
|
25
|
+
>>> client.forget(external_id="pref-dark")
|
|
26
|
+
>>> client.close()
|
|
27
|
+
|
|
28
|
+
Every write supports ``external_id`` for idempotency: re-pushing the
|
|
29
|
+
same ``(agent_id, user_id, external_id)`` tuple updates the row in
|
|
30
|
+
place instead of creating a duplicate.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
import os
|
|
37
|
+
import time
|
|
38
|
+
import urllib.error
|
|
39
|
+
import urllib.parse
|
|
40
|
+
import urllib.request
|
|
41
|
+
from contextlib import AbstractContextManager
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from typing import Any, Iterable, Iterator
|
|
45
|
+
|
|
46
|
+
from .sdk_extensions import (
|
|
47
|
+
CognitiveActionView, CognitiveReportView, ExportView, GraphHit,
|
|
48
|
+
ImportView, MemoryClientExt, MemoryClientError, MemoryNamespace,
|
|
49
|
+
SubgraphView, http_delete_json, http_get_json, http_post_json,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Public dataclasses — the Agent-facing vocabulary
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class Memory:
|
|
60
|
+
"""Agent-facing view of one memory row.
|
|
61
|
+
|
|
62
|
+
Fields mirror the storage layer but stay JSON-serialisable so
|
|
63
|
+
callers can pass them straight to any LLM prompt.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
id: str
|
|
67
|
+
text: str
|
|
68
|
+
kind: str
|
|
69
|
+
importance: float
|
|
70
|
+
score: float
|
|
71
|
+
source: str | None
|
|
72
|
+
session_id: str | None
|
|
73
|
+
agent_id: str | None
|
|
74
|
+
user_id: str | None
|
|
75
|
+
external_id: str | None
|
|
76
|
+
tags: list[str] = field(default_factory=list)
|
|
77
|
+
created_at: float = 0.0
|
|
78
|
+
updated_at: float = 0.0
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_dict(cls, d: dict[str, Any]) -> Memory:
|
|
82
|
+
return cls(
|
|
83
|
+
id=d.get("id", ""),
|
|
84
|
+
text=d.get("text", ""),
|
|
85
|
+
kind=d.get("kind", "fact"),
|
|
86
|
+
importance=float(d.get("importance", 0.5) or 0.5),
|
|
87
|
+
score=float(d.get("score", 0.5) or 0.5),
|
|
88
|
+
source=d.get("source"),
|
|
89
|
+
session_id=d.get("session_id"),
|
|
90
|
+
agent_id=d.get("agent_id"),
|
|
91
|
+
user_id=d.get("user_id"),
|
|
92
|
+
external_id=d.get("external_id"),
|
|
93
|
+
tags=list(d.get("tags") or []),
|
|
94
|
+
created_at=float(d.get("created_at") or 0.0),
|
|
95
|
+
updated_at=float(d.get("updated_at") or 0.0),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class RecallHit:
|
|
101
|
+
"""One ranked item in a recall response.
|
|
102
|
+
|
|
103
|
+
``kind`` is one of "memory" | "wiki" | "entity" so a single
|
|
104
|
+
Agent prompt can render any of them in a unified way. Memory
|
|
105
|
+
hits carry ``agent_id`` / ``user_id`` / ``external_id`` so the
|
|
106
|
+
caller can route feedback / forget calls back to the store
|
|
107
|
+
without a second lookup.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
kind: str
|
|
111
|
+
id: str
|
|
112
|
+
text: str
|
|
113
|
+
score: float
|
|
114
|
+
title: str | None = None
|
|
115
|
+
source: str | None = None
|
|
116
|
+
tags: list[str] = field(default_factory=list)
|
|
117
|
+
snippet: str | None = None
|
|
118
|
+
agent_id: str | None = None
|
|
119
|
+
user_id: str | None = None
|
|
120
|
+
external_id: str | None = None
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_dict(cls, d: dict[str, Any]) -> RecallHit:
|
|
124
|
+
return cls(
|
|
125
|
+
kind=d.get("kind", "memory"),
|
|
126
|
+
id=d.get("id", ""),
|
|
127
|
+
text=d.get("text", ""),
|
|
128
|
+
score=float(d.get("score", 0.0) or 0.0),
|
|
129
|
+
title=d.get("title"),
|
|
130
|
+
source=d.get("source"),
|
|
131
|
+
tags=list(d.get("tags") or []),
|
|
132
|
+
snippet=d.get("preview") or d.get("snippet"),
|
|
133
|
+
agent_id=d.get("agent_id"),
|
|
134
|
+
user_id=d.get("user_id"),
|
|
135
|
+
external_id=d.get("external_id"),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class RecallResult:
|
|
141
|
+
"""The full payload returned by :meth:`MemoryClient.recall`."""
|
|
142
|
+
|
|
143
|
+
query: str
|
|
144
|
+
memories: list[RecallHit] = field(default_factory=list)
|
|
145
|
+
wiki: list[RecallHit] = field(default_factory=list)
|
|
146
|
+
entities: list[RecallHit] = field(default_factory=list)
|
|
147
|
+
temporal_intent: str = "any"
|
|
148
|
+
temporal_confidence: float = 0.0
|
|
149
|
+
source: str | None = None
|
|
150
|
+
|
|
151
|
+
def all(self) -> list[RecallHit]:
|
|
152
|
+
"""Return a single sorted stream across all three channels."""
|
|
153
|
+
merged = self.memories + self.wiki + self.entities
|
|
154
|
+
merged.sort(key=lambda h: -h.score)
|
|
155
|
+
return merged
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# SDK base class
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class MemoryClient(MemoryClientExt, AbstractContextManager):
|
|
164
|
+
"""Protocol-agnostic Agent Memory client.
|
|
165
|
+
|
|
166
|
+
Use the two factory constructors to pick a backend:
|
|
167
|
+
|
|
168
|
+
* :meth:`memory` — direct in-process access to a ``MemoryStore``
|
|
169
|
+
* :meth:`http` — talks to a running ``loop-memory serve`` over
|
|
170
|
+
HTTP (zero third-party deps, stdlib only)
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def __init__(self) -> None:
|
|
174
|
+
self._owns_backend = False
|
|
175
|
+
|
|
176
|
+
# ---- factories -------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def memory(cls, store, *, agent_id: str | None = None,
|
|
180
|
+
user_id: str | None = None) -> MemoryClient:
|
|
181
|
+
"""Build an SDK bound to an in-process :class:`MemoryStore`."""
|
|
182
|
+
c = _InProcessClient(store)
|
|
183
|
+
if agent_id is not None:
|
|
184
|
+
c._default_agent_id = agent_id
|
|
185
|
+
if user_id is not None:
|
|
186
|
+
c._default_user_id = user_id
|
|
187
|
+
return c
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def http(cls, base_url: str = "http://127.0.0.1:7767",
|
|
191
|
+
*, agent_id: str | None = None,
|
|
192
|
+
user_id: str | None = None,
|
|
193
|
+
timeout: float = 10.0) -> MemoryClient:
|
|
194
|
+
"""Build an SDK that talks to a running ``loop-memory serve``."""
|
|
195
|
+
c = _HttpClient(base_url=base_url, timeout=timeout)
|
|
196
|
+
if agent_id is not None:
|
|
197
|
+
c._default_agent_id = agent_id
|
|
198
|
+
if user_id is not None:
|
|
199
|
+
c._default_user_id = user_id
|
|
200
|
+
return c
|
|
201
|
+
|
|
202
|
+
# ---- shared state for defaults -------------------------------------
|
|
203
|
+
|
|
204
|
+
_default_agent_id: str | None = None
|
|
205
|
+
_default_user_id: str | None = None
|
|
206
|
+
|
|
207
|
+
# ---- Agent-facing API -----------------------------------------------
|
|
208
|
+
|
|
209
|
+
def remember(
|
|
210
|
+
self,
|
|
211
|
+
text: str,
|
|
212
|
+
*,
|
|
213
|
+
kind: str = "fact",
|
|
214
|
+
importance: float = 0.5,
|
|
215
|
+
tags: list[str] | None = None,
|
|
216
|
+
source: str | None = None,
|
|
217
|
+
session_id: str | None = None,
|
|
218
|
+
external_id: str | None = None,
|
|
219
|
+
agent_id: str | None = None,
|
|
220
|
+
user_id: str | None = None,
|
|
221
|
+
ttl: float | None = None,
|
|
222
|
+
created_at: float | None = None,
|
|
223
|
+
) -> Memory:
|
|
224
|
+
"""Push one memory into long-term storage.
|
|
225
|
+
|
|
226
|
+
``external_id`` makes the write idempotent: re-pushing the
|
|
227
|
+
same ``(agent_id, user_id, external_id)`` tuple updates the
|
|
228
|
+
row in place. Use a stable per-agent id (e.g. the tool name
|
|
229
|
+
+ call args hash) so retries from your Agent don't duplicate.
|
|
230
|
+
"""
|
|
231
|
+
raise NotImplementedError
|
|
232
|
+
|
|
233
|
+
def remember_batch(self, items: Iterable[dict[str, Any]]) -> list[Memory]:
|
|
234
|
+
"""Push many memories in one call. Same shape as ``remember``."""
|
|
235
|
+
raise NotImplementedError
|
|
236
|
+
|
|
237
|
+
def recall(
|
|
238
|
+
self,
|
|
239
|
+
query: str,
|
|
240
|
+
*,
|
|
241
|
+
limit: int = 8,
|
|
242
|
+
source: str | None = None,
|
|
243
|
+
agent_id: str | None = None,
|
|
244
|
+
user_id: str | None = None,
|
|
245
|
+
include: str = "memories,wiki,entities",
|
|
246
|
+
) -> RecallResult:
|
|
247
|
+
"""Unified search across the user's memory store."""
|
|
248
|
+
raise NotImplementedError
|
|
249
|
+
|
|
250
|
+
def forget(
|
|
251
|
+
self,
|
|
252
|
+
*,
|
|
253
|
+
external_id: str | None = None,
|
|
254
|
+
memory_id: str | None = None,
|
|
255
|
+
agent_id: str | None = None,
|
|
256
|
+
user_id: str | None = None,
|
|
257
|
+
) -> int:
|
|
258
|
+
"""Delete a memory. Returns the number of rows removed (0 or 1)."""
|
|
259
|
+
raise NotImplementedError
|
|
260
|
+
|
|
261
|
+
def feedback(
|
|
262
|
+
self,
|
|
263
|
+
*,
|
|
264
|
+
memory_id: str | None = None,
|
|
265
|
+
external_id: str | None = None,
|
|
266
|
+
value: str = "up",
|
|
267
|
+
reason: str | None = None,
|
|
268
|
+
agent_id: str | None = None,
|
|
269
|
+
user_id: str | None = None,
|
|
270
|
+
) -> bool:
|
|
271
|
+
"""Send 👍/👎 on a memory. ``value`` is 'up' / 'down' / 'ignore'.
|
|
272
|
+
|
|
273
|
+
Returns True if the signal was recorded, False if no matching
|
|
274
|
+
memory was found.
|
|
275
|
+
"""
|
|
276
|
+
raise NotImplementedError
|
|
277
|
+
|
|
278
|
+
def list(
|
|
279
|
+
self,
|
|
280
|
+
*,
|
|
281
|
+
agent_id: str | None = None,
|
|
282
|
+
user_id: str | None = None,
|
|
283
|
+
session_id: str | None = None,
|
|
284
|
+
kind: str | None = None,
|
|
285
|
+
limit: int = 50,
|
|
286
|
+
) -> list[Memory]:
|
|
287
|
+
"""List recent memories with simple filters."""
|
|
288
|
+
raise NotImplementedError
|
|
289
|
+
|
|
290
|
+
def close(self) -> None:
|
|
291
|
+
"""Release any resources. Default is a no-op."""
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
# ---- context manager -------------------------------------------------
|
|
295
|
+
|
|
296
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
297
|
+
self.close()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
# In-process backend
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class _InProcessClient(MemoryClient):
|
|
306
|
+
def __init__(self, store) -> None:
|
|
307
|
+
super().__init__()
|
|
308
|
+
self._store = store
|
|
309
|
+
|
|
310
|
+
def remember(
|
|
311
|
+
self,
|
|
312
|
+
text: str,
|
|
313
|
+
*,
|
|
314
|
+
kind: str = "fact",
|
|
315
|
+
importance: float = 0.5,
|
|
316
|
+
tags: list[str] | None = None,
|
|
317
|
+
source: str | None = None,
|
|
318
|
+
session_id: str | None = None,
|
|
319
|
+
external_id: str | None = None,
|
|
320
|
+
agent_id: str | None = None,
|
|
321
|
+
user_id: str | None = None,
|
|
322
|
+
ttl: float | None = None,
|
|
323
|
+
created_at: float | None = None,
|
|
324
|
+
) -> Memory:
|
|
325
|
+
if not text or not text.strip():
|
|
326
|
+
raise ValueError("text is required")
|
|
327
|
+
row = self._store.upsert_memory(
|
|
328
|
+
kind=kind,
|
|
329
|
+
text=text.strip(),
|
|
330
|
+
importance=float(importance),
|
|
331
|
+
source=source,
|
|
332
|
+
session_id=session_id,
|
|
333
|
+
tags=tags or [],
|
|
334
|
+
agent_id=agent_id if agent_id is not None else self._default_agent_id,
|
|
335
|
+
user_id=user_id if user_id is not None else self._default_user_id,
|
|
336
|
+
external_id=external_id,
|
|
337
|
+
ttl=ttl,
|
|
338
|
+
created_at=created_at,
|
|
339
|
+
)
|
|
340
|
+
return _memory_from_stored(row)
|
|
341
|
+
|
|
342
|
+
def remember_batch(self, items: Iterable[dict[str, Any]]) -> list[Memory]:
|
|
343
|
+
return [self.remember(**item) for item in items]
|
|
344
|
+
|
|
345
|
+
def recall(
|
|
346
|
+
self,
|
|
347
|
+
query: str,
|
|
348
|
+
*,
|
|
349
|
+
limit: int = 8,
|
|
350
|
+
source: str | None = None,
|
|
351
|
+
agent_id: str | None = None,
|
|
352
|
+
user_id: str | None = None,
|
|
353
|
+
include: str = "memories,wiki,entities",
|
|
354
|
+
) -> RecallResult:
|
|
355
|
+
# ``recall_hybrid`` is the production code path; fall back to
|
|
356
|
+
# the legacy LIKE-based ``recall`` if a stale DB doesn't have
|
|
357
|
+
# it (older installs, tests).
|
|
358
|
+
wanted = tuple(s.strip() for s in include.split(",") if s.strip())
|
|
359
|
+
if hasattr(self._store, "recall_hybrid"):
|
|
360
|
+
r = self._store.recall_hybrid(
|
|
361
|
+
query, limit=limit, include=wanted,
|
|
362
|
+
bump_signals=True, source=source, level=1,
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
r = self._store.recall(
|
|
366
|
+
query,
|
|
367
|
+
limit=limit,
|
|
368
|
+
include=wanted,
|
|
369
|
+
source=source,
|
|
370
|
+
)
|
|
371
|
+
# Optional post-filter: even if the store returned hits, the
|
|
372
|
+
# caller may want only their agent's namespace.
|
|
373
|
+
def _own(h: dict[str, Any]) -> bool:
|
|
374
|
+
ag = h.get("agent_id")
|
|
375
|
+
ur = h.get("user_id")
|
|
376
|
+
# Global memories (no agent_id, no user_id) are visible
|
|
377
|
+
# to every caller. If either namespace is set, both must
|
|
378
|
+
# match the caller's filter or be unset on the memory.
|
|
379
|
+
if ag is None and ur is None:
|
|
380
|
+
return True
|
|
381
|
+
if agent_id is not None and ag not in (None, agent_id):
|
|
382
|
+
return False
|
|
383
|
+
if user_id is not None and ur not in (None, user_id):
|
|
384
|
+
return False
|
|
385
|
+
return True
|
|
386
|
+
return RecallResult(
|
|
387
|
+
query=query,
|
|
388
|
+
memories=[RecallHit.from_dict(m) for m in r.get("memories", []) if _own(m)],
|
|
389
|
+
wiki=[RecallHit.from_dict(w) for w in r.get("wiki", [])],
|
|
390
|
+
entities=[RecallHit.from_dict(e) for e in r.get("entities", [])],
|
|
391
|
+
temporal_intent=r.get("temporal_intent", "any"),
|
|
392
|
+
temporal_confidence=float(r.get("temporal_confidence", 0.0) or 0.0),
|
|
393
|
+
source=source,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def forget(
|
|
397
|
+
self,
|
|
398
|
+
*,
|
|
399
|
+
external_id: str | None = None,
|
|
400
|
+
memory_id: str | None = None,
|
|
401
|
+
agent_id: str | None = None,
|
|
402
|
+
user_id: str | None = None,
|
|
403
|
+
) -> int:
|
|
404
|
+
if not memory_id and not external_id:
|
|
405
|
+
raise ValueError("forget() needs memory_id or external_id")
|
|
406
|
+
mid = memory_id
|
|
407
|
+
if not mid and external_id:
|
|
408
|
+
target_agent = agent_id if agent_id is not None else self._default_agent_id
|
|
409
|
+
target_user = user_id if user_id is not None else self._default_user_id
|
|
410
|
+
row = self._store.find_memory_by_external_id(
|
|
411
|
+
target_agent or "", external_id, user_id=target_user,
|
|
412
|
+
)
|
|
413
|
+
if row is None:
|
|
414
|
+
return 0
|
|
415
|
+
mid = row.id
|
|
416
|
+
return self._store.delete_memory(mid)
|
|
417
|
+
|
|
418
|
+
def feedback(
|
|
419
|
+
self,
|
|
420
|
+
*,
|
|
421
|
+
memory_id: str | None = None,
|
|
422
|
+
external_id: str | None = None,
|
|
423
|
+
value: str = "up",
|
|
424
|
+
reason: str | None = None,
|
|
425
|
+
agent_id: str | None = None,
|
|
426
|
+
user_id: str | None = None,
|
|
427
|
+
) -> bool:
|
|
428
|
+
if not memory_id and not external_id:
|
|
429
|
+
raise ValueError("feedback() needs memory_id or external_id")
|
|
430
|
+
if not memory_id:
|
|
431
|
+
target_agent = agent_id if agent_id is not None else self._default_agent_id
|
|
432
|
+
target_user = user_id if user_id is not None else self._default_user_id
|
|
433
|
+
row = self._store.find_memory_by_external_id(
|
|
434
|
+
target_agent or "", external_id, user_id=target_user,
|
|
435
|
+
)
|
|
436
|
+
if row is None:
|
|
437
|
+
return False
|
|
438
|
+
memory_id = row.id
|
|
439
|
+
v = (value or "up").strip().lower()
|
|
440
|
+
if v not in ("up", "down", "ignore"):
|
|
441
|
+
raise ValueError("value must be up|down|ignore")
|
|
442
|
+
# record_signal is the store primitive; mirrors the HTTP
|
|
443
|
+
# feedback endpoint's behaviour. "ignore" is a soft-delete:
|
|
444
|
+
# record the negative signal then remove the row so future
|
|
445
|
+
# recalls stop surfacing it.
|
|
446
|
+
self._store.record_signal(memory_id, positive=(v == "up"))
|
|
447
|
+
if v == "ignore":
|
|
448
|
+
self._store.delete_memory(memory_id)
|
|
449
|
+
return True
|
|
450
|
+
|
|
451
|
+
def list(
|
|
452
|
+
self,
|
|
453
|
+
*,
|
|
454
|
+
agent_id: str | None = None,
|
|
455
|
+
user_id: str | None = None,
|
|
456
|
+
session_id: str | None = None,
|
|
457
|
+
kind: str | None = None,
|
|
458
|
+
limit: int = 50,
|
|
459
|
+
) -> list[Memory]:
|
|
460
|
+
rows = self._store.list_memories(
|
|
461
|
+
agent_id=agent_id if agent_id is not None else self._default_agent_id,
|
|
462
|
+
user_id=user_id if user_id is not None else self._default_user_id,
|
|
463
|
+
session_id=session_id,
|
|
464
|
+
kind=kind,
|
|
465
|
+
limit=limit,
|
|
466
|
+
)
|
|
467
|
+
return [_memory_from_stored(r) for r in rows]
|
|
468
|
+
|
|
469
|
+
# ---- graph ----------------------------------------------------
|
|
470
|
+
|
|
471
|
+
def remember_edge(self, src: str, dst: str, *,
|
|
472
|
+
kind: str = "relates_to", weight: float = 0.5,
|
|
473
|
+
evidence_id: str | None = None) -> GraphHit:
|
|
474
|
+
from .jobs.graph import upsert_semantic_edge
|
|
475
|
+
info = upsert_semantic_edge(
|
|
476
|
+
self._store, src, dst, kind=kind, weight=weight,
|
|
477
|
+
evidence_id=evidence_id,
|
|
478
|
+
)
|
|
479
|
+
return GraphHit(kind=info["kind"], src=info["src"],
|
|
480
|
+
dst=info["dst"], weight=info["weight"],
|
|
481
|
+
evidence_id=info.get("evidence_id"))
|
|
482
|
+
|
|
483
|
+
def subgraph(self, query: str, *, max_nodes: int = 32,
|
|
484
|
+
max_edges: int = 64) -> SubgraphView:
|
|
485
|
+
from .jobs.graph import subgraph_for
|
|
486
|
+
sg = subgraph_for(self._store, query,
|
|
487
|
+
max_nodes=max_nodes, max_edges=max_edges)
|
|
488
|
+
return SubgraphView.from_dict(sg.to_dict())
|
|
489
|
+
|
|
490
|
+
def rebuild_graph(self) -> int:
|
|
491
|
+
from .graph.build import KnowledgeGraph
|
|
492
|
+
KnowledgeGraph(self._store).rebuild(clear=True)
|
|
493
|
+
return self._store.rebuild_entity_mentions()
|
|
494
|
+
|
|
495
|
+
def recall_adaptive(self, query: str, *, limit: int = 8, **kwargs) -> Any:
|
|
496
|
+
if not hasattr(self._store, "recall_hybrid"):
|
|
497
|
+
return self.recall(query, limit=limit, **kwargs)
|
|
498
|
+
include = ("memories", "wiki", "entities")
|
|
499
|
+
if "include" in kwargs:
|
|
500
|
+
raw = kwargs.pop("include")
|
|
501
|
+
include = tuple(s.strip() for s in raw.split(",") if s.strip()) or include
|
|
502
|
+
r = self._store.recall_hybrid(
|
|
503
|
+
query, limit=limit, include=include, bump_signals=True,
|
|
504
|
+
level=1, adaptive=True,
|
|
505
|
+
)
|
|
506
|
+
return RecallResult(
|
|
507
|
+
query=query,
|
|
508
|
+
memories=[RecallHit.from_dict(m) for m in r.get("memories", [])],
|
|
509
|
+
wiki=[RecallHit.from_dict(w) for w in r.get("wiki", [])],
|
|
510
|
+
entities=[RecallHit.from_dict(e) for e in r.get("entities", [])],
|
|
511
|
+
temporal_intent=r.get("temporal_intent", "any"),
|
|
512
|
+
temporal_confidence=float(r.get("temporal_confidence", 0.0) or 0.0),
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# ---- cognitive ------------------------------------------------
|
|
516
|
+
|
|
517
|
+
def cognitive_sleep(self, *, apply: bool = False, **kwargs) -> CognitiveReportView:
|
|
518
|
+
from .jobs.cognitive import cognitive_sleep
|
|
519
|
+
rpt = cognitive_sleep(self._store, apply=apply, **kwargs)
|
|
520
|
+
return CognitiveReportView.from_dict(rpt.to_dict())
|
|
521
|
+
|
|
522
|
+
def audit(self, *, kind: str | None = None, action: str | None = None,
|
|
523
|
+
limit: int = 200) -> list[CognitiveActionView]:
|
|
524
|
+
rows = self._store.list_audit(kind=kind, action=action, limit=limit)
|
|
525
|
+
return [CognitiveActionView.from_dict(r) for r in rows]
|
|
526
|
+
|
|
527
|
+
def revert_audit(self, audit_id: str) -> bool:
|
|
528
|
+
self._store.record_audit(
|
|
529
|
+
kind="revert", action="reverted",
|
|
530
|
+
target_kind="memory", target_id=audit_id,
|
|
531
|
+
reason="user marked audit row as reverted",
|
|
532
|
+
)
|
|
533
|
+
return True
|
|
534
|
+
|
|
535
|
+
# ---- export / import / fork -----------------------------------
|
|
536
|
+
|
|
537
|
+
def export(self, out_dir: str, *, agent_id: str | None = None,
|
|
538
|
+
user_id: str | None = None, scope: str = "global",
|
|
539
|
+
min_importance: float = 0.0) -> ExportView:
|
|
540
|
+
from .export import export_bundle
|
|
541
|
+
a = agent_id if agent_id is not None else self._default_agent_id
|
|
542
|
+
u = user_id if user_id is not None else self._default_user_id
|
|
543
|
+
r = export_bundle(self._store, out_dir, agent_id=a, user_id=u,
|
|
544
|
+
scope=scope, min_importance=min_importance)
|
|
545
|
+
return ExportView.from_dict(r.to_dict())
|
|
546
|
+
|
|
547
|
+
def import_bundle(self, in_dir: str, *, agent_id: str | None = None,
|
|
548
|
+
user_id: str | None = None, dry_run: bool = False) -> ImportView:
|
|
549
|
+
from .export import import_bundle
|
|
550
|
+
a = agent_id if agent_id is not None else self._default_agent_id
|
|
551
|
+
u = user_id if user_id is not None else self._default_user_id
|
|
552
|
+
r = import_bundle(self._store, in_dir, agent_id=a, user_id=u,
|
|
553
|
+
dry_run=dry_run)
|
|
554
|
+
return ImportView.from_dict(r.to_dict())
|
|
555
|
+
|
|
556
|
+
def fork(self, branch_tag: str | None = None) -> dict[str, Any]:
|
|
557
|
+
from .export import fork_snapshot
|
|
558
|
+
return fork_snapshot(self._store, branch_tag=branch_tag)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _memory_from_stored(row) -> Memory:
|
|
563
|
+
return Memory(
|
|
564
|
+
id=row.id,
|
|
565
|
+
text=row.text,
|
|
566
|
+
kind=row.kind,
|
|
567
|
+
importance=float(row.importance or 0.0),
|
|
568
|
+
score=float(row.score or 0.0),
|
|
569
|
+
source=row.source,
|
|
570
|
+
session_id=row.session_id,
|
|
571
|
+
agent_id=getattr(row, "agent_id", None),
|
|
572
|
+
user_id=getattr(row, "user_id", None),
|
|
573
|
+
external_id=getattr(row, "external_id", None),
|
|
574
|
+
tags=list(row.tags or []),
|
|
575
|
+
created_at=float(row.created_at or 0.0),
|
|
576
|
+
updated_at=float(getattr(row, "updated_at", 0.0) or 0.0),
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# ---------------------------------------------------------------------------
|
|
581
|
+
# HTTP backend (zero-dep, stdlib only)
|
|
582
|
+
# ---------------------------------------------------------------------------
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class _HttpClient(MemoryClient):
|
|
586
|
+
def __init__(self, base_url: str = "http://127.0.0.1:7767", timeout: float = 10.0):
|
|
587
|
+
super().__init__()
|
|
588
|
+
self._base = base_url.rstrip("/")
|
|
589
|
+
self._timeout = timeout
|
|
590
|
+
|
|
591
|
+
def _request(self, method: str, path: str, body: dict | None = None) -> dict:
|
|
592
|
+
url = f"{self._base}{path}"
|
|
593
|
+
data = None
|
|
594
|
+
headers = {"Accept": "application/json"}
|
|
595
|
+
if body is not None:
|
|
596
|
+
data = json.dumps(body).encode("utf-8")
|
|
597
|
+
headers["Content-Type"] = "application/json"
|
|
598
|
+
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
|
599
|
+
try:
|
|
600
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
601
|
+
raw = resp.read().decode("utf-8")
|
|
602
|
+
except urllib.error.HTTPError as e:
|
|
603
|
+
raw = e.read().decode("utf-8", errors="ignore")
|
|
604
|
+
raise MemoryClientError(
|
|
605
|
+
f"{method} {path} → {e.code}: {raw[:400]}"
|
|
606
|
+
) from e
|
|
607
|
+
except urllib.error.URLError as e:
|
|
608
|
+
raise MemoryClientError(f"cannot reach {self._base}: {e}") from e
|
|
609
|
+
if not raw.strip():
|
|
610
|
+
return {}
|
|
611
|
+
try:
|
|
612
|
+
return json.loads(raw)
|
|
613
|
+
except json.JSONDecodeError as e:
|
|
614
|
+
raise MemoryClientError(f"invalid JSON from {path}: {e}") from e
|
|
615
|
+
|
|
616
|
+
def remember(
|
|
617
|
+
self,
|
|
618
|
+
text: str,
|
|
619
|
+
*,
|
|
620
|
+
kind: str = "fact",
|
|
621
|
+
importance: float = 0.5,
|
|
622
|
+
tags: list[str] | None = None,
|
|
623
|
+
source: str | None = None,
|
|
624
|
+
session_id: str | None = None,
|
|
625
|
+
external_id: str | None = None,
|
|
626
|
+
agent_id: str | None = None,
|
|
627
|
+
user_id: str | None = None,
|
|
628
|
+
ttl: float | None = None,
|
|
629
|
+
created_at: float | None = None,
|
|
630
|
+
) -> Memory:
|
|
631
|
+
if not text or not text.strip():
|
|
632
|
+
raise ValueError("text is required")
|
|
633
|
+
body = {
|
|
634
|
+
"text": text.strip(),
|
|
635
|
+
"kind": kind,
|
|
636
|
+
"importance": float(importance),
|
|
637
|
+
"tags": list(tags or []),
|
|
638
|
+
"source": source,
|
|
639
|
+
"session_id": session_id,
|
|
640
|
+
"external_id": external_id,
|
|
641
|
+
"agent_id": agent_id if agent_id is not None else self._default_agent_id,
|
|
642
|
+
"user_id": user_id if user_id is not None else self._default_user_id,
|
|
643
|
+
}
|
|
644
|
+
if ttl is not None:
|
|
645
|
+
body["ttl"] = float(ttl)
|
|
646
|
+
if created_at is not None:
|
|
647
|
+
body["created_at"] = float(created_at)
|
|
648
|
+
resp = self._request("POST", "/api/v1/memories", body)
|
|
649
|
+
return Memory.from_dict(resp)
|
|
650
|
+
|
|
651
|
+
def remember_batch(self, items: Iterable[dict[str, Any]]) -> list[Memory]:
|
|
652
|
+
resp = self._request("POST", "/api/v1/memories:batch", {"items": list(items)})
|
|
653
|
+
return [Memory.from_dict(m) for m in resp.get("items", [])]
|
|
654
|
+
|
|
655
|
+
def recall(
|
|
656
|
+
self,
|
|
657
|
+
query: str,
|
|
658
|
+
*,
|
|
659
|
+
limit: int = 8,
|
|
660
|
+
source: str | None = None,
|
|
661
|
+
agent_id: str | None = None,
|
|
662
|
+
user_id: str | None = None,
|
|
663
|
+
include: str = "memories,wiki,entities",
|
|
664
|
+
) -> RecallResult:
|
|
665
|
+
params = {
|
|
666
|
+
"q": query,
|
|
667
|
+
"limit": int(limit),
|
|
668
|
+
"include": include,
|
|
669
|
+
"source": source or "",
|
|
670
|
+
}
|
|
671
|
+
if agent_id is not None:
|
|
672
|
+
params["agent_id"] = agent_id
|
|
673
|
+
if user_id is not None:
|
|
674
|
+
params["user_id"] = user_id
|
|
675
|
+
qs = urllib.parse.urlencode({k: v for k, v in params.items() if v != ""})
|
|
676
|
+
resp = self._request("GET", f"/api/v1/recall?{qs}")
|
|
677
|
+
return RecallResult(
|
|
678
|
+
query=query,
|
|
679
|
+
memories=[RecallHit.from_dict(m) for m in resp.get("memories", [])],
|
|
680
|
+
wiki=[RecallHit.from_dict(w) for w in resp.get("wiki", [])],
|
|
681
|
+
entities=[RecallHit.from_dict(e) for e in resp.get("entities", [])],
|
|
682
|
+
temporal_intent=resp.get("temporal_intent", "any"),
|
|
683
|
+
temporal_confidence=float(resp.get("temporal_confidence", 0.0) or 0.0),
|
|
684
|
+
source=source,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
def forget(
|
|
688
|
+
self,
|
|
689
|
+
*,
|
|
690
|
+
external_id: str | None = None,
|
|
691
|
+
memory_id: str | None = None,
|
|
692
|
+
agent_id: str | None = None,
|
|
693
|
+
user_id: str | None = None,
|
|
694
|
+
) -> int:
|
|
695
|
+
if memory_id:
|
|
696
|
+
resp = self._request("DELETE", f"/api/v1/memories/{memory_id}")
|
|
697
|
+
return int(resp.get("deleted", 0) or 0)
|
|
698
|
+
if external_id:
|
|
699
|
+
params = {"external_id": external_id}
|
|
700
|
+
if agent_id is not None:
|
|
701
|
+
params["agent_id"] = agent_id
|
|
702
|
+
elif self._default_agent_id is not None:
|
|
703
|
+
params["agent_id"] = self._default_agent_id
|
|
704
|
+
if user_id is not None:
|
|
705
|
+
params["user_id"] = user_id
|
|
706
|
+
elif self._default_user_id is not None:
|
|
707
|
+
params["user_id"] = self._default_user_id
|
|
708
|
+
qs = urllib.parse.urlencode(params)
|
|
709
|
+
resp = self._request("DELETE", f"/api/v1/memories?{qs}")
|
|
710
|
+
return int(resp.get("deleted", 0) or 0)
|
|
711
|
+
raise ValueError("forget() needs memory_id or external_id")
|
|
712
|
+
|
|
713
|
+
def feedback(
|
|
714
|
+
self,
|
|
715
|
+
*,
|
|
716
|
+
memory_id: str | None = None,
|
|
717
|
+
external_id: str | None = None,
|
|
718
|
+
value: str = "up",
|
|
719
|
+
reason: str | None = None,
|
|
720
|
+
agent_id: str | None = None,
|
|
721
|
+
user_id: str | None = None,
|
|
722
|
+
) -> bool:
|
|
723
|
+
if memory_id:
|
|
724
|
+
body = {"value": value, "reason": reason}
|
|
725
|
+
self._request("POST", f"/api/v1/memories/{memory_id}/feedback", body)
|
|
726
|
+
return True
|
|
727
|
+
if external_id:
|
|
728
|
+
body = {
|
|
729
|
+
"value": value,
|
|
730
|
+
"reason": reason,
|
|
731
|
+
"external_id": external_id,
|
|
732
|
+
"agent_id": agent_id if agent_id is not None else self._default_agent_id,
|
|
733
|
+
"user_id": user_id if user_id is not None else self._default_user_id,
|
|
734
|
+
}
|
|
735
|
+
resp = self._request("POST", "/api/v1/memories/feedback", body)
|
|
736
|
+
return bool(resp.get("ok", False))
|
|
737
|
+
raise ValueError("feedback() needs memory_id or external_id")
|
|
738
|
+
|
|
739
|
+
def list(
|
|
740
|
+
self,
|
|
741
|
+
*,
|
|
742
|
+
agent_id: str | None = None,
|
|
743
|
+
user_id: str | None = None,
|
|
744
|
+
session_id: str | None = None,
|
|
745
|
+
kind: str | None = None,
|
|
746
|
+
limit: int = 50,
|
|
747
|
+
) -> list[Memory]:
|
|
748
|
+
params: dict[str, Any] = {"limit": int(limit)}
|
|
749
|
+
if agent_id is not None:
|
|
750
|
+
params["agent_id"] = agent_id
|
|
751
|
+
elif self._default_agent_id is not None:
|
|
752
|
+
params["agent_id"] = self._default_agent_id
|
|
753
|
+
if user_id is not None:
|
|
754
|
+
params["user_id"] = user_id
|
|
755
|
+
elif self._default_user_id is not None:
|
|
756
|
+
params["user_id"] = self._default_user_id
|
|
757
|
+
if session_id is not None:
|
|
758
|
+
params["session_id"] = session_id
|
|
759
|
+
if kind is not None:
|
|
760
|
+
params["kind"] = kind
|
|
761
|
+
qs = urllib.parse.urlencode(params)
|
|
762
|
+
resp = self._request("GET", f"/api/v1/memories?{qs}")
|
|
763
|
+
return [Memory.from_dict(m) for m in resp.get("memories", [])]
|
|
764
|
+
|
|
765
|
+
# ---- graph ----------------------------------------------------
|
|
766
|
+
|
|
767
|
+
def remember_edge(self, src: str, dst: str, *,
|
|
768
|
+
kind: str = "relates_to", weight: float = 0.5,
|
|
769
|
+
evidence_id: str | None = None) -> GraphHit:
|
|
770
|
+
body = {"src": src, "dst": dst, "kind": kind, "weight": weight}
|
|
771
|
+
if evidence_id is not None:
|
|
772
|
+
body["evidence_id"] = evidence_id
|
|
773
|
+
resp = self._request("POST", "/api/v1/graph/edges", body)
|
|
774
|
+
return GraphHit.from_dict(resp)
|
|
775
|
+
|
|
776
|
+
def subgraph(self, query: str, *, max_nodes: int = 32,
|
|
777
|
+
max_edges: int = 64) -> SubgraphView:
|
|
778
|
+
qs = urllib.parse.urlencode({
|
|
779
|
+
"q": query, "max_nodes": max_nodes, "max_edges": max_edges,
|
|
780
|
+
})
|
|
781
|
+
resp = self._request("GET", f"/api/v1/graph/subgraph?{qs}")
|
|
782
|
+
return SubgraphView.from_dict(resp)
|
|
783
|
+
|
|
784
|
+
def rebuild_graph(self) -> int:
|
|
785
|
+
resp = self._request("POST", "/api/v1/graph/rebuild", {})
|
|
786
|
+
return int(resp.get("entity_mentions", 0) or 0)
|
|
787
|
+
|
|
788
|
+
def recall_adaptive(self, query: str, *, limit: int = 8, **kwargs) -> Any:
|
|
789
|
+
params = {
|
|
790
|
+
"q": query, "limit": int(limit),
|
|
791
|
+
"include": kwargs.pop("include", "memories,wiki,entities"),
|
|
792
|
+
"adaptive": 1,
|
|
793
|
+
}
|
|
794
|
+
for k, v in kwargs.items():
|
|
795
|
+
if v is not None:
|
|
796
|
+
params[k] = v
|
|
797
|
+
qs = urllib.parse.urlencode({k: v for k, v in params.items() if v != ""})
|
|
798
|
+
r = self._request("GET", f"/api/v1/recall?{qs}")
|
|
799
|
+
return RecallResult(
|
|
800
|
+
query=query,
|
|
801
|
+
memories=[RecallHit.from_dict(m) for m in r.get("memories", [])],
|
|
802
|
+
wiki=[RecallHit.from_dict(w) for w in r.get("wiki", [])],
|
|
803
|
+
entities=[RecallHit.from_dict(e) for e in r.get("entities", [])],
|
|
804
|
+
temporal_intent=r.get("temporal_intent", "any"),
|
|
805
|
+
temporal_confidence=float(r.get("temporal_confidence", 0.0) or 0.0),
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
# ---- cognitive ------------------------------------------------
|
|
809
|
+
|
|
810
|
+
def cognitive_sleep(self, *, apply: bool = False, **kwargs) -> CognitiveReportView:
|
|
811
|
+
body = {"apply": bool(apply), **{k: v for k, v in kwargs.items() if v is not None}}
|
|
812
|
+
resp = self._request("POST", "/api/v1/cognitive/sleep", body)
|
|
813
|
+
return CognitiveReportView.from_dict(resp)
|
|
814
|
+
|
|
815
|
+
def audit(self, *, kind: str | None = None, action: str | None = None,
|
|
816
|
+
limit: int = 200) -> list[CognitiveActionView]:
|
|
817
|
+
params: dict[str, Any] = {"limit": int(limit)}
|
|
818
|
+
if kind is not None:
|
|
819
|
+
params["kind"] = kind
|
|
820
|
+
if action is not None:
|
|
821
|
+
params["action"] = action
|
|
822
|
+
qs = urllib.parse.urlencode(params)
|
|
823
|
+
resp = self._request("GET", f"/api/v1/cognitive/audit?{qs}")
|
|
824
|
+
return [CognitiveActionView.from_dict(r) for r in resp.get("rows", [])]
|
|
825
|
+
|
|
826
|
+
def revert_audit(self, audit_id: str) -> bool:
|
|
827
|
+
resp = self._request("POST", "/api/v1/cognitive/audit/revert",
|
|
828
|
+
{"id": audit_id})
|
|
829
|
+
return bool(resp.get("ok", False))
|
|
830
|
+
|
|
831
|
+
# ---- export / import / fork -----------------------------------
|
|
832
|
+
|
|
833
|
+
def export(self, out_dir: str, *, agent_id: str | None = None,
|
|
834
|
+
user_id: str | None = None, scope: str = "global",
|
|
835
|
+
min_importance: float = 0.0) -> ExportView:
|
|
836
|
+
body = {
|
|
837
|
+
"out_dir": out_dir,
|
|
838
|
+
"agent_id": agent_id,
|
|
839
|
+
"user_id": user_id,
|
|
840
|
+
"scope": scope,
|
|
841
|
+
"min_importance": float(min_importance),
|
|
842
|
+
}
|
|
843
|
+
body = {k: v for k, v in body.items() if v is not None and v != ""}
|
|
844
|
+
resp = self._request("POST", "/api/v1/export", body)
|
|
845
|
+
return ExportView.from_dict(resp)
|
|
846
|
+
|
|
847
|
+
def import_bundle(self, in_dir: str, *, agent_id: str | None = None,
|
|
848
|
+
user_id: str | None = None, dry_run: bool = False) -> ImportView:
|
|
849
|
+
body = {
|
|
850
|
+
"in_dir": in_dir,
|
|
851
|
+
"agent_id": agent_id,
|
|
852
|
+
"user_id": user_id,
|
|
853
|
+
"dry_run": bool(dry_run),
|
|
854
|
+
}
|
|
855
|
+
body = {k: v for k, v in body.items() if v is not None and v != ""}
|
|
856
|
+
resp = self._request("POST", "/api/v1/import", body)
|
|
857
|
+
return ImportView.from_dict(resp)
|
|
858
|
+
|
|
859
|
+
def fork(self, branch_tag: str | None = None) -> dict[str, Any]:
|
|
860
|
+
body = {"branch_tag": branch_tag} if branch_tag else {}
|
|
861
|
+
return self._request("POST", "/api/v1/fork", body)
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
# ---------------------------------------------------------------------------
|
|
865
|
+
# Module exports
|
|
866
|
+
# ---------------------------------------------------------------------------
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
__all__ = [
|
|
870
|
+
"Memory",
|
|
871
|
+
"RecallHit",
|
|
872
|
+
"RecallResult",
|
|
873
|
+
"MemoryClient",
|
|
874
|
+
"MemoryClientError",
|
|
875
|
+
]
|