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.
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/PKG-INFO +190 -11
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/README.MD +189 -10
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/__init__.py +40 -1
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/_http.py +58 -10
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/async_client.py +104 -7
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/async_v2_client.py +60 -10
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/client.py +192 -21
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/errors.py +16 -0
- memcode_sdk-2.4.0/memcode_sdk/oauth.py +1001 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/types.py +39 -3
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/v2_client.py +122 -7
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/v2_types.py +39 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk.egg-info/PKG-INFO +190 -11
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk.egg-info/SOURCES.txt +2 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/pyproject.toml +1 -1
- memcode_sdk-2.4.0/tests/test_oauth.py +843 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/tests/test_v2_clients.py +401 -11
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk/py.typed +0 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk.egg-info/dependency_links.txt +0 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk.egg-info/requires.txt +0 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.4.0}/memcode_sdk.egg-info/top_level.txt +0 -0
- {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
|
+
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`, `
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
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
|
-
|
|
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`, `
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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",
|