Metadata-Version: 2.4
Name: memcode-sdk
Version: 2.5.0
Summary: Python SDK for the Memcode long-term memory API
Author: Memcode
License-Expression: Apache-2.0
Project-URL: Homepage, https://memcode.in
Project-URL: Repository, https://gitlab.com/xortex1/memcode-sdk
Project-URL: Issues, https://gitlab.com/xortex1/memcode-sdk/-/issues
Keywords: memory,long-term-memory,llm,rag,ai-agent
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: httpx<1,>=0.24

<h1 align="center">Memcode Client SDKs</h1>

<p align="center">
Official client libraries for the <strong>Memcode long-term memory API</strong>.<br>
Available in Python, TypeScript, and Go.
</p>

All three SDKs share the same design principles:

- Existing v1 clients keep three 1:1 methods: **ingest**, **retrieve**, **search**
- Bearer-token authentication via a static key or refreshable token provider
- Typed error hierarchy so callers can handle auth, rate-limit, and server errors distinctly
- Zero config defaults &mdash; point at `localhost:8000` with no key and it just works in dev

The existing `MemcodeClient`/`Client` APIs remain v1-compatible and now expose an
additive advanced personal v2 surface for authenticated personal callers:

- Python: `ingest_v2`, `batch_ingest_v2`, `get_ingest_status_v2`,
  `get_batch_ingest_status_v2`, `list_memories_v2`, `list_all_memories_v2`,
  `get_memory_graph_v2`, `search_v2`, `retrieve_v2`
- TypeScript: `ingestV2`, `batchIngestV2`, `getIngestStatusV2`,
  `getBatchIngestStatusV2`, `listMemoriesV2`, `listAllMemoriesV2`,
  `getMemoryGraphV2`, `searchV2`, `retrieveV2`
- Go: `IngestV2`, `BatchIngestV2`, `GetIngestStatusV2`,
  `GetBatchIngestStatusV2`, `SearchV2`, `SearchV2WithOptions`,
  `SearchV2WithResultMode`, `RetrieveV2`, `ListMemoriesV2`,
  `ListAllMemoriesV2`, `GetMemoryGraphV2`

Personal v2 derives the user from the API key or JWT, so no `user_id` is needed.
Ingestion returns a durable job receipt, retrieval includes
attribution/connection-learning metadata, and personal v2 search uses the
unified `/v2/memory/search` route.
Its optional result `mode` is distinct from `search_mode`: `default` returns
memories and chunks, `chunks` returns only stored chunks, and `memories` returns
only extracted memories. SDKs omit `mode` when it is `default` for compatibility.
Unified v2 search does not accept a domain filter. Whenever the selected mode
includes memory results, the backend searches the fixed `profile`, `summary`,
and `temporal` domains. The result `mode` remains the way to choose memories,
stored chunks, or both.
The deprecated `user_id` argument for single-item ingest remains accepted and is
sent only when supplied. Batch ingest derives identity from the credential and
does not accept `user_id` or `forget`. Explicit `POST /v2/memory/batch-ingest`
requests return `409` when raw original storage is enabled.
For personal v2 ingest, `user_query` is optional only when a nonblank
`image_url` is supplied; text-only and mixed text/image requests are unchanged.

```python
from memcode_sdk import MemcodeClient

client = MemcodeClient(api_url="https://memory.example.com", api_key="sk-...")
job = client.ingest_v2(
    user_query="The launch is Friday",
)
image_job = client.ingest_v2(image_url="https://example.com/whiteboard.jpg")
status = client.get_ingest_status_v2(job.job_id)
batch = client.batch_ingest_v2([
    {"user_query": "The demo starts at 10 AM"},
    {"user_query": "The launch is Friday", "effort_level": "high"},
])
batch_status = client.get_batch_ingest_status_v2(batch.job_id)
memories = client.list_all_memories_v2()
graph = client.get_memory_graph_v2(limit=500, edge_limit=5_000)
hits = client.search_v2(query="launch date")
chunk_hits = client.search_v2(query="launch date", mode="chunks")
answer = client.retrieve_v2(query="When is launch?")
```

## Prerequisites

A running Memcode API server:

```bash
uvicorn src.api.app:create_app --factory --host 0.0.0.0 --port 8000
```

## Python

**Location:** `memcode_sdk/`

### Install

```bash
pip install memcode-sdk
```

### Sync usage

```python
from memcode_sdk import MemcodeClient

client = MemcodeClient(api_url="http://localhost:8000", api_key="sk-...")

# Check health
health = client.ping()
print(health.status, health.pipelines_ready)

# Ingest a conversation turn
result = client.ingest(
    user_query="I just got promoted to senior engineer at Google!",
    agent_response="Congratulations on your promotion!",
    user_id="user_42",
)
print(result.profile, result.temporal)

# Retrieve an LLM-generated answer backed by memory
answer = client.retrieve(query="What is my job title?", user_id="user_42")
print(answer.answer)
print(answer.sources)       # list of SourceRecord
print(answer.confidence)

# Raw semantic search (no LLM answer)
hits = client.search(
    query="work history",
    user_id="user_42",
    domains=["profile", "temporal"],
    top_k=10,
)
for r in hits.results:
    print(f"[{r.domain}] {r.content}  (score={r.score:.2f})")

job = client.ingest_v2(
    user_query="I now lead the platform team",
    effort_level="high",
)
status = client.get_ingest_status_v2(job.job_id)
batch = client.batch_ingest_v2([
    {"user_query": "I now lead the platform team"},
    {"user_query": "Our launch is Friday", "effort_level": "high"},
])
batch_status = client.get_batch_ingest_status_v2(batch.job_id)
memories = client.list_all_memories_v2()
hybrid = client.search_v2(
    query="work history",
    top_k=10,
    original_top_k=5,
    mode="memories",
)
advanced_answer = client.retrieve_v2(
    query="What team do I lead?",
)

client.close()
```

### Async usage

```python
from memcode_sdk import AsyncMemcodeClient

async with AsyncMemcodeClient(api_url="http://localhost:8000") as client:
    result = await client.ingest(
        user_query="I love hiking in the Rockies.",
        user_id="user_42",
    )
    answer = await client.retrieve(query="hobbies", user_id="user_42")
    memories = await client.list_all_memories_v2()
    graph = await client.get_memory_graph_v2(limit=500, edge_limit=5_000)
    print(answer.answer)
```

### Async OAuth and dynamic bearer tokens

`AsyncMemcodeClient` can resolve a bearer token before every request. Static
`api_key` callers are unchanged; use exactly one of `api_key` or
`access_token_provider`.

```python
from memcode_sdk import AsyncMemcodeClient, AsyncMemcodeOAuthClient

# Use an application-owned encrypted AsyncOAuthTokenStore in production. The
# token key must identify one authenticated application user/grant.
oauth = AsyncMemcodeOAuthClient(
    issuer="https://memory.memcode.in/",
    resource="https://memory.memcode.in",
    client_id=persisted_dynamic_client_id,
    token_key=f"pipecat:{application_user_id}",
    token_store=encrypted_token_store,
)

try:
    # Begin the connect flow. Persist this short-lived request server-side,
    # then redirect the user's browser to request.authorization_url.
    request = await oauth.create_authorization_request(
        redirect_uri="https://voice.example.com/oauth/callback",
    )

    # In the callback, validate state and exchange the code. The OAuth helper
    # atomically stores each rotated token set through token_store.
    await oauth.exchange_code(
        code=callback_code,
        returned_state=callback_state,
        authorization_request=request,
    )

    async with AsyncMemcodeClient(
        api_url="https://memory.memcode.in",
        access_token_provider=oauth,
    ) as client:
        memories = await client.search_v2(query="What should I remember?")
finally:
    # The API client does not own an injected provider.
    await oauth.close()
```

Register a public client once per deployment/redirect set and persist the
returned `client_id`:

```python
registration = await oauth.register_client(
    client_name="My Pipecat agent",
    redirect_uris=["https://voice.example.com/oauth/callback"],
    application_type="web",
    software_id="ai.pipecat.memcode",
    software_version="1.0.0",
)
```

`software_id` is a stable public package identifier, not a credential. If it is
registered by Memcode, loopback or otherwise unverified callbacks can be counted
under that integration with `attribution_status="unverified"` and
`attribution_basis="dcr_software_id"`. An exact registered HTTPS callback is
still required for verified SDK attribution. Conflicting callback and software
owners are rejected by the authorization server.

Memcode may return read-only `integration_id`, `integration_channel`,
`attribution_status`, and `attribution_basis` values on the registration.
They are assigned by the server from its trusted integration registry. The SDK
does not send these values during registration or on Memory API requests, so
applications need no attribution parameter or additional attribution secret.
Older servers may omit all four values. Tenant-bound v2 requests reject these
server-owned field names when supplied in request metadata.

The helper uses Authorization Code with S256 PKCE, resource indicators,
rotating refresh tokens, and single-flight refresh per token-store key. A 401
causes at most one refresh and one request retry.

While a Memcode deployment issues only hosted-MCP resource tokens, wrap that
OAuth provider with `DelegatingMemoryTokenProvider`. The rest of the
application continues to use the normal `AsyncMemcodeClient` interface:

```python
from memcode_sdk import AsyncMemcodeOAuthClient, DelegatingMemoryTokenProvider

mcp_oauth = AsyncMemcodeOAuthClient(
    issuer="https://memory.memcode.in/",
    resource="https://mcp.memcode.in/mcp",
    scopes=("memory:read", "memory:write", "memory:connections:write"),
    client_id=persisted_dynamic_client_id,
    token_key=f"pipecat:{application_user_id}",
    token_store=encrypted_token_store,
)
memory_tokens = DelegatingMemoryTokenProvider(mcp_oauth)
try:
    async with AsyncMemcodeClient(
        api_url="https://memory.memcode.in",
        access_token_provider=memory_tokens,
    ) as client:
        memories = await client.search_v2(query="What should I remember?")
finally:
    # Both providers are application-owned; the wrapper does not close its
    # source OAuth provider.
    await memory_tokens.close()
    await mcp_oauth.close()
```

The in-memory token store exported by the SDK is for tests and examples in one
process/event loop only. Production `AsyncOAuthTokenStore` implementations must
encrypt tokens at rest, make token-set replacement atomic, and implement
`refresh_lease(key)` with a distributed lock or transaction covering every
worker that shares the store. The lease must span token reload, refresh, and
save (and renew or fence a time-limited lock) so two workers cannot reuse a
rotating refresh token. The same lease serializes reconnect persistence and
revocation/deletion against refresh. The SDK shields these remote side effects
and their local persistence from request cancellation, then waits for them
during provider cleanup.

### Error handling

```python
from memcode_sdk import MemcodeClient, AuthenticationError, RateLimitError, NotReadyError

client = MemcodeClient(api_key="bad-key")

try:
    client.ingest(user_query="test", user_id="u1")
except AuthenticationError as e:
    print(f"Auth failed (HTTP {e.status_code}): {e.message}")
except RateLimitError as e:
    print(f"Throttled, retry after {e.retry_after}s")
except NotReadyError:
    print("Pipelines still loading, try again shortly")
```

### Configuration

| Parameter | Env var | Default |
|-----------|---------|---------|
| `api_url` | `MEMCODE_API_URL` | `http://localhost:8000` |
| `api_key` | `MEMCODE_API_KEY` | _(empty, no auth)_ |
| `timeout` | &mdash; | `120` seconds |
| `access_token_provider` | &mdash; | _(optional, async clients only)_ |

---

## TypeScript

**Location:** `memcode-ts/`
**Package name:** `memcode-sdk`

### Install

```bash
npm install memcode-sdk
```

### Usage

```typescript
import { MemcodeClient } from "memcode-sdk";

const client = new MemcodeClient("http://localhost:8000", "sk-...");

// Health
const ready = await client.isReady();

// Ingest
const result = await client.ingest({
  user_query: "I just adopted a golden retriever named Max!",
  agent_response: "That's wonderful!",
  user_id: "user_42",
});

// Retrieve
const answer = await client.retrieve({
  query: "Do I have any pets?",
  user_id: "user_42",
});
console.log(answer.answer);

// Search
const hits = await client.search({
  query: "pets",
  user_id: "user_42",
  domains: ["profile", "summary"],
  top_k: 5,
});
hits.results.forEach((r) => console.log(`[${r.domain}] ${r.content}`));

const job = await client.ingestV2({
  user_query: "I now lead the platform team",
  effort_level: "high",
});
const imageJob = await client.ingestV2({
  image_url: "https://example.com/whiteboard.jpg",
});
const status = await client.getIngestStatusV2(job.job_id);
const batch = await client.batchIngestV2({ items: [
  { user_query: "The demo starts at 10 AM" },
  { user_query: "The launch is Friday", effort_level: "high" },
] });
const batchStatus = await client.getBatchIngestStatusV2(batch.job_id);
const memories = await client.listAllMemoriesV2();
const graph = await client.getMemoryGraphV2({ limit: 500, edge_limit: 5_000 });
const hybrid = await client.searchV2({
  query: "pets",
  top_k: 10,
  original_top_k: 5,
  mode: "chunks",
});
const advancedAnswer = await client.retrieveV2({
  query: "Do I have pets?",
});
```

### OAuth and dynamic bearer tokens

TypeScript 2.5 adds `MemcodeOAuthClient`, `OAuthTokenStore`, and
`DelegatingMemoryTokenProvider`. The flow uses dynamic client registration,
Authorization Code with S256 PKCE, resource indicators, rotating refresh
tokens, and revocation:

```typescript
import { MemcodeClient, MemcodeOAuthClient } from "memcode-sdk";

const oauth = new MemcodeOAuthClient({
  issuer: "https://memory.memcode.in/",
  resource: "https://memory.memcode.in",
  clientId: persistedDynamicClientId,
  tokenKey: `voice:${applicationUserId}`,
  tokenStore: encryptedDistributedTokenStore,
});

const request = await oauth.createAuthorizationRequest({
  redirectUri: "https://voice.example.com/oauth/callback",
});
await oauth.exchangeCode({
  code: callbackCode,
  returnedState: callbackState,
  authorizationRequest: request,
});

const oauthClient = new MemcodeClient("https://memory.memcode.in", oauth);
```

Dynamic-registration responses may expose the server-assigned, read-only
`integrationId`, `integrationChannel`, `attributionStatus`, and
`attributionBasis` fields. The SDK never sends those values in registration or
Memory API requests; no integration-supplied attribution parameter or new
secret is required. All fields remain optional for older servers, and the same
server-owned names are rejected in tenant-bound v2 request metadata.

The legacy function-style `MemcodeV2Client.accessTokenProvider` remains
supported. A refreshable object provider gets at most one refresh and one
request retry after a 401.

### Error handling

```typescript
import { MemcodeClient, AuthenticationError, RateLimitError } from "memcode-sdk";

try {
  await client.ingest({ user_query: "test", user_id: "u1" });
} catch (e) {
  if (e instanceof AuthenticationError) {
    console.error(`Auth failed: ${e.message}`);
  } else if (e instanceof RateLimitError) {
    console.error(`Rate limited, retry after ${e.retryAfter}s`);
  }
}
```

---

## Go

**Location:** `memcode-go/`
**Module:** `gitlab.com/xortex1/memcode-sdk/memcode-go/v2`

### Install

```bash
go get gitlab.com/xortex1/memcode-sdk/memcode-go/v2
```

### Usage

```go
package main

import (
    "fmt"
    memcode "gitlab.com/xortex1/memcode-sdk/memcode-go/v2"
)

func main() {
    client := memcode.NewClient("http://localhost:8000", "sk-...")

    // Health
    if client.IsReady() {
        fmt.Println("Memcode API is ready")
    }

    // Ingest
    result, err := client.Ingest(memcode.IngestParams{
        UserQuery:     "I'm moving to Seattle next month.",
        AgentResponse: "Good luck with your move!",
        UserID:        "user_42",
    })
    if err != nil {
        panic(err)
    }
    fmt.Println("Model:", result.Model)

    // Retrieve
    answer, err := client.Retrieve(memcode.RetrieveParams{
        Query:  "Where am I moving?",
        UserID: "user_42",
    })
    if err != nil {
        panic(err)
    }
    fmt.Println("Answer:", answer.Answer)

    // Search
    hits, err := client.Search(memcode.SearchParams{
        Query:   "location",
        UserID:  "user_42",
        Domains: []string{"profile", "temporal"},
        TopK:    10,
    })
    if err != nil {
        panic(err)
    }
    for _, r := range hits.Results {
        fmt.Printf("[%s] %s (%.2f)\n", r.Domain, r.Content, r.Score)
    }

    job, err := client.IngestV2(memcode.PersonalV2IngestParams{
        UserQuery:   "I'm moving to Seattle next month.",
        EffortLevel: "high",
    }, "move-1")
    if err != nil {
        panic(err)
    }
    status, err := client.GetIngestStatusV2(job.JobID)

    imageJob, err := client.IngestV2(memcode.PersonalV2IngestParams{
        ImageURL: "https://example.com/whiteboard.jpg",
    }, "whiteboard-1")
    if err != nil {
        panic(err)
    }

    batch, err := client.BatchIngestV2(memcode.PersonalV2BatchIngestParams{
        Items: []memcode.PersonalV2BatchIngestItem{
            {UserQuery: "The demo starts at 10 AM"},
            {UserQuery: "The launch is Friday", EffortLevel: "high"},
        },
    })
    if err != nil {
        panic(err)
    }
    batchStatus, err := client.GetBatchIngestStatusV2(batch.JobID)

    hybrid, err := client.SearchV2WithResultMode(memcode.HybridSearchParams{
        Query:        "location",
        OriginalTopK: 5,
    }, memcode.PersonalV2SearchOptions{
        TopK: 10,
    }, memcode.SearchResultModeMemories)
    if err != nil {
        panic(err)
    }

    memories, err := client.ListAllMemoriesV2(500)
    if err != nil {
        panic(err)
    }
    edgeLimit := 5_000
    graph, err := client.GetMemoryGraphV2(memcode.PersonalMemoryGraphParams{
        Limit: 500, EdgeLimit: &edgeLimit,
    })
    if err != nil {
        panic(err)
    }
    advancedAnswer, err := client.RetrieveV2(memcode.RetrieveParams{
        Query: "Where am I moving?",
    })
    fmt.Println("V2:", status.Status, imageJob.JobID, batchStatus.Status, len(graph.Nodes), hybrid.Total, advancedAnswer.Answer)
}
```

### OAuth and dynamic bearer tokens

Go 2.4 adds `OAuthClient`, `OAuthTokenStore`, and
`DelegatingMemoryTokenProvider`. OAuth operations take `context.Context`; the
API clients resolve a token per request and perform at most one refresh and one
retry after a 401.

```go
store := encryptedDistributedTokenStore
oauth, err := memcode.NewOAuthClient(memcode.OAuthClientOptions{
    Issuer:     "https://memory.memcode.in/",
    Resource:   "https://memory.memcode.in",
    ClientID:   persistedDynamicClientID,
    TokenKey:   "voice:" + applicationUserID,
    TokenStore: store,
})
if err != nil {
    panic(err)
}

request, err := oauth.CreateAuthorizationRequest(ctx,
    memcode.CreateAuthorizationRequestOptions{
        RedirectURI: "https://voice.example.com/oauth/callback",
    })
// Redirect to request.AuthorizationURL and retain request server-side.
tokens, err := oauth.ExchangeCode(ctx, callbackCode, callbackState, request)
_ = tokens

client, err := memcode.NewClientWithAccessTokenProvider(
    "https://memory.memcode.in",
    oauth,
)
```

`RegisterOAuthClientOptions` also accepts `SoftwareID` and `SoftwareVersion`.
`SoftwareID` is a public analytics identifier, not a credential; when it is the
only registry match, the server returns unverified `dcr_software_id`
attribution. An exact registered HTTPS callback is still required for verified
SDK attribution.

`OAuthClientRegistration` may contain read-only `IntegrationID`,
`IntegrationChannel`, `AttributionStatus`, and `AttributionBasis` values
assigned by the server. They are not DCR inputs and are never copied to Memory
API requests, so callers need no attribution parameter or additional secret.
The fields are empty when an older server omits them. Tenant-bound v2 ingest
rejects the same server-owned names in request metadata.

The supplied store must encrypt tokens, atomically replace rotated token sets,
and implement `WithRefreshLease` as a distributed lease when multiple workers
share a grant. `NewInMemoryOAuthTokenStore` is only for tests and examples.

### Error handling

```go
result, err := client.Ingest(params)
if err != nil {
    switch e := err.(type) {
    case *memcode.AuthenticationError:
        fmt.Println("Bad API key:", e.Message)
    case *memcode.RateLimitError:
        fmt.Printf("Throttled, retry after %ds\n", e.RetryAfter)
    case *memcode.NotReadyError:
        fmt.Println("Pipelines loading, retry shortly")
    default:
        fmt.Println("Error:", err)
    }
}
```

---

## API Reference

All three SDKs expose backwards-compatible v1 methods plus advanced personal v2 methods:

| Method | Endpoint | Description |
|--------|----------|-------------|
| **ingest** | `POST /v1/memory/ingest` | Store a conversation turn. Memcode classifies the input and extracts profile facts, temporal events, and summaries automatically. |
| **retrieve** | `POST /v1/memory/retrieve` | Answer a question using stored memories. Returns an LLM-generated answer with source citations and a confidence score. |
| **search** | `POST /v1/memory/search` | Raw semantic search across memory domains (`profile`, `temporal`, `summary`). Returns matching records without an LLM answer. |
| **personal v2 ingest** | `POST /v2/memory/ingest` | Start a durable normal-user ingest job. |
| **personal v2 status** | `GET /v2/memory/ingest/{job_id}/status` | Poll durable ingest progress. |
| **personal v2 batch ingest** | `POST /v2/memory/batch-ingest` | Start one durable job for 1 to 100 turns. |
| **personal v2 batch status** | `GET /v2/memory/jobs/{job_id}/status` | Poll the status URL returned by batch ingest. |
| **personal v2 list** | `GET /v2/memory` | List memories for the user derived from the credential. Paginate with `limit` and `offset`. |
| **personal v2 graph** | `GET /v2/memory-graph` | Read independently paginated memory nodes and weighted edges for the user derived from the credential. |
| **personal v2 search** | `POST /v2/memory/search` | Search extracted memories and original chunks for the user derived from the credential. |
| **personal v2 retrieve** | `POST /v2/memory/retrieve` | Advanced attributed retrieval with connection-learning metadata. |
| **ping** | `GET /health` | Health/readiness check. Never raises on a valid HTTP response. |

## Error Types

| Error | HTTP status | When |
|-------|-------------|------|
| `AuthenticationError` | 401 / 403 | Missing or invalid API key |
| `RateLimitError` | 429 | Per-key rate limit exceeded |
| `ValidationError` | 422 | Request body failed validation |
| `NotReadyError` | 503 | Pipelines still initializing |
| `ServerError` | 5xx | Server-side failure |
| `ConnectionError` | &mdash; | Network timeout, DNS failure, connection refused |
