memcode-sdk 2.3.1__tar.gz → 2.4.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.
Files changed (22) hide show
  1. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/PKG-INFO +190 -11
  2. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/README.MD +189 -10
  3. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/__init__.py +40 -1
  4. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/_http.py +58 -10
  5. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/async_client.py +104 -7
  6. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/async_v2_client.py +60 -10
  7. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/client.py +192 -21
  8. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/errors.py +16 -0
  9. memcode_sdk-2.4.0/memcode_sdk/oauth.py +1001 -0
  10. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/types.py +39 -3
  11. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/v2_client.py +122 -7
  12. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/v2_types.py +39 -0
  13. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk.egg-info/PKG-INFO +190 -11
  14. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk.egg-info/SOURCES.txt +2 -0
  15. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/pyproject.toml +1 -1
  16. memcode_sdk-2.4.0/tests/test_oauth.py +843 -0
  17. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/tests/test_v2_clients.py +401 -11
  18. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/py.typed +0 -0
  19. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk.egg-info/dependency_links.txt +0 -0
  20. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk.egg-info/requires.txt +0 -0
  21. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk.egg-info/top_level.txt +0 -0
  22. {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memcode-sdk
3
- Version: 2.3.1
3
+ Version: 2.4.0
4
4
  Summary: Python SDK for the Memcode long-term memory API
5
5
  Author: Memcode
6
6
  License-Expression: Apache-2.0
@@ -38,18 +38,34 @@ All three SDKs share the same design principles:
38
38
  The existing `MemcodeClient`/`Client` APIs remain v1-compatible and now expose an
39
39
  additive advanced personal v2 surface for authenticated personal callers:
40
40
 
41
- - Python: `ingest_v2`, `get_ingest_status_v2`, `search_v2`, `retrieve_v2`
42
- - TypeScript: `ingestV2`, `getIngestStatusV2`, `searchV2`, `retrieveV2`
43
- - Go: `IngestV2`, `GetIngestStatusV2`, `SearchV2`,
44
- `SearchV2WithOptions`, `RetrieveV2`
41
+ - Python: `ingest_v2`, `batch_ingest_v2`, `get_ingest_status_v2`,
42
+ `get_batch_ingest_status_v2`, `list_memories_v2`, `list_all_memories_v2`,
43
+ `get_memory_graph_v2`, `search_v2`, `retrieve_v2`
44
+ - TypeScript: `ingestV2`, `batchIngestV2`, `getIngestStatusV2`,
45
+ `getBatchIngestStatusV2`, `listMemoriesV2`, `listAllMemoriesV2`,
46
+ `getMemoryGraphV2`, `searchV2`, `retrieveV2`
47
+ - Go: `IngestV2`, `BatchIngestV2`, `GetIngestStatusV2`,
48
+ `GetBatchIngestStatusV2`, `SearchV2`, `SearchV2WithOptions`,
49
+ `SearchV2WithResultMode`, `RetrieveV2`, `ListMemoriesV2`,
50
+ `ListAllMemoriesV2`, `GetMemoryGraphV2`
45
51
 
46
52
  Personal v2 derives the user from the API key or JWT, so no `user_id` is needed.
47
53
  Ingestion returns a durable job receipt, retrieval includes
48
54
  attribution/connection-learning metadata, and personal v2 search uses the
49
55
  unified `/v2/memory/search` route.
50
- The deprecated `user_id` argument remains accepted and is sent only when supplied.
51
- The API may return `409` for personal v2 ingest when raw original storage is
52
- globally enabled; that backend safety gate remains unchanged.
56
+ Its optional result `mode` is distinct from `search_mode`: `default` returns
57
+ memories and chunks, `chunks` returns only stored chunks, and `memories` returns
58
+ only extracted memories. SDKs omit `mode` when it is `default` for compatibility.
59
+ Unified v2 search does not accept a domain filter. Whenever the selected mode
60
+ includes memory results, the backend searches the fixed `profile`, `summary`,
61
+ and `temporal` domains. The result `mode` remains the way to choose memories,
62
+ stored chunks, or both.
63
+ The deprecated `user_id` argument for single-item ingest remains accepted and is
64
+ sent only when supplied. Batch ingest derives identity from the credential and
65
+ does not accept `user_id` or `forget`. Explicit `POST /v2/memory/batch-ingest`
66
+ requests return `409` when raw original storage is enabled.
67
+ For personal v2 ingest, `user_query` is optional only when a nonblank
68
+ `image_url` is supplied; text-only and mixed text/image requests are unchanged.
53
69
 
54
70
  ```python
55
71
  from memcode_sdk import MemcodeClient
@@ -58,8 +74,17 @@ client = MemcodeClient(api_url="https://memory.example.com", api_key="sk-...")
58
74
  job = client.ingest_v2(
59
75
  user_query="The launch is Friday",
60
76
  )
77
+ image_job = client.ingest_v2(image_url="https://example.com/whiteboard.jpg")
61
78
  status = client.get_ingest_status_v2(job.job_id)
79
+ batch = client.batch_ingest_v2([
80
+ {"user_query": "The demo starts at 10 AM"},
81
+ {"user_query": "The launch is Friday", "effort_level": "high"},
82
+ ])
83
+ batch_status = client.get_batch_ingest_status_v2(batch.job_id)
84
+ memories = client.list_all_memories_v2()
85
+ graph = client.get_memory_graph_v2(limit=500, edge_limit=5_000)
62
86
  hits = client.search_v2(query="launch date")
87
+ chunk_hits = client.search_v2(query="launch date", mode="chunks")
63
88
  answer = client.retrieve_v2(query="When is launch?")
64
89
  ```
65
90
 
@@ -98,7 +123,7 @@ result = client.ingest(
98
123
  agent_response="Congratulations on your promotion!",
99
124
  user_id="user_42",
100
125
  )
101
- print(result.model, result.profile, result.temporal)
126
+ print(result.profile, result.temporal)
102
127
 
103
128
  # Retrieve an LLM-generated answer backed by memory
104
129
  answer = client.retrieve(query="What is my job title?", user_id="user_42")
@@ -121,10 +146,17 @@ job = client.ingest_v2(
121
146
  effort_level="high",
122
147
  )
123
148
  status = client.get_ingest_status_v2(job.job_id)
149
+ batch = client.batch_ingest_v2([
150
+ {"user_query": "I now lead the platform team"},
151
+ {"user_query": "Our launch is Friday", "effort_level": "high"},
152
+ ])
153
+ batch_status = client.get_batch_ingest_status_v2(batch.job_id)
154
+ memories = client.list_all_memories_v2()
124
155
  hybrid = client.search_v2(
125
156
  query="work history",
126
157
  top_k=10,
127
158
  original_top_k=5,
159
+ mode="memories",
128
160
  )
129
161
  advanced_answer = client.retrieve_v2(
130
162
  query="What team do I lead?",
@@ -144,9 +176,110 @@ async with AsyncMemcodeClient(api_url="http://localhost:8000") as client:
144
176
  user_id="user_42",
145
177
  )
146
178
  answer = await client.retrieve(query="hobbies", user_id="user_42")
179
+ memories = await client.list_all_memories_v2()
180
+ graph = await client.get_memory_graph_v2(limit=500, edge_limit=5_000)
147
181
  print(answer.answer)
148
182
  ```
149
183
 
184
+ ### Async OAuth and dynamic bearer tokens
185
+
186
+ `AsyncMemcodeClient` can resolve a bearer token before every request. Static
187
+ `api_key` callers are unchanged; use exactly one of `api_key` or
188
+ `access_token_provider`.
189
+
190
+ ```python
191
+ from memcode_sdk import AsyncMemcodeClient, AsyncMemcodeOAuthClient
192
+
193
+ # Use an application-owned encrypted AsyncOAuthTokenStore in production. The
194
+ # token key must identify one authenticated application user/grant.
195
+ oauth = AsyncMemcodeOAuthClient(
196
+ issuer="https://memory.memcode.in/",
197
+ resource="https://memory.memcode.in",
198
+ client_id=persisted_dynamic_client_id,
199
+ token_key=f"pipecat:{application_user_id}",
200
+ token_store=encrypted_token_store,
201
+ )
202
+
203
+ try:
204
+ # Begin the connect flow. Persist this short-lived request server-side,
205
+ # then redirect the user's browser to request.authorization_url.
206
+ request = await oauth.create_authorization_request(
207
+ redirect_uri="https://voice.example.com/oauth/callback",
208
+ )
209
+
210
+ # In the callback, validate state and exchange the code. The OAuth helper
211
+ # atomically stores each rotated token set through token_store.
212
+ await oauth.exchange_code(
213
+ code=callback_code,
214
+ returned_state=callback_state,
215
+ authorization_request=request,
216
+ )
217
+
218
+ async with AsyncMemcodeClient(
219
+ api_url="https://memory.memcode.in",
220
+ access_token_provider=oauth,
221
+ ) as client:
222
+ memories = await client.search_v2(query="What should I remember?")
223
+ finally:
224
+ # The API client does not own an injected provider.
225
+ await oauth.close()
226
+ ```
227
+
228
+ Register a public client once per deployment/redirect set and persist the
229
+ returned `client_id`:
230
+
231
+ ```python
232
+ registration = await oauth.register_client(
233
+ client_name="My Pipecat agent",
234
+ redirect_uris=["https://voice.example.com/oauth/callback"],
235
+ application_type="web",
236
+ )
237
+ ```
238
+
239
+ The helper uses Authorization Code with S256 PKCE, resource indicators,
240
+ rotating refresh tokens, and single-flight refresh per token-store key. A 401
241
+ causes at most one refresh and one request retry.
242
+
243
+ While a Memcode deployment issues only hosted-MCP resource tokens, wrap that
244
+ OAuth provider with `DelegatingMemoryTokenProvider`. The rest of the
245
+ application continues to use the normal `AsyncMemcodeClient` interface:
246
+
247
+ ```python
248
+ from memcode_sdk import AsyncMemcodeOAuthClient, DelegatingMemoryTokenProvider
249
+
250
+ mcp_oauth = AsyncMemcodeOAuthClient(
251
+ issuer="https://memory.memcode.in/",
252
+ resource="https://mcp.memcode.in/mcp",
253
+ scopes=("memory:read", "memory:write", "memory:connections:write"),
254
+ client_id=persisted_dynamic_client_id,
255
+ token_key=f"pipecat:{application_user_id}",
256
+ token_store=encrypted_token_store,
257
+ )
258
+ memory_tokens = DelegatingMemoryTokenProvider(mcp_oauth)
259
+ try:
260
+ async with AsyncMemcodeClient(
261
+ api_url="https://memory.memcode.in",
262
+ access_token_provider=memory_tokens,
263
+ ) as client:
264
+ memories = await client.search_v2(query="What should I remember?")
265
+ finally:
266
+ # Both providers are application-owned; the wrapper does not close its
267
+ # source OAuth provider.
268
+ await memory_tokens.close()
269
+ await mcp_oauth.close()
270
+ ```
271
+
272
+ The in-memory token store exported by the SDK is for tests and examples in one
273
+ process/event loop only. Production `AsyncOAuthTokenStore` implementations must
274
+ encrypt tokens at rest, make token-set replacement atomic, and implement
275
+ `refresh_lease(key)` with a distributed lock or transaction covering every
276
+ worker that shares the store. The lease must span token reload, refresh, and
277
+ save (and renew or fence a time-limited lock) so two workers cannot reuse a
278
+ rotating refresh token. The same lease serializes reconnect persistence and
279
+ revocation/deletion against refresh. The SDK shields these remote side effects
280
+ and their local persistence from request cancellation, then waits for them
281
+ during provider cleanup.
282
+
150
283
  ### Error handling
151
284
 
152
285
  ```python
@@ -171,6 +304,7 @@ except NotReadyError:
171
304
  | `api_url` | `MEMCODE_API_URL` | `http://localhost:8000` |
172
305
  | `api_key` | `MEMCODE_API_KEY` | _(empty, no auth)_ |
173
306
  | `timeout` | — | `120` seconds |
307
+ | `access_token_provider` | — | _(optional, async clients only)_ |
174
308
 
175
309
  ---
176
310
 
@@ -222,11 +356,22 @@ const job = await client.ingestV2({
222
356
  user_query: "I now lead the platform team",
223
357
  effort_level: "high",
224
358
  });
359
+ const imageJob = await client.ingestV2({
360
+ image_url: "https://example.com/whiteboard.jpg",
361
+ });
225
362
  const status = await client.getIngestStatusV2(job.job_id);
363
+ const batch = await client.batchIngestV2({ items: [
364
+ { user_query: "The demo starts at 10 AM" },
365
+ { user_query: "The launch is Friday", effort_level: "high" },
366
+ ] });
367
+ const batchStatus = await client.getBatchIngestStatusV2(batch.job_id);
368
+ const memories = await client.listAllMemoriesV2();
369
+ const graph = await client.getMemoryGraphV2({ limit: 500, edge_limit: 5_000 });
226
370
  const hybrid = await client.searchV2({
227
371
  query: "pets",
228
372
  top_k: 10,
229
373
  original_top_k: 5,
374
+ mode: "chunks",
230
375
  });
231
376
  const advancedAnswer = await client.retrieveV2({
232
377
  query: "Do I have pets?",
@@ -324,11 +469,41 @@ func main() {
324
469
  }
325
470
  status, err := client.GetIngestStatusV2(job.JobID)
326
471
 
327
- hybrid, err := client.SearchV2WithOptions(memcode.HybridSearchParams{
472
+ imageJob, err := client.IngestV2(memcode.PersonalV2IngestParams{
473
+ ImageURL: "https://example.com/whiteboard.jpg",
474
+ }, "whiteboard-1")
475
+ if err != nil {
476
+ panic(err)
477
+ }
478
+
479
+ batch, err := client.BatchIngestV2(memcode.PersonalV2BatchIngestParams{
480
+ Items: []memcode.PersonalV2BatchIngestItem{
481
+ {UserQuery: "The demo starts at 10 AM"},
482
+ {UserQuery: "The launch is Friday", EffortLevel: "high"},
483
+ },
484
+ })
485
+ if err != nil {
486
+ panic(err)
487
+ }
488
+ batchStatus, err := client.GetBatchIngestStatusV2(batch.JobID)
489
+
490
+ hybrid, err := client.SearchV2WithResultMode(memcode.HybridSearchParams{
328
491
  Query: "location",
329
492
  OriginalTopK: 5,
330
493
  }, memcode.PersonalV2SearchOptions{
331
494
  TopK: 10,
495
+ }, memcode.SearchResultModeMemories)
496
+ if err != nil {
497
+ panic(err)
498
+ }
499
+
500
+ memories, err := client.ListAllMemoriesV2(500)
501
+ if err != nil {
502
+ panic(err)
503
+ }
504
+ edgeLimit := 5_000
505
+ graph, err := client.GetMemoryGraphV2(memcode.PersonalMemoryGraphParams{
506
+ Limit: 500, EdgeLimit: &edgeLimit,
332
507
  })
333
508
  if err != nil {
334
509
  panic(err)
@@ -336,7 +511,7 @@ func main() {
336
511
  advancedAnswer, err := client.RetrieveV2(memcode.RetrieveParams{
337
512
  Query: "Where am I moving?",
338
513
  })
339
- fmt.Println("V2:", status.Status, hybrid.Total, advancedAnswer.Answer)
514
+ fmt.Println("V2:", status.Status, imageJob.JobID, batchStatus.Status, len(graph.Nodes), hybrid.Total, advancedAnswer.Answer)
340
515
  }
341
516
  ```
342
517
 
@@ -371,6 +546,10 @@ All three SDKs expose backwards-compatible v1 methods plus advanced personal v2
371
546
  | **search** | `POST /v1/memory/search` | Raw semantic search across memory domains (`profile`, `temporal`, `summary`). Returns matching records without an LLM answer. |
372
547
  | **personal v2 ingest** | `POST /v2/memory/ingest` | Start a durable normal-user ingest job. |
373
548
  | **personal v2 status** | `GET /v2/memory/ingest/{job_id}/status` | Poll durable ingest progress. |
549
+ | **personal v2 batch ingest** | `POST /v2/memory/batch-ingest` | Start one durable job for 1 to 100 turns. |
550
+ | **personal v2 batch status** | `GET /v2/memory/jobs/{job_id}/status` | Poll the status URL returned by batch ingest. |
551
+ | **personal v2 list** | `GET /v2/memory` | List memories for the user derived from the credential. Paginate with `limit` and `offset`. |
552
+ | **personal v2 graph** | `GET /v2/memory-graph` | Read independently paginated memory nodes and weighted edges for the user derived from the credential. |
374
553
  | **personal v2 search** | `POST /v2/memory/search` | Search extracted memories and original chunks for the user derived from the credential. |
375
554
  | **personal v2 retrieve** | `POST /v2/memory/retrieve` | Advanced attributed retrieval with connection-learning metadata. |
376
555
  | **ping** | `GET /health` | Health/readiness check. Never raises on a valid HTTP response. |
@@ -15,18 +15,34 @@ All three SDKs share the same design principles:
15
15
  The existing `MemcodeClient`/`Client` APIs remain v1-compatible and now expose an
16
16
  additive advanced personal v2 surface for authenticated personal callers:
17
17
 
18
- - Python: `ingest_v2`, `get_ingest_status_v2`, `search_v2`, `retrieve_v2`
19
- - TypeScript: `ingestV2`, `getIngestStatusV2`, `searchV2`, `retrieveV2`
20
- - Go: `IngestV2`, `GetIngestStatusV2`, `SearchV2`,
21
- `SearchV2WithOptions`, `RetrieveV2`
18
+ - Python: `ingest_v2`, `batch_ingest_v2`, `get_ingest_status_v2`,
19
+ `get_batch_ingest_status_v2`, `list_memories_v2`, `list_all_memories_v2`,
20
+ `get_memory_graph_v2`, `search_v2`, `retrieve_v2`
21
+ - TypeScript: `ingestV2`, `batchIngestV2`, `getIngestStatusV2`,
22
+ `getBatchIngestStatusV2`, `listMemoriesV2`, `listAllMemoriesV2`,
23
+ `getMemoryGraphV2`, `searchV2`, `retrieveV2`
24
+ - Go: `IngestV2`, `BatchIngestV2`, `GetIngestStatusV2`,
25
+ `GetBatchIngestStatusV2`, `SearchV2`, `SearchV2WithOptions`,
26
+ `SearchV2WithResultMode`, `RetrieveV2`, `ListMemoriesV2`,
27
+ `ListAllMemoriesV2`, `GetMemoryGraphV2`
22
28
 
23
29
  Personal v2 derives the user from the API key or JWT, so no `user_id` is needed.
24
30
  Ingestion returns a durable job receipt, retrieval includes
25
31
  attribution/connection-learning metadata, and personal v2 search uses the
26
32
  unified `/v2/memory/search` route.
27
- The deprecated `user_id` argument remains accepted and is sent only when supplied.
28
- The API may return `409` for personal v2 ingest when raw original storage is
29
- globally enabled; that backend safety gate remains unchanged.
33
+ Its optional result `mode` is distinct from `search_mode`: `default` returns
34
+ memories and chunks, `chunks` returns only stored chunks, and `memories` returns
35
+ only extracted memories. SDKs omit `mode` when it is `default` for compatibility.
36
+ Unified v2 search does not accept a domain filter. Whenever the selected mode
37
+ includes memory results, the backend searches the fixed `profile`, `summary`,
38
+ and `temporal` domains. The result `mode` remains the way to choose memories,
39
+ stored chunks, or both.
40
+ The deprecated `user_id` argument for single-item ingest remains accepted and is
41
+ sent only when supplied. Batch ingest derives identity from the credential and
42
+ does not accept `user_id` or `forget`. Explicit `POST /v2/memory/batch-ingest`
43
+ requests return `409` when raw original storage is enabled.
44
+ For personal v2 ingest, `user_query` is optional only when a nonblank
45
+ `image_url` is supplied; text-only and mixed text/image requests are unchanged.
30
46
 
31
47
  ```python
32
48
  from memcode_sdk import MemcodeClient
@@ -35,8 +51,17 @@ client = MemcodeClient(api_url="https://memory.example.com", api_key="sk-...")
35
51
  job = client.ingest_v2(
36
52
  user_query="The launch is Friday",
37
53
  )
54
+ image_job = client.ingest_v2(image_url="https://example.com/whiteboard.jpg")
38
55
  status = client.get_ingest_status_v2(job.job_id)
56
+ batch = client.batch_ingest_v2([
57
+ {"user_query": "The demo starts at 10 AM"},
58
+ {"user_query": "The launch is Friday", "effort_level": "high"},
59
+ ])
60
+ batch_status = client.get_batch_ingest_status_v2(batch.job_id)
61
+ memories = client.list_all_memories_v2()
62
+ graph = client.get_memory_graph_v2(limit=500, edge_limit=5_000)
39
63
  hits = client.search_v2(query="launch date")
64
+ chunk_hits = client.search_v2(query="launch date", mode="chunks")
40
65
  answer = client.retrieve_v2(query="When is launch?")
41
66
  ```
42
67
 
@@ -75,7 +100,7 @@ result = client.ingest(
75
100
  agent_response="Congratulations on your promotion!",
76
101
  user_id="user_42",
77
102
  )
78
- print(result.model, result.profile, result.temporal)
103
+ print(result.profile, result.temporal)
79
104
 
80
105
  # Retrieve an LLM-generated answer backed by memory
81
106
  answer = client.retrieve(query="What is my job title?", user_id="user_42")
@@ -98,10 +123,17 @@ job = client.ingest_v2(
98
123
  effort_level="high",
99
124
  )
100
125
  status = client.get_ingest_status_v2(job.job_id)
126
+ batch = client.batch_ingest_v2([
127
+ {"user_query": "I now lead the platform team"},
128
+ {"user_query": "Our launch is Friday", "effort_level": "high"},
129
+ ])
130
+ batch_status = client.get_batch_ingest_status_v2(batch.job_id)
131
+ memories = client.list_all_memories_v2()
101
132
  hybrid = client.search_v2(
102
133
  query="work history",
103
134
  top_k=10,
104
135
  original_top_k=5,
136
+ mode="memories",
105
137
  )
106
138
  advanced_answer = client.retrieve_v2(
107
139
  query="What team do I lead?",
@@ -121,9 +153,110 @@ async with AsyncMemcodeClient(api_url="http://localhost:8000") as client:
121
153
  user_id="user_42",
122
154
  )
123
155
  answer = await client.retrieve(query="hobbies", user_id="user_42")
156
+ memories = await client.list_all_memories_v2()
157
+ graph = await client.get_memory_graph_v2(limit=500, edge_limit=5_000)
124
158
  print(answer.answer)
125
159
  ```
126
160
 
161
+ ### Async OAuth and dynamic bearer tokens
162
+
163
+ `AsyncMemcodeClient` can resolve a bearer token before every request. Static
164
+ `api_key` callers are unchanged; use exactly one of `api_key` or
165
+ `access_token_provider`.
166
+
167
+ ```python
168
+ from memcode_sdk import AsyncMemcodeClient, AsyncMemcodeOAuthClient
169
+
170
+ # Use an application-owned encrypted AsyncOAuthTokenStore in production. The
171
+ # token key must identify one authenticated application user/grant.
172
+ oauth = AsyncMemcodeOAuthClient(
173
+ issuer="https://memory.memcode.in/",
174
+ resource="https://memory.memcode.in",
175
+ client_id=persisted_dynamic_client_id,
176
+ token_key=f"pipecat:{application_user_id}",
177
+ token_store=encrypted_token_store,
178
+ )
179
+
180
+ try:
181
+ # Begin the connect flow. Persist this short-lived request server-side,
182
+ # then redirect the user's browser to request.authorization_url.
183
+ request = await oauth.create_authorization_request(
184
+ redirect_uri="https://voice.example.com/oauth/callback",
185
+ )
186
+
187
+ # In the callback, validate state and exchange the code. The OAuth helper
188
+ # atomically stores each rotated token set through token_store.
189
+ await oauth.exchange_code(
190
+ code=callback_code,
191
+ returned_state=callback_state,
192
+ authorization_request=request,
193
+ )
194
+
195
+ async with AsyncMemcodeClient(
196
+ api_url="https://memory.memcode.in",
197
+ access_token_provider=oauth,
198
+ ) as client:
199
+ memories = await client.search_v2(query="What should I remember?")
200
+ finally:
201
+ # The API client does not own an injected provider.
202
+ await oauth.close()
203
+ ```
204
+
205
+ Register a public client once per deployment/redirect set and persist the
206
+ returned `client_id`:
207
+
208
+ ```python
209
+ registration = await oauth.register_client(
210
+ client_name="My Pipecat agent",
211
+ redirect_uris=["https://voice.example.com/oauth/callback"],
212
+ application_type="web",
213
+ )
214
+ ```
215
+
216
+ The helper uses Authorization Code with S256 PKCE, resource indicators,
217
+ rotating refresh tokens, and single-flight refresh per token-store key. A 401
218
+ causes at most one refresh and one request retry.
219
+
220
+ While a Memcode deployment issues only hosted-MCP resource tokens, wrap that
221
+ OAuth provider with `DelegatingMemoryTokenProvider`. The rest of the
222
+ application continues to use the normal `AsyncMemcodeClient` interface:
223
+
224
+ ```python
225
+ from memcode_sdk import AsyncMemcodeOAuthClient, DelegatingMemoryTokenProvider
226
+
227
+ mcp_oauth = AsyncMemcodeOAuthClient(
228
+ issuer="https://memory.memcode.in/",
229
+ resource="https://mcp.memcode.in/mcp",
230
+ scopes=("memory:read", "memory:write", "memory:connections:write"),
231
+ client_id=persisted_dynamic_client_id,
232
+ token_key=f"pipecat:{application_user_id}",
233
+ token_store=encrypted_token_store,
234
+ )
235
+ memory_tokens = DelegatingMemoryTokenProvider(mcp_oauth)
236
+ try:
237
+ async with AsyncMemcodeClient(
238
+ api_url="https://memory.memcode.in",
239
+ access_token_provider=memory_tokens,
240
+ ) as client:
241
+ memories = await client.search_v2(query="What should I remember?")
242
+ finally:
243
+ # Both providers are application-owned; the wrapper does not close its
244
+ # source OAuth provider.
245
+ await memory_tokens.close()
246
+ await mcp_oauth.close()
247
+ ```
248
+
249
+ The in-memory token store exported by the SDK is for tests and examples in one
250
+ process/event loop only. Production `AsyncOAuthTokenStore` implementations must
251
+ encrypt tokens at rest, make token-set replacement atomic, and implement
252
+ `refresh_lease(key)` with a distributed lock or transaction covering every
253
+ worker that shares the store. The lease must span token reload, refresh, and
254
+ save (and renew or fence a time-limited lock) so two workers cannot reuse a
255
+ rotating refresh token. The same lease serializes reconnect persistence and
256
+ revocation/deletion against refresh. The SDK shields these remote side effects
257
+ and their local persistence from request cancellation, then waits for them
258
+ during provider cleanup.
259
+
127
260
  ### Error handling
128
261
 
129
262
  ```python
@@ -148,6 +281,7 @@ except NotReadyError:
148
281
  | `api_url` | `MEMCODE_API_URL` | `http://localhost:8000` |
149
282
  | `api_key` | `MEMCODE_API_KEY` | _(empty, no auth)_ |
150
283
  | `timeout` | — | `120` seconds |
284
+ | `access_token_provider` | — | _(optional, async clients only)_ |
151
285
 
152
286
  ---
153
287
 
@@ -199,11 +333,22 @@ const job = await client.ingestV2({
199
333
  user_query: "I now lead the platform team",
200
334
  effort_level: "high",
201
335
  });
336
+ const imageJob = await client.ingestV2({
337
+ image_url: "https://example.com/whiteboard.jpg",
338
+ });
202
339
  const status = await client.getIngestStatusV2(job.job_id);
340
+ const batch = await client.batchIngestV2({ items: [
341
+ { user_query: "The demo starts at 10 AM" },
342
+ { user_query: "The launch is Friday", effort_level: "high" },
343
+ ] });
344
+ const batchStatus = await client.getBatchIngestStatusV2(batch.job_id);
345
+ const memories = await client.listAllMemoriesV2();
346
+ const graph = await client.getMemoryGraphV2({ limit: 500, edge_limit: 5_000 });
203
347
  const hybrid = await client.searchV2({
204
348
  query: "pets",
205
349
  top_k: 10,
206
350
  original_top_k: 5,
351
+ mode: "chunks",
207
352
  });
208
353
  const advancedAnswer = await client.retrieveV2({
209
354
  query: "Do I have pets?",
@@ -301,11 +446,41 @@ func main() {
301
446
  }
302
447
  status, err := client.GetIngestStatusV2(job.JobID)
303
448
 
304
- hybrid, err := client.SearchV2WithOptions(memcode.HybridSearchParams{
449
+ imageJob, err := client.IngestV2(memcode.PersonalV2IngestParams{
450
+ ImageURL: "https://example.com/whiteboard.jpg",
451
+ }, "whiteboard-1")
452
+ if err != nil {
453
+ panic(err)
454
+ }
455
+
456
+ batch, err := client.BatchIngestV2(memcode.PersonalV2BatchIngestParams{
457
+ Items: []memcode.PersonalV2BatchIngestItem{
458
+ {UserQuery: "The demo starts at 10 AM"},
459
+ {UserQuery: "The launch is Friday", EffortLevel: "high"},
460
+ },
461
+ })
462
+ if err != nil {
463
+ panic(err)
464
+ }
465
+ batchStatus, err := client.GetBatchIngestStatusV2(batch.JobID)
466
+
467
+ hybrid, err := client.SearchV2WithResultMode(memcode.HybridSearchParams{
305
468
  Query: "location",
306
469
  OriginalTopK: 5,
307
470
  }, memcode.PersonalV2SearchOptions{
308
471
  TopK: 10,
472
+ }, memcode.SearchResultModeMemories)
473
+ if err != nil {
474
+ panic(err)
475
+ }
476
+
477
+ memories, err := client.ListAllMemoriesV2(500)
478
+ if err != nil {
479
+ panic(err)
480
+ }
481
+ edgeLimit := 5_000
482
+ graph, err := client.GetMemoryGraphV2(memcode.PersonalMemoryGraphParams{
483
+ Limit: 500, EdgeLimit: &edgeLimit,
309
484
  })
310
485
  if err != nil {
311
486
  panic(err)
@@ -313,7 +488,7 @@ func main() {
313
488
  advancedAnswer, err := client.RetrieveV2(memcode.RetrieveParams{
314
489
  Query: "Where am I moving?",
315
490
  })
316
- fmt.Println("V2:", status.Status, hybrid.Total, advancedAnswer.Answer)
491
+ fmt.Println("V2:", status.Status, imageJob.JobID, batchStatus.Status, len(graph.Nodes), hybrid.Total, advancedAnswer.Answer)
317
492
  }
318
493
  ```
319
494
 
@@ -348,6 +523,10 @@ All three SDKs expose backwards-compatible v1 methods plus advanced personal v2
348
523
  | **search** | `POST /v1/memory/search` | Raw semantic search across memory domains (`profile`, `temporal`, `summary`). Returns matching records without an LLM answer. |
349
524
  | **personal v2 ingest** | `POST /v2/memory/ingest` | Start a durable normal-user ingest job. |
350
525
  | **personal v2 status** | `GET /v2/memory/ingest/{job_id}/status` | Poll durable ingest progress. |
526
+ | **personal v2 batch ingest** | `POST /v2/memory/batch-ingest` | Start one durable job for 1 to 100 turns. |
527
+ | **personal v2 batch status** | `GET /v2/memory/jobs/{job_id}/status` | Poll the status URL returned by batch ingest. |
528
+ | **personal v2 list** | `GET /v2/memory` | List memories for the user derived from the credential. Paginate with `limit` and `offset`. |
529
+ | **personal v2 graph** | `GET /v2/memory-graph` | Read independently paginated memory nodes and weighted edges for the user derived from the credential. |
351
530
  | **personal v2 search** | `POST /v2/memory/search` | Search extracted memories and original chunks for the user derived from the credential. |
352
531
  | **personal v2 retrieve** | `POST /v2/memory/retrieve` | Advanced attributed retrieval with connection-learning metadata. |
353
532
  | **ping** | `GET /health` | Health/readiness check. Never raises on a valid HTTP response. |
@@ -30,8 +30,12 @@ from .types import (
30
30
  OperationDetail,
31
31
  PersonalV2IngestResult,
32
32
  PersonalV2IngestStatus,
33
+ PersonalV2BatchIngestItem,
34
+ PersonalMemory,
35
+ PersonalMemoryPage,
33
36
  PersonalV2RetrieveResult,
34
37
  RetrieveResult,
38
+ SearchResultMode,
35
39
  SearchResult,
36
40
  SourceRecord,
37
41
  WeaverSummary,
@@ -41,14 +45,30 @@ from .errors import (
41
45
  AuthenticationError,
42
46
  ConnectionError,
43
47
  NotReadyError,
48
+ OAuthError,
44
49
  RateLimitError,
45
50
  ServerError,
46
51
  ValidationError,
47
52
  )
53
+ from .oauth import (
54
+ AsyncAccessTokenProvider,
55
+ AsyncMemcodeOAuthClient,
56
+ AsyncOAuthTokenStore,
57
+ DelegatingMemoryTokenProvider,
58
+ InMemoryOAuthTokenStore,
59
+ OAuthAuthorizationServerMetadata,
60
+ OAuthClientRegistration,
61
+ OAuthProtectedResourceMetadata,
62
+ OAuthTokenSet,
63
+ PKCEAuthorizationRequest,
64
+ )
48
65
  from .v2_types import (
49
66
  V2IngestResult,
50
67
  V2IngestStatus,
51
68
  V2MemorySource,
69
+ V2MemoryGraphEdge,
70
+ V2MemoryGraphNode,
71
+ V2MemoryGraphResult,
52
72
  V2OperationCounts,
53
73
  V2OriginalStorageStatus,
54
74
  V2RetrieveResult,
@@ -59,7 +79,7 @@ from .v2_types import (
59
79
  )
60
80
 
61
81
  __author__ = "Memcode Team"
62
- __version__ = "2.3.1"
82
+ __version__ = "2.4.0"
63
83
  __license__ = "Apache-2.0"
64
84
 
65
85
  __all__ = [
@@ -70,6 +90,17 @@ __all__ = [
70
90
  "MemoryV2Client",
71
91
  "AsyncMemcodeV2Client",
72
92
  "AsyncMemoryV2Client",
93
+ # OAuth and dynamic bearer-token support
94
+ "AsyncAccessTokenProvider",
95
+ "AsyncMemcodeOAuthClient",
96
+ "AsyncOAuthTokenStore",
97
+ "DelegatingMemoryTokenProvider",
98
+ "InMemoryOAuthTokenStore",
99
+ "OAuthAuthorizationServerMetadata",
100
+ "OAuthClientRegistration",
101
+ "OAuthProtectedResourceMetadata",
102
+ "OAuthTokenSet",
103
+ "PKCEAuthorizationRequest",
73
104
  # Result types
74
105
  "DomainResult",
75
106
  "HealthStatus",
@@ -78,14 +109,21 @@ __all__ = [
78
109
  "OperationDetail",
79
110
  "PersonalV2IngestResult",
80
111
  "PersonalV2IngestStatus",
112
+ "PersonalV2BatchIngestItem",
113
+ "PersonalMemory",
114
+ "PersonalMemoryPage",
81
115
  "PersonalV2RetrieveResult",
82
116
  "RetrieveResult",
117
+ "SearchResultMode",
83
118
  "SearchResult",
84
119
  "SourceRecord",
85
120
  "WeaverSummary",
86
121
  "V2IngestResult",
87
122
  "V2IngestStatus",
88
123
  "V2MemorySource",
124
+ "V2MemoryGraphEdge",
125
+ "V2MemoryGraphNode",
126
+ "V2MemoryGraphResult",
89
127
  "V2OperationCounts",
90
128
  "V2OriginalStorageStatus",
91
129
  "V2RetrieveResult",
@@ -98,6 +136,7 @@ __all__ = [
98
136
  "AuthenticationError",
99
137
  "ConnectionError",
100
138
  "NotReadyError",
139
+ "OAuthError",
101
140
  "RateLimitError",
102
141
  "ServerError",
103
142
  "ValidationError",