Metadata-Version: 2.4
Name: memcode-sdk
Version: 2.4.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 constructor arg or `MEMCODE_API_KEY` env var
- 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",
)
```

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?",
});
```

### 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)
}
```

### 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 |
