Python SDK
The khwan Python package is a thin HTTP wrapper over the
REST API — no engine code, just requests.
pip install khwanfrom khwan import Khwan, Turn, KhwanErrorKhwan(...)
The client. All arguments are keyword-only.
kw = Khwan(
api_key="kwk_live_xxx", # required
user_id="alice", # required — sets X-Khwan-User
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 | — | Required. End-user id; sent as X-Khwan-User. Identifies the user, not a separate brain. |
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.
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.
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.
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.
kw.sync()memory(limit=20) -> dict
Returns recent memory entries for the selected core. GET /memory?limit=....
mem = kw.memory(limit=50)cores() -> dict
Lists the account’s cores. GET /cores.
cores = kw.cores()metrics() -> dict
Returns coherence / learning / usage metrics for the selected core. GET /metrics.
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).
try:
turn = kw.prepare("hi")
except KhwanError as e:
print(e.status, e) # e.g. 429 quota exceeded — you are over your plan's limitFull example
from khwan import Khwan, KhwanError
import anthropic
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)