autogen-memorysync 1.0.0__tar.gz

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.
@@ -0,0 +1,5 @@
1
+ venv/
2
+ dist/
3
+ __pycache__/
4
+ *.egg-info/
5
+ .pytest_cache/
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.5
2
+ Name: autogen-memorysync
3
+ Version: 1.0.0
4
+ Summary: MemorySync for Microsoft AutoGen: an async-native Memory implementation with a hard recall budget, role-aware retrieval, duplicate-proof persistence, and session-scoped clear.
5
+ Project-URL: Homepage, https://docs.memorysync.io/guides/autogen
6
+ Project-URL: Documentation, https://docs.memorysync.io/guides/autogen
7
+ Project-URL: Repository, https://github.com/Rafay121/memorysync-plugins
8
+ Author-email: MemorySync <support@memorysync.io>
9
+ License-Expression: MIT
10
+ Keywords: agents,autogen,autogen-agentchat,long-term-memory,memory,memorysync
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: autogen-core<0.8,>=0.4.0
21
+ Requires-Dist: httpx<1,>=0.25
22
+ Description-Content-Type: text/markdown
23
+
24
+ # autogen-memorysync
25
+
26
+ [MemorySync](https://memorysync.io) for [Microsoft AutoGen](https://github.com/microsoft/autogen)
27
+ (`autogen-agentchat` 0.4+): agents that remember users across sessions —
28
+ without ever stalling a turn.
29
+
30
+ ```bash
31
+ pip install autogen-memorysync
32
+ ```
33
+
34
+ ## Quick start
35
+
36
+ ```python
37
+ from autogen_agentchat.agents import AssistantAgent
38
+ from autogen_memorysync import MemorySyncMemory
39
+
40
+ memory = MemorySyncMemory(
41
+ api_key="ms_...", # or MEMORYSYNC_API_KEY
42
+ user_id="customer-42", # required — who these memories belong to
43
+ session_id="support-chat", # scopes the transcript
44
+ )
45
+
46
+ agent = AssistantAgent("assistant", model_client=..., memory=[memory])
47
+
48
+ result = await agent.run(task=user_text)
49
+ # AutoGen never persists automatically — capture the exchange in one call:
50
+ await memory.add_turn_pair(user_text, result.messages[-1].content)
51
+ ```
52
+
53
+ `update_context` runs automatically before every model call: relevant
54
+ memories are recalled and injected as a `SystemMessage`, and the
55
+ retrieval surfaces to observers as a `MemoryQueryEvent`.
56
+
57
+ ## Why this one
58
+
59
+ | | Mem0 (`autogen-ext[mem0]`) | Zep (`zep-autogen`) | **MemorySync** |
60
+ | --- | --- | --- | --- |
61
+ | Async correctness | ✗ sync client inside `async def` — blocks the event loop | ✓ | ✓ `httpx.AsyncClient` throughout |
62
+ | Recall latency budget | ✗ none | ✗ none | ✓ hard 1.2s default — a slow backend means an unenriched turn, never a late one |
63
+ | Retrieval query | ✗ `messages[-1]` even when it's assistant/tool text | last context message | ✓ last **user** message, role-aware |
64
+ | Query errors | ✗ swallowed — outage looks like amnesia | logged | ✓ explicit `query()` raises; only the hot path fails open |
65
+ | `clear()` blast radius | ✗ entire user | ✗ entire user | ✓ **session-scoped by default**; whole-user wipe is an explicit opt-in |
66
+ | Retry safety | ✗ | ✗ | ✓ deterministic idempotency seeds — retries converge on one row |
67
+ | Caller's metadata dict | ✗ mutated (`pop`) | — | ✓ copied, never touched |
68
+ | `close()` | `pass` | ✓ | ✓ releases the HTTP client |
69
+
70
+ ## Semantics worth knowing
71
+
72
+ - **The turn is never stalled and never broken.** Recall waits at most
73
+ `recall_timeout` (default 1.2s); on timeout, outage, or quota
74
+ exhaustion the agent simply answers without memories.
75
+ - **`user_id` is required.** No silent auto-generated UUID namespaces
76
+ where stored memories can never be found again.
77
+ - Turns store verbatim under the `autogen::<session>` transcript scope —
78
+ separate history, same shared user memories as every other MemorySync
79
+ surface.
80
+ - Free-tier quota exhaustion is silent by design (adds accepted-without-
81
+ storing, reads empty); evaluation keys surface strict `429`s.
82
+ - `create_memory_tools(memory)` returns `search_memory` + `save_memory`
83
+ `FunctionTool`s for tool-equipped agents.
84
+
85
+ ## Configuration
86
+
87
+ | Parameter | Default | Meaning |
88
+ | --- | --- | --- |
89
+ | `user_id` | — (required) | End user the memories belong to |
90
+ | `session_id` | `"default"` | Transcript scope |
91
+ | `top_k` | `5` | Memories considered per turn |
92
+ | `recall_timeout` | `1.2` | Hard recall budget, seconds |
93
+ | `min_prompt_chars` | `8` | Skip recall for trivial prompts |
94
+ | `context_template` | built-in | `{context}` placeholder, brace-safe `.replace` rendering |
95
+ | `clear_scope` | `"session"` | `clear()` blast radius; `"user"` opt-in wipes everything |
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ pip install -e . autogen-agentchat autogen-ext pytest pytest-asyncio
101
+ python -m pytest tests -q # 33 tests incl. a REAL AssistantAgent drive
102
+ ```
103
+
104
+ Note: `autogen-agentchat` is in maintenance mode (Microsoft's successor
105
+ is the Microsoft Agent Framework — a separate MemorySync adapter target).
106
+ Maintenance mode means the `Memory` protocol this package implements is
107
+ frozen and stable.
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,88 @@
1
+ # autogen-memorysync
2
+
3
+ [MemorySync](https://memorysync.io) for [Microsoft AutoGen](https://github.com/microsoft/autogen)
4
+ (`autogen-agentchat` 0.4+): agents that remember users across sessions —
5
+ without ever stalling a turn.
6
+
7
+ ```bash
8
+ pip install autogen-memorysync
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from autogen_agentchat.agents import AssistantAgent
15
+ from autogen_memorysync import MemorySyncMemory
16
+
17
+ memory = MemorySyncMemory(
18
+ api_key="ms_...", # or MEMORYSYNC_API_KEY
19
+ user_id="customer-42", # required — who these memories belong to
20
+ session_id="support-chat", # scopes the transcript
21
+ )
22
+
23
+ agent = AssistantAgent("assistant", model_client=..., memory=[memory])
24
+
25
+ result = await agent.run(task=user_text)
26
+ # AutoGen never persists automatically — capture the exchange in one call:
27
+ await memory.add_turn_pair(user_text, result.messages[-1].content)
28
+ ```
29
+
30
+ `update_context` runs automatically before every model call: relevant
31
+ memories are recalled and injected as a `SystemMessage`, and the
32
+ retrieval surfaces to observers as a `MemoryQueryEvent`.
33
+
34
+ ## Why this one
35
+
36
+ | | Mem0 (`autogen-ext[mem0]`) | Zep (`zep-autogen`) | **MemorySync** |
37
+ | --- | --- | --- | --- |
38
+ | Async correctness | ✗ sync client inside `async def` — blocks the event loop | ✓ | ✓ `httpx.AsyncClient` throughout |
39
+ | Recall latency budget | ✗ none | ✗ none | ✓ hard 1.2s default — a slow backend means an unenriched turn, never a late one |
40
+ | Retrieval query | ✗ `messages[-1]` even when it's assistant/tool text | last context message | ✓ last **user** message, role-aware |
41
+ | Query errors | ✗ swallowed — outage looks like amnesia | logged | ✓ explicit `query()` raises; only the hot path fails open |
42
+ | `clear()` blast radius | ✗ entire user | ✗ entire user | ✓ **session-scoped by default**; whole-user wipe is an explicit opt-in |
43
+ | Retry safety | ✗ | ✗ | ✓ deterministic idempotency seeds — retries converge on one row |
44
+ | Caller's metadata dict | ✗ mutated (`pop`) | — | ✓ copied, never touched |
45
+ | `close()` | `pass` | ✓ | ✓ releases the HTTP client |
46
+
47
+ ## Semantics worth knowing
48
+
49
+ - **The turn is never stalled and never broken.** Recall waits at most
50
+ `recall_timeout` (default 1.2s); on timeout, outage, or quota
51
+ exhaustion the agent simply answers without memories.
52
+ - **`user_id` is required.** No silent auto-generated UUID namespaces
53
+ where stored memories can never be found again.
54
+ - Turns store verbatim under the `autogen::<session>` transcript scope —
55
+ separate history, same shared user memories as every other MemorySync
56
+ surface.
57
+ - Free-tier quota exhaustion is silent by design (adds accepted-without-
58
+ storing, reads empty); evaluation keys surface strict `429`s.
59
+ - `create_memory_tools(memory)` returns `search_memory` + `save_memory`
60
+ `FunctionTool`s for tool-equipped agents.
61
+
62
+ ## Configuration
63
+
64
+ | Parameter | Default | Meaning |
65
+ | --- | --- | --- |
66
+ | `user_id` | — (required) | End user the memories belong to |
67
+ | `session_id` | `"default"` | Transcript scope |
68
+ | `top_k` | `5` | Memories considered per turn |
69
+ | `recall_timeout` | `1.2` | Hard recall budget, seconds |
70
+ | `min_prompt_chars` | `8` | Skip recall for trivial prompts |
71
+ | `context_template` | built-in | `{context}` placeholder, brace-safe `.replace` rendering |
72
+ | `clear_scope` | `"session"` | `clear()` blast radius; `"user"` opt-in wipes everything |
73
+
74
+ ## Development
75
+
76
+ ```bash
77
+ pip install -e . autogen-agentchat autogen-ext pytest pytest-asyncio
78
+ python -m pytest tests -q # 33 tests incl. a REAL AssistantAgent drive
79
+ ```
80
+
81
+ Note: `autogen-agentchat` is in maintenance mode (Microsoft's successor
82
+ is the Microsoft Agent Framework — a separate MemorySync adapter target).
83
+ Maintenance mode means the `Memory` protocol this package implements is
84
+ frozen and stable.
85
+
86
+ ## License
87
+
88
+ MIT
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "autogen-memorysync"
7
+ dynamic = ["version"]
8
+ description = "MemorySync for Microsoft AutoGen: an async-native Memory implementation with a hard recall budget, role-aware retrieval, duplicate-proof persistence, and session-scoped clear."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "MemorySync", email = "support@memorysync.io" }]
13
+ keywords = ["autogen", "autogen-agentchat", "agents", "memory", "memorysync", "long-term-memory"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ ]
24
+ dependencies = [
25
+ "autogen-core>=0.4.0,<0.8",
26
+ "httpx>=0.25,<1",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://docs.memorysync.io/guides/autogen"
31
+ Documentation = "https://docs.memorysync.io/guides/autogen"
32
+ Repository = "https://github.com/Rafay121/memorysync-plugins"
33
+
34
+ [tool.hatch.version]
35
+ path = "src/autogen_memorysync/_version.py"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/autogen_memorysync"]
39
+
40
+ [tool.pytest.ini_options]
41
+ asyncio_mode = "auto"
42
+ testpaths = ["tests"]
@@ -0,0 +1,20 @@
1
+ """MemorySync for Microsoft AutoGen (autogen-agentchat 0.4+)."""
2
+
3
+ from ._api import MemorySyncAPIError, fnv1a64
4
+ from ._version import __version__
5
+ from .memory import (
6
+ DEFAULT_CONTEXT_TEMPLATE,
7
+ MemorySyncMemory,
8
+ MemorySyncMemoryConfig,
9
+ )
10
+ from .tools import create_memory_tools
11
+
12
+ __all__ = [
13
+ "MemorySyncMemory",
14
+ "MemorySyncMemoryConfig",
15
+ "MemorySyncAPIError",
16
+ "DEFAULT_CONTEXT_TEMPLATE",
17
+ "create_memory_tools",
18
+ "fnv1a64",
19
+ "__version__",
20
+ ]
@@ -0,0 +1,311 @@
1
+ """Async client for the MemorySync v1 data plane used by this adapter.
2
+
3
+ Conversation turns persist through the *episodic* ingestion path
4
+ (``POST /v1/memory/add_turn``), which stores text verbatim — no fact
5
+ extraction, no low-value-chatter gate, no rewriting. An agent transcript
6
+ must round-trip byte-for-byte; a plane that second-guessed it would
7
+ corrupt the user's history.
8
+
9
+ Everything here is async-native because AutoGen's ``Memory`` protocol is
10
+ fully async — a blocking HTTP client inside ``update_context`` would
11
+ stall the event loop for every agent in the process (the exact bug in
12
+ the Mem0 adapter that ships inside autogen-ext).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ import httpx
21
+
22
+ from ._version import __version__
23
+
24
+ DEFAULT_BASE_URL = "https://api.memorysync.io"
25
+ _USER_AGENT = f"autogen-memorysync/{__version__}"
26
+
27
+ #: Namespace used when the key cannot list projects (see resolve_tenant_id).
28
+ FALLBACK_TENANT = "default"
29
+
30
+ #: One turn beyond this length is truncated before storage.
31
+ MAX_TURN_CHARS = 16000
32
+
33
+
34
+ class MemorySyncAPIError(Exception):
35
+ """A MemorySync call failed. Carries the status code and server detail."""
36
+
37
+ def __init__(self, message: str, *, status_code: Optional[int] = None) -> None:
38
+ super().__init__(message)
39
+ self.status_code = status_code
40
+
41
+
42
+ def resolve_api_key(api_key: Optional[str]) -> str:
43
+ key = api_key or os.environ.get("MEMORYSYNC_API_KEY", "")
44
+ if not key or not key.strip():
45
+ raise ValueError(
46
+ "A MemorySync API key is required. Pass api_key=... or set the "
47
+ "MEMORYSYNC_API_KEY environment variable."
48
+ )
49
+ return key.strip()
50
+
51
+
52
+ def resolve_base_url(base_url: Optional[str]) -> str:
53
+ url = base_url or os.environ.get("MEMORYSYNC_BASE_URL", "") or DEFAULT_BASE_URL
54
+ return url.rstrip("/")
55
+
56
+
57
+ def fnv1a64(value: str) -> str:
58
+ """FNV-1a 64-bit over UTF-16 code units, as a fixed-width hex string.
59
+
60
+ Over UTF-16 code units — not code points, not UTF-8 bytes — so the
61
+ output matches every other MemorySync adapter (Python and JS)
62
+ character for character. Identical seeds across surfaces mean a turn
63
+ persisted here and again elsewhere converge on one stored row.
64
+ """
65
+ prime = 0x100000001B3
66
+ mask = 0xFFFFFFFFFFFFFFFF
67
+ h = 0xCBF29CE484222325
68
+ data = value.encode("utf-16-le")
69
+ for i in range(0, len(data), 2):
70
+ unit = data[i] | (data[i + 1] << 8)
71
+ h ^= unit
72
+ h = (h * prime) & mask
73
+ return format(h, "016x")
74
+
75
+
76
+ class AsyncV1Api:
77
+ """Minimal asynchronous v1 client: add_turn, recall, query, list, forget."""
78
+
79
+ def __init__(
80
+ self,
81
+ *,
82
+ api_key: str,
83
+ base_url: str,
84
+ project_id: Optional[str] = None,
85
+ timeout: float = 30.0,
86
+ transport: Optional[httpx.AsyncBaseTransport] = None,
87
+ ) -> None:
88
+ self._api_key = api_key
89
+ self._base_url = base_url.rstrip("/")
90
+ self._project_id = project_id
91
+ self._timeout = timeout
92
+ self._transport = transport
93
+ self._http = httpx.AsyncClient(timeout=timeout, transport=transport)
94
+ self._tenant_id: Optional[str] = None
95
+ self._tenant_is_fallback = False
96
+
97
+ async def aclose(self) -> None:
98
+ await self._http.aclose()
99
+
100
+ @property
101
+ def base_url(self) -> str:
102
+ return self._base_url
103
+
104
+ # ── plumbing ─────────────────────────────────────────────────────
105
+
106
+ def _headers(self, *, end_user_id: Optional[str] = None) -> Dict[str, str]:
107
+ h = {
108
+ "X-API-Key": self._api_key,
109
+ "Accept": "application/json",
110
+ "User-Agent": _USER_AGENT,
111
+ }
112
+ if self._project_id:
113
+ h["X-Project-ID"] = self._project_id
114
+ if end_user_id:
115
+ h["X-End-User-ID"] = end_user_id
116
+ return h
117
+
118
+ async def _request(
119
+ self,
120
+ method: str,
121
+ path: str,
122
+ *,
123
+ json: Optional[Dict[str, Any]] = None,
124
+ params: Optional[Dict[str, Any]] = None,
125
+ end_user_id: Optional[str] = None,
126
+ ) -> Any:
127
+ url = f"{self._base_url}{path}"
128
+ try:
129
+ response = await self._http.request(
130
+ method,
131
+ url,
132
+ headers=self._headers(end_user_id=end_user_id),
133
+ json=json,
134
+ params=params,
135
+ )
136
+ except httpx.TimeoutException as e:
137
+ raise MemorySyncAPIError(f"Request timed out: {e}") from e
138
+ except httpx.HTTPError as e:
139
+ raise MemorySyncAPIError(f"Network error: {e}") from e
140
+
141
+ if response.status_code == 204:
142
+ return None
143
+ try:
144
+ body: Any = response.json()
145
+ except ValueError:
146
+ body = response.text or None
147
+ if response.status_code >= 400:
148
+ detail = body.get("detail") if isinstance(body, dict) else body
149
+ raise MemorySyncAPIError(
150
+ f"{method} {path} failed with HTTP {response.status_code}: {detail}",
151
+ status_code=response.status_code,
152
+ )
153
+ return body
154
+
155
+ # ── calls ────────────────────────────────────────────────────────
156
+
157
+ async def resolve_tenant_id(self) -> str:
158
+ """The tenant id, which the v1 routes need in path or body.
159
+
160
+ Derived from the project listing rather than asked for. Cached for
161
+ the lifetime of this client. Keys without the ``projects:read``
162
+ scope (evaluation keys) fall back to the fixed namespace
163
+ ``"default"`` — deterministic, so every read and write through
164
+ this client lands in one namespace. Only a definite 401/403
165
+ triggers the fallback; a transient server error re-raises rather
166
+ than silently switching namespaces.
167
+ """
168
+ if self._tenant_id:
169
+ return self._tenant_id
170
+ try:
171
+ projects = await self._request("GET", "/org/projects")
172
+ except MemorySyncAPIError as exc:
173
+ if exc.status_code in (401, 403):
174
+ self._tenant_id = FALLBACK_TENANT
175
+ self._tenant_is_fallback = True
176
+ return self._tenant_id
177
+ raise
178
+ first = projects[0] if isinstance(projects, list) and projects else None
179
+ tenant = first.get("tenant_id") if isinstance(first, dict) else None
180
+ if not tenant:
181
+ raise MemorySyncAPIError(
182
+ "Could not determine the tenant for this API key. Pass "
183
+ "tenant_id explicitly, or verify the key with `memorysync doctor`."
184
+ )
185
+ self._tenant_id = str(tenant)
186
+ return self._tenant_id
187
+
188
+ def set_tenant_id(self, tenant_id: str) -> None:
189
+ self._tenant_id = tenant_id
190
+
191
+ async def add_turn(
192
+ self,
193
+ *,
194
+ tenant_id: str,
195
+ user_id: str,
196
+ text: str,
197
+ speaker: Optional[str] = None,
198
+ metadata: Optional[Dict[str, Any]] = None,
199
+ source: str = "autogen",
200
+ sync_embed: bool = False,
201
+ ) -> Dict[str, Any]:
202
+ """Store one item verbatim (episodic ingestion).
203
+
204
+ ``speaker`` participates in the server's idempotency seed, so
205
+ retrying an identical payload is recognised
206
+ (``already_exists: true``) instead of stored twice.
207
+ """
208
+ body: Dict[str, Any] = {
209
+ "tenant_id": tenant_id,
210
+ "user_id": user_id,
211
+ "source": source,
212
+ "text": text,
213
+ "sync_embed": sync_embed,
214
+ }
215
+ if speaker is not None:
216
+ body["speaker"] = speaker
217
+ if metadata is not None:
218
+ body["metadata"] = metadata
219
+ return await self._request("POST", "/v1/memory/add_turn", json=body) or {}
220
+
221
+ async def recall(
222
+ self,
223
+ *,
224
+ tenant_id: str,
225
+ user_id: str,
226
+ prompt: str,
227
+ k: Optional[int] = None,
228
+ ) -> Dict[str, Any]:
229
+ """Hierarchical recall: grouped, prompt-ready context block."""
230
+ body: Dict[str, Any] = {
231
+ "tenant_id": tenant_id,
232
+ "user_id": user_id,
233
+ "prompt": prompt,
234
+ }
235
+ if k is not None:
236
+ body["k"] = k
237
+ return await self._request("POST", "/v1/memory/recall", json=body) or {}
238
+
239
+ async def query(
240
+ self,
241
+ *,
242
+ tenant_id: str,
243
+ user_id: str,
244
+ prompt: str,
245
+ k: Optional[int] = None,
246
+ ) -> Dict[str, Any]:
247
+ """Plain semantic search over the pair's memories (episodic included)."""
248
+ body: Dict[str, Any] = {
249
+ "tenant_id": tenant_id,
250
+ "user_id": user_id,
251
+ "prompt": prompt,
252
+ }
253
+ if k is not None:
254
+ body["k"] = k
255
+ return await self._request("POST", "/v1/memory/query", json=body) or {}
256
+
257
+ async def list_memories(
258
+ self,
259
+ *,
260
+ tenant_id: str,
261
+ user_id: str,
262
+ limit: int = 0,
263
+ ) -> List[Dict[str, Any]]:
264
+ """Every memory for the tenant/user pair, newest first.
265
+
266
+ ``limit=0`` means no limit — a scoped clear must see every row,
267
+ so that is the default here.
268
+ """
269
+ from urllib.parse import quote
270
+
271
+ raw = await self._request(
272
+ "GET",
273
+ f"/v1/memory/{quote(tenant_id, safe='')}/{quote(user_id, safe='')}/list",
274
+ params={"limit": limit},
275
+ )
276
+ memories = raw.get("memories") if isinstance(raw, dict) else None
277
+ return list(memories) if isinstance(memories, list) else []
278
+
279
+ async def forget(self, *, user_id: str, memory_ids: List[int]) -> None:
280
+ """Delete specific memories by numeric id, scoped to one end user."""
281
+ if not memory_ids:
282
+ return
283
+ # Deletion in bounded batches so one enormous clear cannot build
284
+ # an unbounded request body.
285
+ for start in range(0, len(memory_ids), 100):
286
+ batch = memory_ids[start : start + 100]
287
+ await self._request(
288
+ "DELETE",
289
+ "/memory/forget",
290
+ json={"memory_ids": batch},
291
+ end_user_id=user_id,
292
+ )
293
+
294
+ async def add_memory(
295
+ self,
296
+ *,
297
+ user_id: str,
298
+ text: str,
299
+ source: str = "autogen",
300
+ metadata: Optional[Dict[str, Any]] = None,
301
+ ) -> Dict[str, Any]:
302
+ """Store a fact through the extraction path (server-side gating)."""
303
+ body: Dict[str, Any] = {"text": text, "source": source}
304
+ if metadata is not None:
305
+ body["metadata"] = metadata
306
+ return (
307
+ await self._request(
308
+ "POST", "/memory/add", json=body, end_user_id=user_id
309
+ )
310
+ or {}
311
+ )
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"