> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aethis.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

> Typed Python client for the Aethis decision API — sync and async clients, stateful decision sessions, ergonomic Pydantic models.

The Python SDK wraps `api.aethis.ai` with sync and async clients, typed Pydantic response models, and a stateful `DecisionSession` adapter for wizard / chatbot intake flows.

Use this when you're shipping a Python service that calls Aethis from a server context — FastAPI, Django, a worker, a notebook. For ad-hoc evaluation from a terminal, prefer the [CLI](/interfaces/cli) or raw [curl](/interfaces/rest-api).

## Install

```bash theme={null}
uv add 'aethis-sdk>=0.7.0'
# or, for an isolated tool install:
uv pip install 'aethis-sdk>=0.7.0'
```

Python 3.11+. Pulls in `httpx` and `pydantic`. Source: [github.com/Aethis-ai/aethis-sdk-python](https://github.com/Aethis-ai/aethis-sdk-python).

<Note>
  **0.5.0 or later required for the rulebook surface.** `Aethis.decide_rulebook()` / `AsyncAethis.decide_rulebook()` and the `rulebook_id` field on `DecideResponse` ship in 0.5.0. The audit fields (`inputs_hash`, `decision_id`, `decision_time`, `engine_version`) on `DecideResponse` have been present since 0.3.2.
</Note>

<Note>
  **API key is optional for decision endpoints.** `Aethis()` without a key works for anonymous decides on public rulesets (`client.decide(ruleset_id="aethis/uk-fsm/child-eligibility", ...)`). A key is required for: composed rulebook decides via `decide_rulebook()`, private rulesets, and any authoring endpoint. [Request access →](https://aethis.ai/developer-access)
</Note>

## Quickstart — sync (anonymous on public rulesets)

```python theme={null}
from aethis_sdk import Aethis

with Aethis() as client:        # no api_key — public ruleset, anonymous OK
    response = client.decide(
        ruleset_id="aethis/uk-fsm/child-eligibility",
        field_values={
            "child.age": 10,
            "child.school_type": "state_funded",
        },
    )
    print(response.decision)        # "eligible" | "not_eligible" | "undetermined"
    print(response.inputs_hash)     # canonical SHA-256 fingerprint of the input set
    print(response.decision_id)     # per-call audit identifier
    print(response.decision_time)   # ISO-8601 timestamp
    print(response.engine_version)  # e.g. "aethis-core@0.36.0"
```

### Composed rulebook (requires API key)

To evaluate a multi-ruleset rulebook (e.g. `aethis/uk-fsm`'s `A AND (B OR C)`), use `decide_rulebook()` and supply an API key:

```python theme={null}
from aethis_sdk import Aethis

with Aethis(api_key="YOUR_KEY") as client:
    response = client.decide_rulebook(
        rulebook_id="aethis/uk-fsm",
        field_values={
            "child.age": 10, "child.year_group": "year_6",
            "child.school_type": "state_funded",
            "household.receives_universal_credit": True,
            "household.annual_net_earnings": 5000,
            # ... rest of household fields
        },
    )
    print(response.decision)      # eligible
    print(response.rulebook_id)   # rb_kzZ_td0tbKW_OLRB (resolved from slug)
```

Rulebook decide is always scope-gated — anonymous callers get HTTP 401, regardless of rulebook visibility.

`response` is a typed `DecideResponse` — see the [decision envelope reference](/concepts/decision-envelope) for the full shape. The four audit fields above are the determinism proof: same `inputs_hash` always produces the same outcome from the same `engine_version`, and the `decision_id` lets you pin a specific decision to a log line.

`Aethis` must be used as a context manager — the underlying `httpx` client is created in `__enter__` and closed in `__exit__`, so calling methods on a non-entered client raises `RuntimeError`.

## Quickstart — async

```python theme={null}
import asyncio
from aethis_sdk import AsyncAethis

async def main():
    async with AsyncAethis() as client:    # no key — public ruleset
        response = await client.decide(
            ruleset_id="aethis/uk-fsm/child-eligibility",
            field_values={"child.age": 10, "child.school_type": "state_funded"},
        )
        print(response.decision)

asyncio.run(main())
```

The async surface mirrors the sync one method-for-method. Use it inside FastAPI handlers, async workers, or any code path already on the asyncio event loop.

## With trace

```python theme={null}
response = client.decide(
    ruleset_id="aethis/spacecraft-crew-certification",
    field_values={"space.crew.species": "Vogon"},
    include_trace=True,
)
print(response.trace["failure_reasons"])
print(response.trace["group_statuses"])
```

## Discover fields

```python theme={null}
schema = client.get_schema("aethis/uk-fsm/child-eligibility")
for field in schema.fields:
    print(f"{field.field_id}  {field.field_type}  {field.description}")
```

## Stateful decision session

For interactive flows — chatbot, wizard, multi-step form — `SyncDecisionSession` accumulates answers locally and only hits `/decide` when the input set has actually changed. The session does not own the client; keep the `Aethis` context open for the session's lifetime.

```python theme={null}
from aethis_sdk import Aethis, SyncDecisionSession

with Aethis() as client:        # no key — public ruleset
    schema = client.get_schema("aethis/spacecraft-crew-certification")
    session = SyncDecisionSession(
        "aethis/spacecraft-crew-certification", client, schema,
    )
    session.answer("space.crew.species", "Human")

    while (nq := session.next_question()) is not None:
        answer = input(f"{nq.question} ")
        session.answer(nq.field_id, answer)

    print("Eligible:", session.is_eligible())
```

The async equivalent is `DecisionSession` — same surface with `await` on the I/O methods.

## FastAPI integration

A typical server-side wiring: one client per process, an async dependency, decision endpoints that return the envelope verbatim.

```python theme={null}
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException
from aethis_sdk import AsyncAethis, AethisAPIError, AethisUnavailable

@asynccontextmanager
async def lifespan(app: FastAPI):
    # `async with` is required — it opens the underlying httpx client.
    async with AsyncAethis(api_key=os.environ["AETHIS_API_KEY"]) as client:
        app.state.aethis = client
        yield

app = FastAPI(lifespan=lifespan)

def get_aethis() -> AsyncAethis:
    return app.state.aethis

@app.post("/eligibility/free-school-meals")
async def free_school_meals(
    payload: dict, client: AsyncAethis = Depends(get_aethis),
):
    try:
        decision = await client.decide(
            ruleset_id="aethis/uk-fsm/child-eligibility",
            field_values=payload,
        )
    except AethisAPIError as e:
        raise HTTPException(status_code=e.status_code or 502, detail=str(e))
    except AethisUnavailable:
        raise HTTPException(status_code=503, detail="decision_engine_unavailable")
    return decision.model_dump()
```

## Django integration

The simplest pattern is per-request: instantiate inside the view's `with` block. New connections each call (no pooling across requests), but no lifecycle wiring required.

```python theme={null}
# views.py
import json
import os
from django.http import JsonResponse
from aethis_sdk import Aethis

API_KEY = os.environ["AETHIS_API_KEY"]

def free_school_meals(request):
    payload = json.loads(request.body)
    with Aethis(api_key=API_KEY) as client:
        decision = client.decide(
            ruleset_id="aethis/uk-fsm/child-eligibility",
            field_values=payload,
        )
    return JsonResponse(decision.model_dump())
```

For high-traffic services that want connection pooling across requests, lift the `with` block into a Django `AppConfig.ready()` (call `client.__enter__()` at startup, register `atexit(client.close)` for shutdown) and stash the client in `apps.get_app_config(...)`. For async views, swap to `AsyncAethis` with `async with` and `await` the call.

## Configuration

| Argument    | Required | Default                 | Notes                                                                                                                                                                                                                      |
| ----------- | -------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `api_key`   | no       | `None`                  | Optional. Anonymous decides on public rulesets work without it; required for composed rulebook decides, private rulesets, and authoring. Provisioned via [aethis.ai/developer-access](https://aethis.ai/developer-access). |
| `base_url`  | no       | `https://api.aethis.ai` | HTTP only permitted for `localhost` / `127.0.0.1` or with a test transport.                                                                                                                                                |
| `timeout`   | no       | `5.0` (seconds)         | Per-request, applies to all HTTP methods.                                                                                                                                                                                  |
| `iam_token` | no       | —                       | Cloud Run service-to-service bearer token. Most users won't need this.                                                                                                                                                     |

## Errors

The SDK exposes a small exception hierarchy. Catch the base class for blanket handling, or the specific subclasses for tailored retry / fallback logic.

```python theme={null}
from aethis_sdk import (
    AethisError,         # base — catch this for "anything went wrong"
    AethisAPIError,      # 4xx / 5xx with structured body
    AethisUnavailable,   # network failure, DNS, connection refused
    AethisTimeout,       # timeout exceeded
)

try:
    decision = client.decide(ruleset_id=..., field_values=...)
except AethisTimeout:
    # log + degrade — engine missed its 5ms budget under back-pressure
    ...
except AethisUnavailable:
    # network / engine down — circuit-break and fall back
    ...
except AethisAPIError as e:
    # 4xx (your input) or 5xx (their bug)
    if e.status_code == 422:
        # validation: missing required field, wrong type, etc.
        ...
```

See the [error reference](/reference/errors) for response shapes and codes.

## Public API surface

| Import                                                                                                            | Purpose                                                                                                                                                                                                                              |
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Aethis`, `AsyncAethis`                                                                                           | HTTP clients for `/decide`, `/rulesets` (list), `/rulesets/{id}/schema`, `/me`, `/explain`, `/source`. `decide()` / `decide_rulebook()` accept `include_explanation` and `list_rulesets()` pages the public catalogue (both 0.7.0+). |
| `SyncDecisionSession`, `DecisionSession`                                                                          | Stateful adapters over the stateless `/decide` endpoint                                                                                                                                                                              |
| `DecideResponse`, `SchemaResponse`, `SchemaField`, `NextQuestion`, `FieldNote`, `SectionResult`, `RulesetSummary` | Pydantic response models                                                                                                                                                                                                             |
| `Decision`, `SectionStatus`, `SessionStatus`                                                                      | Enum-like result types                                                                                                                                                                                                               |
| `AethisError`, `AethisAPIError`, `AethisUnavailable`, `AethisTimeout`                                             | Exception hierarchy (`.detail` / `.body` carry the API's error payload)                                                                                                                                                              |

## Status

Pre-1.0. The decision surface (`/decide`, `/schema`, `/explain`, `/source`, `/me`) is stable. Authoring endpoints (projects, rulesets, publishing) are not yet exposed in the SDK — use the [CLI](/interfaces/cli) or [MCP server](/mcp-server/overview) for those.

## See also

* [Which interface?](/interfaces/which-to-use) — pick between SDK, CLI, MCP, REST.
* [Decision envelope](/concepts/decision-envelope) — every field on `DecideResponse`.
* [Error reference](/reference/errors) — codes and recovery patterns.
* [aethis-sdk on PyPI](https://pypi.org/project/aethis-sdk/) · [source on GitHub](https://github.com/Aethis-ai/aethis-sdk-python)

<Note>
  **Help improve this page**

  If something here is unclear or missing an example, use the feedback button at the bottom of the page.

  Found a bug? [Open a GitHub issue](https://github.com/Aethis-ai/feedback/issues). Evaluating Aethis for a regulated workflow? [Contact us directly](https://aethis.ai/developer-access).
</Note>
