<!-- Source: https://docs.khwan.ai/sdk-python -->
<!-- The whole documentation in one file: https://docs.khwan.ai/llms-full.txt -->

# Python SDK

The `khwan` Python package is a thin HTTP wrapper over the
[REST API](/api-reference) — no engine code, just `requests`. It is open source:
read it at
[github.com/khwanlabs/khwan-client-python](https://github.com/khwanlabs/khwan-client-python).
"Thin" is a claim about code that handles your prompts, so it is one worth
checking rather than taking.

```bash
pip install khwan
```

```python
from khwan import Khwan, Turn, KhwanError
```

## `Khwan(...)`

The client. All arguments are keyword-only.

```python
kw = Khwan(
    api_key="kwk_live_xxx",           # required
    user_id="alice",                  # optional — X-Khwan-User → isolated per-user sub-brain
    core=None,                        # optional — sets X-Khwan-Core (isolated brain)
    base_url="https://api.khwan.ai",  # optional; override for on-prem
    model=None,                       # optional session model hint
    constitution=None,                # optional named constitution profile
    timeout=60,                       # optional request timeout (seconds)
)
```

| Argument | Type | Default | Notes |
| --- | --- | --- | --- |
| `api_key` | `str` | — | **Required.** Your Khwan key (`kwk_...`). Raises `ValueError` if missing. |
| `user_id` | `str \| None` | `None` | Optional. End-user id; sent as `X-Khwan-User`. Omit for one shared brain per account/core; set it to give each end-user an **isolated per-user sub-brain** (paid — the free plan includes a few). Composes with `core`. |
| `core` | `str \| None` | `None` | Core slug; sent as `X-Khwan-Core`. Selects an isolated brain. Omit for the default core. |
| `base_url` | `str` | `https://api.khwan.ai` | Override to point at an on-prem instance. |
| `model` | `str \| None` | `None` | Forwarded on `prepare`; may be overridden by dashboard settings. |
| `constitution` | `str \| None` | `None` | Named constitution profile reference. |
| `timeout` | `int` | `60` | Per-request timeout in seconds. |

  `memory=` and `embedder=` are **not** configurable here — they are server-managed
  and exist only in the on-prem engine. Passing them raises `TypeError`.

### Isolated cores

Each named `core` is a fully separate brain (own memory, identity, learning). Omit
`core` for the account's default core. Quota is pooled at the account level.

```python
client1 = Khwan(api_key="kwk_live_xxx", user_id="alice", core="client1")
```

## Methods

### `prepare(user_input) -> Turn`

Builds the turn context (memory + constitution + coherence). No LLM call. `POST /prepare`.

```python
turn = kw.prepare("remember I prefer short answers in Thai")
```

### `record(turn, answer) -> dict`

Hands your model's answer back so Khwan can persist and learn. `POST /record`.

```python
kw.record(turn, answer)
```

| Param | Type | Notes |
| --- | --- | --- |
| `turn` | `Turn` | The object returned by `prepare`. |
| `answer` | `str` | Your model's reply text. |

### `sync() -> dict`

Triggers a learning / consolidation pass. `POST /sync`.

```python
kw.sync()
```

### `memory(limit=20) -> dict`

Returns recent memory entries for the selected core. `GET /memory?limit=...`.

```python
mem = kw.memory(limit=50)
```

### `cores() -> dict`

Lists the account's cores. `GET /cores`.

```python
cores = kw.cores()
```

### `metrics() -> dict`

Returns coherence / learning / usage metrics for the selected core. `GET /metrics`.

```python
stats = kw.metrics()
```

## `Turn`

Returned by `prepare()`. Feed `.messages` to your model, then pass the object to
`record()`.

| Property | Type | Notes |
| --- | --- | --- |
| `messages` | `list[dict]` | `[{role, content}]` brief for your model. |
| `coherence` | `float \| None` | Coherence signal. |
| `sources` | `list` | Memory / retrieval sources used. |
| `allowed` | `bool` | `False` if the turn was gated. Defaults to `True`. |
| `reason` | `str \| None` | Why it was gated. |
| `turn_token` | `str \| None` | Opaque handle used by `record()`. |
| `raw()` | `dict` | Full server payload. |

## `KhwanError`

Raised on any non-2xx response. Has a `.status` attribute (the HTTP code).

```python
try:
    turn = kw.prepare("hi")
except KhwanError as e:
    print(e.status, e)   # e.g. 429 quota exceeded — you are over your plan's limit
```

## Full example

```python
from khwan import Khwan, KhwanError

kw  = Khwan(api_key="kwk_live_xxx", user_id="alice", core="client1")
llm = anthropic.Anthropic(api_key="sk-ant-...")

def my_own_llm(messages):
    system = next((m["content"] for m in messages if m["role"] == "system"), "")
    chat   = [m for m in messages if m["role"] != "system"]
    r = llm.messages.create(model="claude-sonnet-4-6", max_tokens=1024,
                            system=system, messages=chat)
    return r.content[0].text

try:
    turn = kw.prepare("remember I prefer short answers in Thai")
    if turn.allowed:
        answer = my_own_llm(turn.messages)
        kw.record(turn, answer)
        print(answer)
    else:
        print("blocked:", turn.reason)
except KhwanError as e:
    print("Khwan error", e.status, e)
```
