Metadata-Version: 2.5
Name: mnemoscale-sdk
Version: 0.1.1
Summary: Typed sync and async Python clients for the Mnemos memory API.
Project-URL: Homepage, https://mnemoscale.com
Project-URL: Repository, https://github.com/agentic-saas-developments/mnemos
Project-URL: Issues, https://support.mnemoscale.com
Author-email: Mnemos <support@mnemoscale.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agents,llm,memory,mnemos,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.8.0
Description-Content-Type: text/markdown

# Mnemos Python SDK

Typed sync + async clients for the Mnemos memory API (PRD §8): API-key auth
with retry/backoff on 429/5xx, document batching, job polling, a session
context manager, and webhook signature verification.

```bash
pip install mnemoscale-sdk    # in this repo: uv sync --all-packages
```

## Quickstart

```python
from mnemos_sdk import MnemosClient

client = MnemosClient("https://api.example.com", api_key="mk_live_...")

with client.session(agent_id=AGENT_ID, namespace_id=NAMESPACE_ID) as s:
    # Blocks until the extraction job completes (extract_async to fire-and-poll later).
    s.extract([{"content": "the customer asked about delivery delays", "role": "user"}])

    result = s.query("delivery delays", top_k=5, rerank=True)
    for r in result["results"]:
        if r["from_working_memory"]:
            continue  # session turns merged per PRD §7.1
        print(r["hybrid_score"], r["content"])
```

`AsyncMnemosClient` mirrors the same surface with `async`/`await`.

## Query controls (PRD §7.1)

```python
result = client.query(
    "what did the customer ask about delivery delays?",
    agent_id=AGENT_ID,
    routing="auto",            # auto | keyword | semantic | hybrid
    top_k=10,
    metadata_filter={          # optional JSONB containment (@>) over source metadata —
        "topics": ["billing"],  # e.g. T-001 enrichment fields topics/people/actions/type;
    },                         # composes AND-wise with scenario_type
    rerank=True,               # cross-encoder reranking, Pro tier and above;
                               # lower tiers skip with reason "tier_disallowed"
    scoring={                  # optional per-query hybrid weights (normalized to 1.0)
        "cosine_weight": 0.50,
        "importance_weight": 0.30,
        "recency_weight": 0.20,
        "recency_half_life_days": 7,
    },
    ef_search=80,              # optional HNSW beam width (10-400): recall ↔ latency
    max_context_tokens=2048,   # optional: pack merged results under a token budget
                               # (near-duplicate L0/search content suppressed)
)
print(result["query_id"], result["routing_used"], result["reranker_skipped_reason"])
```

## Verifying webhooks (PRD §13.2)

Mnemos signs every delivery with `X-Mnemos-Signature: t=<unix>,v1=<hex>`
(HMAC-SHA-256 over `"{t}.{body}"`, 300 s replay tolerance). Verify against the
**raw** request body before parsing it — any web framework works:

```python
from mnemos_sdk import WebhookVerificationError, verify_webhook_signature

async def webhook_endpoint(request):          # FastAPI/Starlette-style
    raw = await request.body()
    try:
        verify_webhook_signature(
            WEBHOOK_SECRET,
            request.headers.get("X-Mnemos-Signature"),
            raw,
        )
    except WebhookVerificationError as exc:
        # exc.reason: malformed_header | stale_timestamp | signature_mismatch
        return Response(status_code=400)
    event = json.loads(raw)
    ...
```

## Errors

Everything the SDK raises is a `MnemosError`, split into two branches:

```
MnemosError
├── MnemosApiError          # the API answered with a failure status
│   ├── AuthenticationError (401)   PaymentRequiredError (402)
│   ├── PermissionError     (403)   NotFoundError        (404)
│   ├── ConflictError       (409)   ValidationError      (422)
│   └── RateLimitError      (429)   ServerError          (5xx)
├── MnemosConnectionError   # the request never produced a response
├── MnemosTimeoutError      # a request, or a job poll, ran out of time
└── WebhookVerificationError
```

Catch `MnemosApiError` for "any HTTP failure but not a transport failure". Each
one carries `status_code`, `code` (the machine-readable reason from a structured
body), `correlation_id` (the `X-Correlation-Id` the API echoed — quote it in a
support ticket), `retryable`, and the raw `detail`. `PaymentRequiredError` adds
`manage_url`; `WebhookVerificationError` carries a `reason`.

Transport failures are wrapped, so handling an unreachable API never requires
importing `httpx` — the underlying exception stays on `__cause__`:

```python
from mnemos_sdk import MnemosApiError, MnemosConnectionError, RateLimitError

try:
    result = client.query("delivery delays", agent_id=AGENT_ID)
except RateLimitError as exc:
    ...                                  # exc.code, exc.retryable
except MnemosApiError as exc:
    log.error("mnemos %s (correlation %s)", exc.status_code, exc.correlation_id)
except MnemosConnectionError as exc:
    log.error("mnemos unreachable: %s", exc)
```

## Retries and per-request options

429 and 500/502/503/504 are retried `max_retries` times (default 3) with
exponential backoff (`retry_backoff_base * 2 ** (attempt - 1)`), except a 429
carrying `X-Mnemos-Retryable: false` and a 402 — both permanent, so both
surface at once. A server-sent `Retry-After` (delay-seconds or HTTP-date) wins
over the backoff, clamped to `retry_after_cap` (30 s). Transport failures are
replayed only when the request provably never left (connection-phase) or is
replay-safe (GET/DELETE, or a body with an `idempotency_key`).

Every method takes `agent_id`, `correlation_id` and `timeout` alongside its own
arguments; the client takes `default_headers` for headers that ride on every
request:

```python
client = MnemosClient(BASE_URL, API_KEY, default_headers={"X-Client-Name": "acme-agent"})
usage = client.get_usage(agent_id=AGENT_ID, correlation_id=request_id, timeout=5.0)
```

## Parity with the TypeScript SDK

`sdk-parity.json` at the repo root is the shared surface contract between this
package and `sdk-ts`: methods, error taxonomy, constants, retry policy and
request options, named in both languages. `tests/unit/test_sdk_parity.py` and
`sdk-ts/tests/parity.test.ts` assert their own side against it, so adding
something to one SDK fails the other's suite until it lands there too. The
manifest's `notes` record the differences that are deliberate (seconds vs
milliseconds, sync+async vs async-only, and so on).

See `quickstart.py` at the repo root for the runnable end-to-end example.

To hand these operations to an LLM as tools rather than calling them yourself,
`docs/Mnemos_ToolSpec_Cookbook_v1.0.md` has copy-paste OpenAI and Anthropic
tool definitions for query/extract/feedback, plus the dispatcher that routes a
tool call to this client. Both are CI-verified against these signatures.
