Skip to main content

Aethis SDK for Python

PyPI Python License: MIT

Official Python SDK for the Aethis developer API — eligibility decisions, ruleset schemas, and stateful decision sessions.

Documentation: docs.aethis.ai · OpenAPI spec · agents via MCP: claude mcp add aethis -- npx -y aethis-mcp

Two access boundaries

Know which one you are on before you write any code — the SDK will tell you the same thing in the error message if you cross it.

Evaluation Authoring
Key None. Aethis() works anonymously. Invite-only API key.
What decide on a public ruleset, list_rulesets, get_schema, get_graph, get_explanation / explain, get_source. Publishing rulesets, project endpoints, rulebook decide + rulebook schema, whoami.
If you get it wrong — HTTP 401/403 → AethisAuthError / AethisPermissionError with .boundary == "authoring" and the access-request link in the message.

Authoring is in private beta. The CLI (aethis-cli) is the supported authoring path during the beta — see docs.aethis.ai/recipes/author-a-rule for the test-driven workflow (rulesets cannot publish with a failing test). Request access at aethis.ai/developer-access.

from aethis_sdk import Aethis, AethisAuthError

with Aethis() as client:                       # evaluation — no key
    client.decide("aethis/construction-all-risks", {...})
    try:
        client.whoami()                        # authoring — needs an invite
    except AethisAuthError as err:
        print(err.boundary)                    # "authoring"

Install

uv add aethis-sdk

# Or, for a standalone venv:
uv pip install aethis-sdk

Python 3.11+. Requires httpx and pydantic. SDK v0.5.0+ ships the composed-rulebook decision surface (decide_rulebook) and rulebook_id on DecideResponse.

Quickstart

Examples below target aethis/uk-fsm/child-eligibility — a live public ruleset (UK Free School Meals, child-eligibility section). Browse all live rulesets with curl https://api.aethis.ai/api/v1/public/rulesets.

Single-ruleset decision endpoints are anonymous on public rulesets — Aethis() works with no key. Composed-rulebook decisions (decide_rulebook) and authoring endpoints require an API key. Pass api_key="ak_live_..." to Aethis(...) for those paths.

One-shot decision (sync)

from aethis_sdk import Aethis

with Aethis() as client:
    response = client.decide(
        ruleset_id="aethis/uk-fsm/child-eligibility",
        field_values={
            "child.age": 10,
            "child.school_type": "state_funded",
        },
    )
    if response.has_blocking_errors:
        # Some inputs could not be used. The decision is undetermined and
        # there is no next question — do not read that as "finished".
        raise SystemExit(response.blocking_errors)

    print(response.decision)        # "eligible" | "not_eligible" | "undetermined"
    print(response.is_terminal)     # True only for a clean, unblocked verdict
    print(response.inputs_hash)     # canonical SHA-256 fingerprint of the input set
    print(response.decision_id)     # per-call audit identifier
    print(response.engine_version)  # e.g. "aethis-core@0.48.0"

Blocking input errors

field_errors is the structured channel for inputs the engine could not use — an unknown field key, a value that fails the field's type. Every entry is blocking, so the decision is always undetermined, and the engine stops proposing a next question. That last part is the trap: a blocked response and a finished one look identical if you only test next_question is None.

response = client.decide(RULESET_ID, answers)
if response.has_blocking_errors:
    for field, message in response.blocking_errors.items():
        print(f"{field}: {message}")
elif response.is_terminal:
    print("verdict:", response.decision)
else:
    print("next:", response.next_question.question)

# Or make it an exception instead of a branch:
response.raise_for_blocking_errors()   # raises AethisFieldErrors when blocked

The SDK also refuses to parse a response that reports eligible/not_eligible beside blocking errors — that contradicts the API contract, so it raises AethisContractViolation rather than handing you a verdict computed from inputs you did not knowingly send.

Replay identity

Retain these with your own inputs and a decision can be replayed and audited later:

identity = response.require_replay_identity()
identity.ruleset_id       # immutable id, never the slug you asked with
identity.ruleset_version  # published version label, e.g. "v99"
identity.content_digest   # sha256 of the exact rule content evaluated
identity.engine_version   # the engine build that decided
identity.decision_id, identity.inputs_hash

require_replay_identity() raises AethisReplayIdentityError — naming the missing parts — when the engine could not resolve one (a rulebook decision, until composed identity lands). It never invents a placeholder: ruleset_version is None in that case, never the string "unknown". Use response.replay_identity for the non-raising form.

One-shot decision (async)

import asyncio
from aethis_sdk import AsyncAethis

async def main():
    async with AsyncAethis() as client:
        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())

Composed rulebook (requires API key — v0.5.0+)

A Rulebook composes multiple rulesets via an outcome_logic expression — e.g. UK FSM's child_eligibility AND (household_criteria OR universal_infant). Hit the whole-form decision with decide_rulebook:

from aethis_sdk import Aethis

with Aethis(api_key="ak_live_...") 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,
            "household.receives_income_support": False,
            "household.receives_income_based_jsa": False,
            "household.receives_income_related_esa": False,
            "household.receives_child_tax_credit_only": False,
            "household.receives_nass_support": False,
            "child.is_looked_after": False,
            "child.is_care_leaver": False,
        },
    )
    print(response.decision)      # "eligible"
    print(response.rulebook_id)   # "rb_kzZ_td0tbKW_OLRB" (slug resolved)

Rulebook decide is always scope-gated by the engine — anonymous callers get HTTP 401, regardless of rulebook visibility. The decide_rulebook method and the rulebook_id field on DecideResponse ship in SDK v0.5.0. AsyncAethis.decide_rulebook(...) is the async equivalent.

Stateful decision session

Accumulate answers locally and query the API only when needed. Cached until an answer changes. The session does not manage the client — the caller keeps the Aethis context open for the session's lifetime.

from aethis_sdk import Aethis, SyncDecisionSession

RULESET_ID = "aethis/uk-fsm/child-eligibility"

with Aethis() as client:
    schema = client.get_schema(RULESET_ID)
    session = SyncDecisionSession(RULESET_ID, client, schema)
    session.answer("child.school_type", "state_funded")

    while True:
        status = session.status()
        if status.blocked:                     # inputs the engine could not use
            raise SystemExit(status.field_errors)
        if status.is_complete:                 # a real verdict
            break
        if status.next_question is None:       # undetermined on these answers
            break
        answer = input(f"{status.next_question.question} ")
        session.answer(status.next_question.field_id, answer)

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

Loop on status(), not on next_question(). next_question() returns None in three different situations — the decision is final, the ruleset cannot settle on these answers, or blocking input errors are suppressing further questions — and while session.next_question() is not None: treats all three as success. status.blocked and status.is_complete tell them apart:

blocked is_complete next_question
Still asking False False a field
Finished with a verdict False True None
Undetermined on these answers False False None
Blocked by input errors True False None

status.field_errors is always a dict (empty when clean) and status.replay_identity carries the resolved identity of the content that decided. status.raise_if_blocked() turns the blocked row into an exception if you would rather not branch.

Note: input() returns a string. For non-string fields (int / bool / enum) coerce the answer before calling session.answer() — the API expects the typed value.

Source provenance

Every published citation is fetched, digested, quoted verbatim and licence-checked at publish time — an unresolvable one blocks the publish. Both explanation surfaces return the same typed SourceReference:

explanation = client.get_explanation(RULESET_ID)   # flat `criteria`
for criterion_id, references in explanation.source_references().items():
    for reference in references:
        print(criterion_id, reference.authority, reference.locator)
        print(reference.quote.exact)   # verbatim, never a summary
        print(reference.deep_link)     # links straight to the quoted passage

decision = client.decide(RULESET_ID, answers, include_explanation=True)
decision.source_references()           # same DTO, from `explanation.groups[].criteria[]`

The two envelopes differ — get_explanation() returns a flat criteria list, decide(include_explanation=True) nests criteria inside groups — so the SDK models them separately (ExplainResponse vs DecisionExplanation). Provenance records what a rule cites and that the citation was verified to exist; it is not a claim that the rule's reading of it is correct.

Authoring (requires a key)

with Aethis(api_key="ak_live_...") as client:
    status = client.get_generation_status("proj_example")
    if status.job is not None:
        print(status.job.status, status.job.seconds_since_progress)

    # These top-level fields are authoritative. Retry only when readiness is
    # "ready"; an old heartbeat by itself does not prove worker death.
    print(status.telemetry_availability, status.worker_lifecycle)
    print(status.retry_readiness)

    # Explicit only: this is a destructive, cooperative request. It releases
    # the project but cannot interrupt an in-flight provider request.
    # The SDK rechecks generation_contract_version and this exact active job
    # before sending the destructive request; old engines are refused.
    cancelled = client.cancel_generation("proj_example", status.job.job_id)
    print(cancelled.outcome, cancelled.detail)  # cancelled | already_cancelled

Use AsyncAethis for the same HTTP-client methods with await. DecisionSession remains the stateful async helper over /decide (decide, is_eligible, next_question, status).

What's included

Import Purpose
Aethis, AsyncAethis HTTP clients for /decide, /rulesets, /rulesets/{id}/schema, /rulesets/{id}/graph, /rulebooks/{id}/schema, /me, /rulesets/{id}/explain, /rulesets/{id}/source
decide, decide_rulebook Single-ruleset and composed-rulebook decisions; both take include_trace, include_explanation, and include_graph_overlay
get_graph Fetch a ruleset's field → criterion → group → outcome dependency graph, plus a rendered Mermaid diagram (GraphResponse)
get_rulebook_schema Fetch a rulebook's combined field schema, plus its robot_hints (conversational-agent guidance) and engine_version (RulebookSchemaResponse)
explain_failure Diagnose a mismatched /decide — returns the failing criterion and a fix hint
get_generation_status Read the latest typed lifecycle telemetry for an authoring project; it never changes the job
cancel_generation Explicitly request cooperative cancellation of an observed generation job by project and job id (GenerationCancellationResponse)
list_rulesets Page the public ruleset catalogue (RulesetSummary items)
get_explanation Typed ruleset explanation (ExplainResponse) with resolved identity and SourceReference citations
SyncDecisionSession, DecisionSession Stateful adapters over the stateless /decide endpoint
DecideResponse, SchemaResponse, RulebookSchemaResponse, SchemaField, GraphResponse, RulesetGraph, NextQuestion, FieldNote, SectionResult, RulesetSummary Pydantic response models
ExplainResponse, ExplainCriterion, DecisionExplanation, ExplanationGroup, ExplanationCriterion The two explanation shapes, modelled separately
SourceReference, SourceQuote Publish-validated citation contract shared by both explanation surfaces
ReplayIdentity, ContentIdentity Resolved immutable identity of the content a response came from
AethisError, AethisAPIError, AethisUnavailable, AethisTimeout Exception hierarchy (.detail / .body carry the API's error payload; .boundary names the access boundary on 401/403)
AethisAuthError (401), AethisPermissionError (403), AethisRateLimitError (429) Typed AethisAPIError subclasses carrying .reason_code, .missing_permissions, .hint from the API's structured error envelope
AethisFieldErrors, AethisContractViolation, AethisReplayIdentityError Blocking input errors; a response that contradicts the API contract; an unresolved replay identity

Configuration

  • api_key — not needed for evaluation endpoints (/decide on a public ruleset, /rulesets, /schema, /explain, /source) during the developer beta. Required, and invite-only, for authoring endpoints (publishing rulesets, project and rulebook endpoints). Request access at aethis.ai/developer-access.
  • base_url — defaults to https://api.aethis.ai. HTTP is only permitted for localhost / 127.0.0.1 or when passing a test transport.
  • timeout — per-request, seconds. Defaults to 5.
  • iam_token — optional bearer token for Cloud Run service-to-service auth.

Status

Pre-1.0. The decision surface (/decide, /schema) is stable. The SDK also exposes the narrow authoring recovery surface (get_generation_status and explicit cancel_generation); it does not submit, resume, or automatically cancel generation jobs. Use the Aethis CLI for the full authoring workflow.

Verifying a release

Each published version records the exact bytes it shipped and the commit they were built from, so you can check that what PyPI serves is what was released:

uv run python scripts/release_integrity.py --expect integrity.json --verify-registry

integrity.json (attached to the release run) maps (package, version) to each distribution's sha256 and to the source commit. The same release runs a hermetic install check — temporary HOME, no keys in the environment, empty cache, registry-only download — across the supported Python / OS / architecture matrix, plus a poisoned-artefact control that must fail.

Links

Metadata

Release files for aethis-sdk 0.13.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aethis-sdk 0.13.0
File Size Uploaded
aethis_sdk-0.13.0.tar.gz 66.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aethis-sdk 0.13.0
File Interpreter ABI Platform
aethis_sdk-0.13.0-py3-none-any.whl Python 3 none any Details

Total release size: 100.4 kB

Release files / aethis_sdk-0.13.0.tar.gz

Download URL aethis_sdk-0.13.0.tar.gz
Size 66.7 kB
Tags Source
SHA-256 checksum
How to use checksums
caa5d96733e40c432aaf413e2f70f8b6c6601d5306bae996a87dc525e259ba64
BLAKE2b-256 checksum
How to use checksums
71891c555f2f7ca77f7a98b269443efd9f4aff1bd47c6344f6c3fae6fce03b83
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

Release files / aethis_sdk-0.13.0-py3-none-any.whl

Download URL aethis_sdk-0.13.0-py3-none-any.whl
Size 33.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6261e0e70b93338abbbe846008ae370c5e571e1fc47bcab89ae561fc0a5a3652
BLAKE2b-256 checksum
How to use checksums
63a9b757e632a3a0c8b6498534515863871a6e3df0cc88343f892237649401e4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.13.0 This release

2 release files

0.10.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page