memcode-sdk 2.3.1__tar.gz → 2.5.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 (26) hide show
  1. memcode_sdk-2.5.0/PKG-INFO +675 -0
  2. memcode_sdk-2.5.0/README.MD +652 -0
  3. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/__init__.py +40 -1
  4. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/_http.py +58 -10
  5. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/async_client.py +104 -7
  6. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/async_v2_client.py +62 -11
  7. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/client.py +192 -21
  8. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/errors.py +16 -0
  9. memcode_sdk-2.5.0/memcode_sdk/oauth.py +1061 -0
  10. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/types.py +39 -3
  11. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/v2_client.py +139 -8
  12. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/v2_types.py +39 -0
  13. memcode_sdk-2.5.0/memcode_sdk.egg-info/PKG-INFO +675 -0
  14. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk.egg-info/SOURCES.txt +2 -0
  15. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/pyproject.toml +1 -1
  16. memcode_sdk-2.5.0/tests/test_oauth.py +924 -0
  17. memcode_sdk-2.5.0/tests/test_v2_clients.py +862 -0
  18. memcode_sdk-2.3.1/PKG-INFO +0 -387
  19. memcode_sdk-2.3.1/README.MD +0 -364
  20. memcode_sdk-2.3.1/memcode_sdk.egg-info/PKG-INFO +0 -387
  21. memcode_sdk-2.3.1/tests/test_v2_clients.py +0 -425
  22. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/py.typed +0 -0
  23. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk.egg-info/dependency_links.txt +0 -0
  24. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk.egg-info/requires.txt +0 -0
  25. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk.egg-info/top_level.txt +0 -0
  26. {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/setup.cfg +0 -0
@@ -0,0 +1,675 @@
1
+ Metadata-Version: 2.4
2
+ Name: memcode-sdk
3
+ Version: 2.5.0
4
+ Summary: Python SDK for the Memcode long-term memory API
5
+ Author: Memcode
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://memcode.in
8
+ Project-URL: Repository, https://gitlab.com/xortex1/memcode-sdk
9
+ Project-URL: Issues, https://gitlab.com/xortex1/memcode-sdk/-/issues
10
+ Keywords: memory,long-term-memory,llm,rag,ai-agent
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: httpx<1,>=0.24
23
+
24
+ <h1 align="center">Memcode Client SDKs</h1>
25
+
26
+ <p align="center">
27
+ Official client libraries for the <strong>Memcode long-term memory API</strong>.<br>
28
+ Available in Python, TypeScript, and Go.
29
+ </p>
30
+
31
+ All three SDKs share the same design principles:
32
+
33
+ - Existing v1 clients keep three 1:1 methods: **ingest**, **retrieve**, **search**
34
+ - Bearer-token authentication via a static key or refreshable token provider
35
+ - Typed error hierarchy so callers can handle auth, rate-limit, and server errors distinctly
36
+ - Zero config defaults &mdash; point at `localhost:8000` with no key and it just works in dev
37
+
38
+ The existing `MemcodeClient`/`Client` APIs remain v1-compatible and now expose an
39
+ additive advanced personal v2 surface for authenticated personal callers:
40
+
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`
51
+
52
+ Personal v2 derives the user from the API key or JWT, so no `user_id` is needed.
53
+ Ingestion returns a durable job receipt, retrieval includes
54
+ attribution/connection-learning metadata, and personal v2 search uses the
55
+ unified `/v2/memory/search` route.
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.
69
+
70
+ ```python
71
+ from memcode_sdk import MemcodeClient
72
+
73
+ client = MemcodeClient(api_url="https://memory.example.com", api_key="sk-...")
74
+ job = client.ingest_v2(
75
+ user_query="The launch is Friday",
76
+ )
77
+ image_job = client.ingest_v2(image_url="https://example.com/whiteboard.jpg")
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)
86
+ hits = client.search_v2(query="launch date")
87
+ chunk_hits = client.search_v2(query="launch date", mode="chunks")
88
+ answer = client.retrieve_v2(query="When is launch?")
89
+ ```
90
+
91
+ ## Prerequisites
92
+
93
+ A running Memcode API server:
94
+
95
+ ```bash
96
+ uvicorn src.api.app:create_app --factory --host 0.0.0.0 --port 8000
97
+ ```
98
+
99
+ ## Python
100
+
101
+ **Location:** `memcode_sdk/`
102
+
103
+ ### Install
104
+
105
+ ```bash
106
+ pip install memcode-sdk
107
+ ```
108
+
109
+ ### Sync usage
110
+
111
+ ```python
112
+ from memcode_sdk import MemcodeClient
113
+
114
+ client = MemcodeClient(api_url="http://localhost:8000", api_key="sk-...")
115
+
116
+ # Check health
117
+ health = client.ping()
118
+ print(health.status, health.pipelines_ready)
119
+
120
+ # Ingest a conversation turn
121
+ result = client.ingest(
122
+ user_query="I just got promoted to senior engineer at Google!",
123
+ agent_response="Congratulations on your promotion!",
124
+ user_id="user_42",
125
+ )
126
+ print(result.profile, result.temporal)
127
+
128
+ # Retrieve an LLM-generated answer backed by memory
129
+ answer = client.retrieve(query="What is my job title?", user_id="user_42")
130
+ print(answer.answer)
131
+ print(answer.sources) # list of SourceRecord
132
+ print(answer.confidence)
133
+
134
+ # Raw semantic search (no LLM answer)
135
+ hits = client.search(
136
+ query="work history",
137
+ user_id="user_42",
138
+ domains=["profile", "temporal"],
139
+ top_k=10,
140
+ )
141
+ for r in hits.results:
142
+ print(f"[{r.domain}] {r.content} (score={r.score:.2f})")
143
+
144
+ job = client.ingest_v2(
145
+ user_query="I now lead the platform team",
146
+ effort_level="high",
147
+ )
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()
155
+ hybrid = client.search_v2(
156
+ query="work history",
157
+ top_k=10,
158
+ original_top_k=5,
159
+ mode="memories",
160
+ )
161
+ advanced_answer = client.retrieve_v2(
162
+ query="What team do I lead?",
163
+ )
164
+
165
+ client.close()
166
+ ```
167
+
168
+ ### Async usage
169
+
170
+ ```python
171
+ from memcode_sdk import AsyncMemcodeClient
172
+
173
+ async with AsyncMemcodeClient(api_url="http://localhost:8000") as client:
174
+ result = await client.ingest(
175
+ user_query="I love hiking in the Rockies.",
176
+ user_id="user_42",
177
+ )
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)
181
+ print(answer.answer)
182
+ ```
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
+ software_id="ai.pipecat.memcode",
237
+ software_version="1.0.0",
238
+ )
239
+ ```
240
+
241
+ `software_id` is a stable public package identifier, not a credential. If it is
242
+ registered by Memcode, loopback or otherwise unverified callbacks can be counted
243
+ under that integration with `attribution_status="unverified"` and
244
+ `attribution_basis="dcr_software_id"`. An exact registered HTTPS callback is
245
+ still required for verified SDK attribution. Conflicting callback and software
246
+ owners are rejected by the authorization server.
247
+
248
+ Memcode may return read-only `integration_id`, `integration_channel`,
249
+ `attribution_status`, and `attribution_basis` values on the registration.
250
+ They are assigned by the server from its trusted integration registry. The SDK
251
+ does not send these values during registration or on Memory API requests, so
252
+ applications need no attribution parameter or additional attribution secret.
253
+ Older servers may omit all four values. Tenant-bound v2 requests reject these
254
+ server-owned field names when supplied in request metadata.
255
+
256
+ The helper uses Authorization Code with S256 PKCE, resource indicators,
257
+ rotating refresh tokens, and single-flight refresh per token-store key. A 401
258
+ causes at most one refresh and one request retry.
259
+
260
+ While a Memcode deployment issues only hosted-MCP resource tokens, wrap that
261
+ OAuth provider with `DelegatingMemoryTokenProvider`. The rest of the
262
+ application continues to use the normal `AsyncMemcodeClient` interface:
263
+
264
+ ```python
265
+ from memcode_sdk import AsyncMemcodeOAuthClient, DelegatingMemoryTokenProvider
266
+
267
+ mcp_oauth = AsyncMemcodeOAuthClient(
268
+ issuer="https://memory.memcode.in/",
269
+ resource="https://mcp.memcode.in/mcp",
270
+ scopes=("memory:read", "memory:write", "memory:connections:write"),
271
+ client_id=persisted_dynamic_client_id,
272
+ token_key=f"pipecat:{application_user_id}",
273
+ token_store=encrypted_token_store,
274
+ )
275
+ memory_tokens = DelegatingMemoryTokenProvider(mcp_oauth)
276
+ try:
277
+ async with AsyncMemcodeClient(
278
+ api_url="https://memory.memcode.in",
279
+ access_token_provider=memory_tokens,
280
+ ) as client:
281
+ memories = await client.search_v2(query="What should I remember?")
282
+ finally:
283
+ # Both providers are application-owned; the wrapper does not close its
284
+ # source OAuth provider.
285
+ await memory_tokens.close()
286
+ await mcp_oauth.close()
287
+ ```
288
+
289
+ The in-memory token store exported by the SDK is for tests and examples in one
290
+ process/event loop only. Production `AsyncOAuthTokenStore` implementations must
291
+ encrypt tokens at rest, make token-set replacement atomic, and implement
292
+ `refresh_lease(key)` with a distributed lock or transaction covering every
293
+ worker that shares the store. The lease must span token reload, refresh, and
294
+ save (and renew or fence a time-limited lock) so two workers cannot reuse a
295
+ rotating refresh token. The same lease serializes reconnect persistence and
296
+ revocation/deletion against refresh. The SDK shields these remote side effects
297
+ and their local persistence from request cancellation, then waits for them
298
+ during provider cleanup.
299
+
300
+ ### Error handling
301
+
302
+ ```python
303
+ from memcode_sdk import MemcodeClient, AuthenticationError, RateLimitError, NotReadyError
304
+
305
+ client = MemcodeClient(api_key="bad-key")
306
+
307
+ try:
308
+ client.ingest(user_query="test", user_id="u1")
309
+ except AuthenticationError as e:
310
+ print(f"Auth failed (HTTP {e.status_code}): {e.message}")
311
+ except RateLimitError as e:
312
+ print(f"Throttled, retry after {e.retry_after}s")
313
+ except NotReadyError:
314
+ print("Pipelines still loading, try again shortly")
315
+ ```
316
+
317
+ ### Configuration
318
+
319
+ | Parameter | Env var | Default |
320
+ |-----------|---------|---------|
321
+ | `api_url` | `MEMCODE_API_URL` | `http://localhost:8000` |
322
+ | `api_key` | `MEMCODE_API_KEY` | _(empty, no auth)_ |
323
+ | `timeout` | &mdash; | `120` seconds |
324
+ | `access_token_provider` | &mdash; | _(optional, async clients only)_ |
325
+
326
+ ---
327
+
328
+ ## TypeScript
329
+
330
+ **Location:** `memcode-ts/`
331
+ **Package name:** `memcode-sdk`
332
+
333
+ ### Install
334
+
335
+ ```bash
336
+ npm install memcode-sdk
337
+ ```
338
+
339
+ ### Usage
340
+
341
+ ```typescript
342
+ import { MemcodeClient } from "memcode-sdk";
343
+
344
+ const client = new MemcodeClient("http://localhost:8000", "sk-...");
345
+
346
+ // Health
347
+ const ready = await client.isReady();
348
+
349
+ // Ingest
350
+ const result = await client.ingest({
351
+ user_query: "I just adopted a golden retriever named Max!",
352
+ agent_response: "That's wonderful!",
353
+ user_id: "user_42",
354
+ });
355
+
356
+ // Retrieve
357
+ const answer = await client.retrieve({
358
+ query: "Do I have any pets?",
359
+ user_id: "user_42",
360
+ });
361
+ console.log(answer.answer);
362
+
363
+ // Search
364
+ const hits = await client.search({
365
+ query: "pets",
366
+ user_id: "user_42",
367
+ domains: ["profile", "summary"],
368
+ top_k: 5,
369
+ });
370
+ hits.results.forEach((r) => console.log(`[${r.domain}] ${r.content}`));
371
+
372
+ const job = await client.ingestV2({
373
+ user_query: "I now lead the platform team",
374
+ effort_level: "high",
375
+ });
376
+ const imageJob = await client.ingestV2({
377
+ image_url: "https://example.com/whiteboard.jpg",
378
+ });
379
+ const status = await client.getIngestStatusV2(job.job_id);
380
+ const batch = await client.batchIngestV2({ items: [
381
+ { user_query: "The demo starts at 10 AM" },
382
+ { user_query: "The launch is Friday", effort_level: "high" },
383
+ ] });
384
+ const batchStatus = await client.getBatchIngestStatusV2(batch.job_id);
385
+ const memories = await client.listAllMemoriesV2();
386
+ const graph = await client.getMemoryGraphV2({ limit: 500, edge_limit: 5_000 });
387
+ const hybrid = await client.searchV2({
388
+ query: "pets",
389
+ top_k: 10,
390
+ original_top_k: 5,
391
+ mode: "chunks",
392
+ });
393
+ const advancedAnswer = await client.retrieveV2({
394
+ query: "Do I have pets?",
395
+ });
396
+ ```
397
+
398
+ ### OAuth and dynamic bearer tokens
399
+
400
+ TypeScript 2.5 adds `MemcodeOAuthClient`, `OAuthTokenStore`, and
401
+ `DelegatingMemoryTokenProvider`. The flow uses dynamic client registration,
402
+ Authorization Code with S256 PKCE, resource indicators, rotating refresh
403
+ tokens, and revocation:
404
+
405
+ ```typescript
406
+ import { MemcodeClient, MemcodeOAuthClient } from "memcode-sdk";
407
+
408
+ const oauth = new MemcodeOAuthClient({
409
+ issuer: "https://memory.memcode.in/",
410
+ resource: "https://memory.memcode.in",
411
+ clientId: persistedDynamicClientId,
412
+ tokenKey: `voice:${applicationUserId}`,
413
+ tokenStore: encryptedDistributedTokenStore,
414
+ });
415
+
416
+ const request = await oauth.createAuthorizationRequest({
417
+ redirectUri: "https://voice.example.com/oauth/callback",
418
+ });
419
+ await oauth.exchangeCode({
420
+ code: callbackCode,
421
+ returnedState: callbackState,
422
+ authorizationRequest: request,
423
+ });
424
+
425
+ const oauthClient = new MemcodeClient("https://memory.memcode.in", oauth);
426
+ ```
427
+
428
+ Dynamic-registration responses may expose the server-assigned, read-only
429
+ `integrationId`, `integrationChannel`, `attributionStatus`, and
430
+ `attributionBasis` fields. The SDK never sends those values in registration or
431
+ Memory API requests; no integration-supplied attribution parameter or new
432
+ secret is required. All fields remain optional for older servers, and the same
433
+ server-owned names are rejected in tenant-bound v2 request metadata.
434
+
435
+ The legacy function-style `MemcodeV2Client.accessTokenProvider` remains
436
+ supported. A refreshable object provider gets at most one refresh and one
437
+ request retry after a 401.
438
+
439
+ ### Error handling
440
+
441
+ ```typescript
442
+ import { MemcodeClient, AuthenticationError, RateLimitError } from "memcode-sdk";
443
+
444
+ try {
445
+ await client.ingest({ user_query: "test", user_id: "u1" });
446
+ } catch (e) {
447
+ if (e instanceof AuthenticationError) {
448
+ console.error(`Auth failed: ${e.message}`);
449
+ } else if (e instanceof RateLimitError) {
450
+ console.error(`Rate limited, retry after ${e.retryAfter}s`);
451
+ }
452
+ }
453
+ ```
454
+
455
+ ---
456
+
457
+ ## Go
458
+
459
+ **Location:** `memcode-go/`
460
+ **Module:** `gitlab.com/xortex1/memcode-sdk/memcode-go/v2`
461
+
462
+ ### Install
463
+
464
+ ```bash
465
+ go get gitlab.com/xortex1/memcode-sdk/memcode-go/v2
466
+ ```
467
+
468
+ ### Usage
469
+
470
+ ```go
471
+ package main
472
+
473
+ import (
474
+ "fmt"
475
+ memcode "gitlab.com/xortex1/memcode-sdk/memcode-go/v2"
476
+ )
477
+
478
+ func main() {
479
+ client := memcode.NewClient("http://localhost:8000", "sk-...")
480
+
481
+ // Health
482
+ if client.IsReady() {
483
+ fmt.Println("Memcode API is ready")
484
+ }
485
+
486
+ // Ingest
487
+ result, err := client.Ingest(memcode.IngestParams{
488
+ UserQuery: "I'm moving to Seattle next month.",
489
+ AgentResponse: "Good luck with your move!",
490
+ UserID: "user_42",
491
+ })
492
+ if err != nil {
493
+ panic(err)
494
+ }
495
+ fmt.Println("Model:", result.Model)
496
+
497
+ // Retrieve
498
+ answer, err := client.Retrieve(memcode.RetrieveParams{
499
+ Query: "Where am I moving?",
500
+ UserID: "user_42",
501
+ })
502
+ if err != nil {
503
+ panic(err)
504
+ }
505
+ fmt.Println("Answer:", answer.Answer)
506
+
507
+ // Search
508
+ hits, err := client.Search(memcode.SearchParams{
509
+ Query: "location",
510
+ UserID: "user_42",
511
+ Domains: []string{"profile", "temporal"},
512
+ TopK: 10,
513
+ })
514
+ if err != nil {
515
+ panic(err)
516
+ }
517
+ for _, r := range hits.Results {
518
+ fmt.Printf("[%s] %s (%.2f)\n", r.Domain, r.Content, r.Score)
519
+ }
520
+
521
+ job, err := client.IngestV2(memcode.PersonalV2IngestParams{
522
+ UserQuery: "I'm moving to Seattle next month.",
523
+ EffortLevel: "high",
524
+ }, "move-1")
525
+ if err != nil {
526
+ panic(err)
527
+ }
528
+ status, err := client.GetIngestStatusV2(job.JobID)
529
+
530
+ imageJob, err := client.IngestV2(memcode.PersonalV2IngestParams{
531
+ ImageURL: "https://example.com/whiteboard.jpg",
532
+ }, "whiteboard-1")
533
+ if err != nil {
534
+ panic(err)
535
+ }
536
+
537
+ batch, err := client.BatchIngestV2(memcode.PersonalV2BatchIngestParams{
538
+ Items: []memcode.PersonalV2BatchIngestItem{
539
+ {UserQuery: "The demo starts at 10 AM"},
540
+ {UserQuery: "The launch is Friday", EffortLevel: "high"},
541
+ },
542
+ })
543
+ if err != nil {
544
+ panic(err)
545
+ }
546
+ batchStatus, err := client.GetBatchIngestStatusV2(batch.JobID)
547
+
548
+ hybrid, err := client.SearchV2WithResultMode(memcode.HybridSearchParams{
549
+ Query: "location",
550
+ OriginalTopK: 5,
551
+ }, memcode.PersonalV2SearchOptions{
552
+ TopK: 10,
553
+ }, memcode.SearchResultModeMemories)
554
+ if err != nil {
555
+ panic(err)
556
+ }
557
+
558
+ memories, err := client.ListAllMemoriesV2(500)
559
+ if err != nil {
560
+ panic(err)
561
+ }
562
+ edgeLimit := 5_000
563
+ graph, err := client.GetMemoryGraphV2(memcode.PersonalMemoryGraphParams{
564
+ Limit: 500, EdgeLimit: &edgeLimit,
565
+ })
566
+ if err != nil {
567
+ panic(err)
568
+ }
569
+ advancedAnswer, err := client.RetrieveV2(memcode.RetrieveParams{
570
+ Query: "Where am I moving?",
571
+ })
572
+ fmt.Println("V2:", status.Status, imageJob.JobID, batchStatus.Status, len(graph.Nodes), hybrid.Total, advancedAnswer.Answer)
573
+ }
574
+ ```
575
+
576
+ ### OAuth and dynamic bearer tokens
577
+
578
+ Go 2.4 adds `OAuthClient`, `OAuthTokenStore`, and
579
+ `DelegatingMemoryTokenProvider`. OAuth operations take `context.Context`; the
580
+ API clients resolve a token per request and perform at most one refresh and one
581
+ retry after a 401.
582
+
583
+ ```go
584
+ store := encryptedDistributedTokenStore
585
+ oauth, err := memcode.NewOAuthClient(memcode.OAuthClientOptions{
586
+ Issuer: "https://memory.memcode.in/",
587
+ Resource: "https://memory.memcode.in",
588
+ ClientID: persistedDynamicClientID,
589
+ TokenKey: "voice:" + applicationUserID,
590
+ TokenStore: store,
591
+ })
592
+ if err != nil {
593
+ panic(err)
594
+ }
595
+
596
+ request, err := oauth.CreateAuthorizationRequest(ctx,
597
+ memcode.CreateAuthorizationRequestOptions{
598
+ RedirectURI: "https://voice.example.com/oauth/callback",
599
+ })
600
+ // Redirect to request.AuthorizationURL and retain request server-side.
601
+ tokens, err := oauth.ExchangeCode(ctx, callbackCode, callbackState, request)
602
+ _ = tokens
603
+
604
+ client, err := memcode.NewClientWithAccessTokenProvider(
605
+ "https://memory.memcode.in",
606
+ oauth,
607
+ )
608
+ ```
609
+
610
+ `RegisterOAuthClientOptions` also accepts `SoftwareID` and `SoftwareVersion`.
611
+ `SoftwareID` is a public analytics identifier, not a credential; when it is the
612
+ only registry match, the server returns unverified `dcr_software_id`
613
+ attribution. An exact registered HTTPS callback is still required for verified
614
+ SDK attribution.
615
+
616
+ `OAuthClientRegistration` may contain read-only `IntegrationID`,
617
+ `IntegrationChannel`, `AttributionStatus`, and `AttributionBasis` values
618
+ assigned by the server. They are not DCR inputs and are never copied to Memory
619
+ API requests, so callers need no attribution parameter or additional secret.
620
+ The fields are empty when an older server omits them. Tenant-bound v2 ingest
621
+ rejects the same server-owned names in request metadata.
622
+
623
+ The supplied store must encrypt tokens, atomically replace rotated token sets,
624
+ and implement `WithRefreshLease` as a distributed lease when multiple workers
625
+ share a grant. `NewInMemoryOAuthTokenStore` is only for tests and examples.
626
+
627
+ ### Error handling
628
+
629
+ ```go
630
+ result, err := client.Ingest(params)
631
+ if err != nil {
632
+ switch e := err.(type) {
633
+ case *memcode.AuthenticationError:
634
+ fmt.Println("Bad API key:", e.Message)
635
+ case *memcode.RateLimitError:
636
+ fmt.Printf("Throttled, retry after %ds\n", e.RetryAfter)
637
+ case *memcode.NotReadyError:
638
+ fmt.Println("Pipelines loading, retry shortly")
639
+ default:
640
+ fmt.Println("Error:", err)
641
+ }
642
+ }
643
+ ```
644
+
645
+ ---
646
+
647
+ ## API Reference
648
+
649
+ All three SDKs expose backwards-compatible v1 methods plus advanced personal v2 methods:
650
+
651
+ | Method | Endpoint | Description |
652
+ |--------|----------|-------------|
653
+ | **ingest** | `POST /v1/memory/ingest` | Store a conversation turn. Memcode classifies the input and extracts profile facts, temporal events, and summaries automatically. |
654
+ | **retrieve** | `POST /v1/memory/retrieve` | Answer a question using stored memories. Returns an LLM-generated answer with source citations and a confidence score. |
655
+ | **search** | `POST /v1/memory/search` | Raw semantic search across memory domains (`profile`, `temporal`, `summary`). Returns matching records without an LLM answer. |
656
+ | **personal v2 ingest** | `POST /v2/memory/ingest` | Start a durable normal-user ingest job. |
657
+ | **personal v2 status** | `GET /v2/memory/ingest/{job_id}/status` | Poll durable ingest progress. |
658
+ | **personal v2 batch ingest** | `POST /v2/memory/batch-ingest` | Start one durable job for 1 to 100 turns. |
659
+ | **personal v2 batch status** | `GET /v2/memory/jobs/{job_id}/status` | Poll the status URL returned by batch ingest. |
660
+ | **personal v2 list** | `GET /v2/memory` | List memories for the user derived from the credential. Paginate with `limit` and `offset`. |
661
+ | **personal v2 graph** | `GET /v2/memory-graph` | Read independently paginated memory nodes and weighted edges for the user derived from the credential. |
662
+ | **personal v2 search** | `POST /v2/memory/search` | Search extracted memories and original chunks for the user derived from the credential. |
663
+ | **personal v2 retrieve** | `POST /v2/memory/retrieve` | Advanced attributed retrieval with connection-learning metadata. |
664
+ | **ping** | `GET /health` | Health/readiness check. Never raises on a valid HTTP response. |
665
+
666
+ ## Error Types
667
+
668
+ | Error | HTTP status | When |
669
+ |-------|-------------|------|
670
+ | `AuthenticationError` | 401 / 403 | Missing or invalid API key |
671
+ | `RateLimitError` | 429 | Per-key rate limit exceeded |
672
+ | `ValidationError` | 422 | Request body failed validation |
673
+ | `NotReadyError` | 503 | Pipelines still initializing |
674
+ | `ServerError` | 5xx | Server-side failure |
675
+ | `ConnectionError` | &mdash; | Network timeout, DNS failure, connection refused |