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.
- memcode_sdk-2.5.0/PKG-INFO +675 -0
- memcode_sdk-2.5.0/README.MD +652 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/__init__.py +40 -1
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/_http.py +58 -10
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/async_client.py +104 -7
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/async_v2_client.py +62 -11
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/client.py +192 -21
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/errors.py +16 -0
- memcode_sdk-2.5.0/memcode_sdk/oauth.py +1061 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/types.py +39 -3
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/v2_client.py +139 -8
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/v2_types.py +39 -0
- memcode_sdk-2.5.0/memcode_sdk.egg-info/PKG-INFO +675 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk.egg-info/SOURCES.txt +2 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/pyproject.toml +1 -1
- memcode_sdk-2.5.0/tests/test_oauth.py +924 -0
- memcode_sdk-2.5.0/tests/test_v2_clients.py +862 -0
- memcode_sdk-2.3.1/PKG-INFO +0 -387
- memcode_sdk-2.3.1/README.MD +0 -364
- memcode_sdk-2.3.1/memcode_sdk.egg-info/PKG-INFO +0 -387
- memcode_sdk-2.3.1/tests/test_v2_clients.py +0 -425
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk/py.typed +0 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk.egg-info/dependency_links.txt +0 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk.egg-info/requires.txt +0 -0
- {memcode_sdk-2.3.1 → memcode_sdk-2.5.0}/memcode_sdk.egg-info/top_level.txt +0 -0
- {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 — 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` | — | `120` seconds |
|
|
324
|
+
| `access_token_provider` | — | _(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` | — | Network timeout, DNS failure, connection refused |
|