# Python SDK

The Python SDK supports Python 3.11+ with typed synchronous and native asynchronous clients backed by pooled `httpx` connections.

## Install from this repository

```powershell
pip install .\sdk\python
```

The package has not been published to PyPI. CI can build the same wheel with `npm run python:wheel`.

## Synchronous client

```python
from memplumb import MemPlumbClient

with MemPlumbClient(
    base_url="http://127.0.0.1:6060",
    api_key="mp_live_...",
) as memory:
    event = memory.ingest(
        {
            "actor_id": "user_123",
            "facts": [
                {
                    "kind": "preference",
                    "key": "answer_style",
                    "value": "concise",
                    "confidence": 0.95,
                }
            ],
        },
        idempotency_key="conversation-42-message-7",
        request_id="agent-request-42",
    )
    context = memory.context(
        actor_id="user_123",
        query="How should I answer?",
        budget_tokens=1200,
    )
```

Use one client per service process or worker, rather than creating one per request. `close()` and the context manager release pooled connections.

## Asynchronous client

```python
import asyncio
from memplumb import AsyncMemPlumbClient

async def main() -> None:
    async with AsyncMemPlumbClient(api_key="mp_live_...") as memory:
        context, health = await asyncio.gather(
            memory.context(actor_id="user_123", query="restaurant"),
            memory.health(),
        )
        print(context["data"]["context_policy"])
        print(health["data"]["status"])

asyncio.run(main())
```

The async client uses native asynchronous sockets and supports cancellation. `aclose()` or `async with` releases the pool.

## Governed retrieval quality

Both clients expose the complete HTTP quality surface: `adjudicate_retrieval`,
`retrieval_adjudications`, `quality_review_queue`, `quality_review_tasks`, task
`assign`/`claim`/`renew`/`release`/`complete`/`resume`, and `quality_dataset`.
Candidate labels require the exact `memory_version` returned by Context
attribution.

```python
trace = memory.context_trace(context["data"]["context_id"])
candidate = trace["data"]["retrieval_candidates"][0]

memory.adjudicate_retrieval(
    trace["data"]["id"],
    {
        "verdict": "accepted",
        "rubric_version": "retrieval-v1",
        "labels": [
            {
                "memory_id": candidate["memory_id"],
                "memory_version": candidate["memory_version"],
                "label": "hard_negative",
                "reason": "stale",
            }
        ],
    },
    idempotency_key="incident-482-review-1",
)

queue = memory.quality_review_queue(
    rubric_version="retrieval-v1",
    signals=["unhelpful", "safety_failure"],
)

tasks = memory.quality_review_tasks(
    rubric_version="retrieval-v1",
    state="pending",
)
```

The typed clients generate a cryptographically random 256-bit token for claim or
takeover and return it as `data["lease_token"]` beside the claimed task. The SDK
does not log the token, but the return value is an ordinary typed mapping rather
than a special redacting handle. Application code assumes custody of this
operational credential and passes it explicitly with `owner_id` and the current
`lease_version` to renew, release, or complete. The server revalidates the
current stored generation and signal basis; those fields are not caller-supplied
lease preconditions.

Task completion requires an evaluator-bound managed client and atomically writes
the Retrieval Adjudication. Calling `adjudicate_retrieval` directly while the
same Context/rubric task is active returns a stable conflict instead of bypassing
assignment or fencing. `resume` is available only on an admin client and
requires an audit reason plus `evaluator_id` and `evaluator_type` for the new
generation. See
[quality-review-task-leases.md](./quality-review-task-leases.md).

`quality_dataset({"rubric_version": ..., "cutoff": ...})` requires `evaluation:export` and returns `quality-dataset-v1` in memory. The payload contains original queries and Memory values and is marked `restricted_memory_content`; the SDK neither writes nor logs it. The caller owns encryption, persistence, access control, retention, and deletion. For the adjudication rules and replay gate, see [retrieval-quality-loop.md](./retrieval-quality-loop.md).

Admin clients also expose synchronous and asynchronous
`control_retrieval_adjudication_evidence(...)` and
`retrieval_adjudication_evidence_controls(...)`. The control input targets an
evaluator principal, optional rubric, inclusive `since`, and exclusive/open
`until`; only quarantine may request atomic key revocation. Dataset manifest
types expose `quarantined_evidence_count`, `matching_control_count`, and
`evidence_control_fingerprint`. See
[retrieval-adjudication-evidence-controls.md](./retrieval-adjudication-evidence-controls.md).

## Memory Write Counterfactual

The sync and async clients expose the same discriminated Case and Replay inputs as OpenAPI. A write Case must select only `source_type="write_decision"`; a write Run must select only the Memory Policy family.

```python
write_case = memory.create_memory_case(
    {
        "source_type": "write_decision",
        "decision_id": "mwd_...",
    },
    idempotency_key="write-case-42",
)

write_run = memory.create_replay_run(
    {
        "case_ids": [write_case["data"]["case"]["id"]],
        "candidate_memory_policy": {
            "name": "candidate-memory",
            "version": 2,
            "minimum_confidence": 0.85,
        },
        "thresholds": {
            "maximum_new_sensitive_accepts": 0,
            "maximum_new_blocked_key_accepts": 0,
            "maximum_new_overwrites": 0,
            "maximum_execution_failures": 0,
        },
    },
    idempotency_key="write-replay-42",
)
```

`memory_cases(scenario_type="memory_write_counterfactual.v1")` lists only write Cases. The returned union types keep retrieval metrics separate from write totals/risks; callers should not interpret `changed` as improved or regressed. See [Memory Write Counterfactual](./memory-write-counterfactual.md).

## Reliability contract

| Operation                                | Automatic retry | Reason                                                                              |
| ---------------------------------------- | --------------- | ----------------------------------------------------------------------------------- |
| Health, list, trace, Context attribution | Yes             | Read-only                                                                           |
| Ingest                                   | Yes             | Every retry reuses the same persistent `Idempotency-Key`                            |
| Record Outcome                           | Yes             | Every retry reuses the same key; the Store atomically returns the original Outcome  |
| Record Retrieval Adjudication            | Yes             | Every retry reuses the same key; the Store atomically returns the original revision |
| Claim review task                        | Yes             | One call reuses its generated/provided token and `Idempotency-Key` across retries   |
| Renew/release review task                | No              | Expiry and ownership can change; re-read state before deciding whether to retry     |
| Complete review task                     | Yes             | One call reuses its token, lease version, request, and `Idempotency-Key`            |
| Materialize quality dataset              | Yes             | Deterministic read/materialization for the same cutoff and evidence state           |
| Build Context                            | No              | Creates a durable Context Run                                                       |
| Feedback                                 | No              | Creates a durable feedback label and may trigger rollback                           |

Retries cover network errors, `429`, and `5xx`, with bounded exponential backoff and `Retry-After` support. Set `max_retries=0` to disable them. A caller retrying Context or feedback must decide how duplicate evidence should be handled.

With managed authentication, create the API key with an evaluator binding. `record_outcome` and `adjudicate_retrieval` can omit `evaluator_id` and `evaluator_type`; the server supplies them and returns `evaluator_authentication="authenticated"`. Supplying a different identity is rejected. Idempotency is bound to the API-key principal, so two evaluator workers must share a key only when they represent the same logical evaluator.

Admin clients expose separate retrieval and Outcome evidence-control methods. Both map to append-only quarantine/restore APIs, and evaluator-scoped clients cannot call them. A control for one evidence kind never affects the other. See [outcome-evidence-controls.md](./outcome-evidence-controls.md) before using Outcome `revoke_key`, open-ended windows, or restore.

`MemPlumbError` exposes `status`, stable `code`, and `request_id`. Network failures use `NETWORK_ERROR`; malformed successful responses use `INVALID_RESPONSE`. Response bodies and authorization credentials are not retained on the exception.

## Types

The wheel ships `py.typed` and exports `TypedDict` contracts for ingest, memory, Context ranking explanations, Embedding Provider identity, feedback, Retrieval Adjudications and controls, quality review tasks/leases/events, quality datasets, release metadata, and response envelopes. `QualityReviewTaskClaim` deliberately includes the raw `lease_token`; callers must prevent it from entering logs, exception context, or persistent diagnostics.

```python
from memplumb import ContextResult, Envelope, IngestInput
```

All operations accept an optional `request_id`, allowing application traces to correlate with MemPlumb logs and HTTP envelopes.

For SDK development and CI:

```powershell
pip install -e ".\sdk\python[dev]"
npm run python:typecheck
npm run python:lint
npm run python:test
npm run python:wheel
```

Run the source example against a daemon:

```powershell
$env:PYTHONPATH = "sdk\python\src"
$env:MEMPLUMB_API_KEY = "mp_live_..."
python examples\python-quickstart.py
python examples\python-async-quickstart.py
```
