Skip to main content

Modaic Python SDK

The official, HTTP-only Python client for the public Modaic API. It includes matching synchronous and asynchronous clients and never invokes Git or reads or writes repository files.

Install

pip install modaic

Set an API key from https://modaic.dev/settings/keys:

export MODAIC_API_KEY="mdc_..."

Quickstart

from modaic import Modaic

with Modaic() as modaic:
    result = modaic.decisions.create(
        state={
            "ticket": "I was charged twice for order 4832. Please refund one charge."
        },
        model="typesafe/jev-latest",
        questions={
            "needs_refund": {
                "type": "noul",
                "instructions": "Should this customer receive a refund?",
                "criteria": {
                    "true": "A duplicate or invalid charge should be refunded.",
                    "false": "The charge is valid or more information is required.",
                },
            },
            "priority": {
                "type": "choice",
                "instructions": "Choose the support priority.",
                "criteria": {
                    "low": "No financial or time-sensitive impact.",
                    "normal": "Routine customer issue.",
                    "high": "Financial impact or an urgent blocker.",
                },
            },
        },
        idempotency_key="ticket-4832-v1",
    )

print(result.answers["priority"])

The API URL defaults to https://modaic.dev/api/v1. Set MODAIC_API_URL to override it, or pass base_url to the client. The client option takes precedence over the environment variable. This applies to both Modaic and AsyncModaic.

For a local server, pass the versioned API URL explicitly:

modaic = Modaic(api_key="mdc_...", base_url="http://localhost:3001/v1")

Question objects and typed responses

Use Noul, Choice, and Score to define questions. Dictionary questions still work, including alongside question objects. Both forms are accepted by decisions.create, models.create, and models.update.

Choice requires at least one option; Score requires at least one rubric level. Empty choice criteria sent as a dictionary are rejected by the API with 422 and code validation_error.

Define a Pydantic response model by extending DecisionResponse with answer fields matching your question names:

from modaic import DecisionResponse, Modaic, Noul, NoulAnswer


class BillingResponse(DecisionResponse):
    billing: NoulAnswer


with Modaic() as modaic:
    result = modaic.decisions.create(
        model="typesafe/jev-latest",
        state="I was charged twice.",
        questions={"billing": Noul(instructions="Is this about billing?")},
        response_model=BillingResponse,
    )
    assert result.billing == result.nouls["billing"]
    print(result.billing.noul)
    print(result.request_id)

Use ChoiceAnswer and ScoreAnswer for choice and score fields. answers retains every answer, with nouls, choices, and scores providing typed views. Usage and capture metadata remain available. Required answer fields are validated; missing answers or mismatched types raise ModaicConnectionError. Use Pydantic Field(alias="question-name") for names that are not Python identifiers. Keep response metadata names, such as model and usage, reserved.

response_model accepts any Pydantic model, not only DecisionResponse subclasses. Extending DecisionResponse is the shorter path, since it supplies model, answers, usage, the typed views, and request_id, and lifts your answer fields to the top level. To control the whole schema instead, declare a plain BaseModel and spell out the parts you want:

from pydantic import BaseModel

from modaic import NoulAnswer


class BillingAnswers(BaseModel):
    billing: NoulAnswer


class BillingEnvelope(BaseModel):
    answers: BillingAnswers

Pydantic drops response fields the model does not declare, so this validates only answers.billing and reads as result.answers.billing.noul. Nothing is grafted on: request_id and the typed views exist only on DecisionResponse subclasses. BaseModel itself is rejected, because Pydantic cannot validate into it.

response_model also works with AsyncModaic and model.decisions.create. It controls local response parsing only and is not sent to the API. The request_id comes from the HTTP response header and is excluded from model_dump() and model_dump_json().

Async

The async client has the same resources and methods:

import asyncio
from modaic import AsyncModaic


async def main() -> None:
    async with AsyncModaic() as modaic:
        models = await modaic.models.list()
        print([model.name for model in models.models])


asyncio.run(main())

Model-bound resources

Models returned by models.create, models.get, and models.update can run decisions directly:

with Modaic() as modaic:
    model = modaic.models.get(workspace="acme", model="support-priority")
    result = model.decisions.create(
        state={"ticket": "Please refund my duplicate charge."},
    )
    examples = model.examples.list(page_size=10)
    batches = model.jobs.batch_decisions.list()
    alignments = model.jobs.alignments.list()

models.get includes configuration with the model and questions saved on the default branch (None until the model has a manifest), so you can diff before updating. models.update with questions that already match is a no-op: it returns unchanged=True, a commit whose commit_sha equals previous_sha, and the stored configuration. Nothing is committed and the checkpoint is not reset.

Alignment writes its optimized instructions into the same questions, so a setup script that re-pushes the schema as authored would revert an aligned model. Once a model has a checkpoint, models.update with questions that differ from the stored ones fails with 409 alignment_would_be_discarded. Omit questions to keep the aligned instructions, or pass discard_alignment=True to replace them and reset the checkpoint to 0.

modaic.__version__ is the installed SDK version, for logging alongside results. models.get also returns commit, the head of the default branch. Pass commit.commit_sha back as expected_head_sha on models.update and a stale client gets 409 expected_head_mismatch instead of overwriting newer commits.

Branches, tags, and rollback

Every model is a Git repository, and models exposes its history:

model = modaic.models.get(workspace="acme", model="support-priority")

# Name the aligned commit so production can pin to it.
modaic.models.create_tag(model.id, name="v1", commit_sha=model.commit.commit_sha)
modaic.decisions.create(model="acme/support-priority", revision="v1", state={...})

# Undo a bad update: move main back to the aligned commit.
commits = modaic.models.list_commits(model.id, branch="main")
modaic.models.rollback(
    model.id,
    branch="main",
    target_commit_sha=commits.commits[1].sha,
    expected_head_sha=commits.commits[0].sha,
)

Rollback commits the target's files back onto the branch, so model.json returns with its checkpoint and metrics intact. list_branches, create_branch(name=, source_ref=), delete_branch, list_tags, and delete_tag round out the surface.

With AsyncModaic, await both calls. The bound method accepts every decision option except model and uses the same client; keep that client open while running decisions. Pass revision to pin a version. model_dump() and model_dump_json() contain only response data. The top-level modaic.decisions.create remains available.

model.examples exposes ingest, list, get, annotate, and list_decisions without a model ID argument. model.jobs.alignments and model.jobs.batch_decisions expose model-bound create and list. Use the top-level job resources to retrieve, wait for, or cancel a job by its ID.

Job progress

Pass progress=True to either job resource's wait() method for a tqdm display:

finished = modaic.batch_decisions.wait(job.id, progress=True)
finished = modaic.alignments.wait(alignment.id, progress=True)

With AsyncModaic, use await with the same option. Progress is off by default. Batch jobs show processed examples and failures; alignment shows its stage and metric-call budget usage, not an overall completion percentage. Updates use the existing polling interval. Timing out stops waiting without cancelling the job.

Resources

Resource Methods
decisions create
models list, create, get, update, delete
examples ingest, list, get, annotate, list_decisions
batch_decisions create, list, get, cancel, wait
alignments create, list, get, logs, cancel, wait

Responses are Pydantic models. Python attributes use snake_case even when the wire format uses camelCase.

Errors

Non-2xx responses raise ModaicAPIError, which exposes status_code, code, request_id, details, and the decoded response body. Network failures raise ModaicConnectionError; request and polling deadlines raise ModaicTimeoutError.

See the complete API documentation at https://docs.modaic.dev.

Release files for modaic 0.51.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 modaic 0.51.0
File Size Uploaded
modaic-0.51.0.tar.gz 88.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for modaic 0.51.0
File Interpreter ABI Platform
modaic-0.51.0-py3-none-any.whl Python 3 none any Details

Total release size: 107.7 kB

Release files / modaic-0.51.0.tar.gz

Download URL modaic-0.51.0.tar.gz
Size 88.7 kB
Tags Source
SHA-256 checksum
How to use checksums
40554b67241d2142399f5e889f94c42bd35e8343c0703eba54e004bbb6af16b4
BLAKE2b-256 checksum
How to use checksums
1797c9017854394cf9c97788d95f63835989a4db3ec61b545678687778f7e9d6
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 25, 2026.

Transparency log

Release files / modaic-0.51.0-py3-none-any.whl

Download URL modaic-0.51.0-py3-none-any.whl
Size 19.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e42f52b5aa1e6bd913b9a6effa3022b9c3fd2c3775ca2f91ed1f09146388c7c7
BLAKE2b-256 checksum
How to use checksums
ba0864e60cd730b5bb154b17cbf504e889e205721f8602522cf3b7f9c8b3c4a4
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 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.51.0 This release

2 release files

0.50.0

2 release files

0.49.0

2 release files

0.48.0

1 release file

0.47.0

1 release file

0.46.0

2 release files

0.45.5

2 release files

0.45.2

2 release files

0.45.1

2 release files

0.45.0

2 release files

0.44.3

2 release files

0.44.2

2 release files

0.43.0

2 release files

0.37.0

2 release files

0.36.3

2 release files

0.36.2

2 release files

0.36.1

2 release files

0.36.0

2 release files

0.35.0

2 release files

0.34.2

2 release files

0.34.1

2 release files

0.34.0

2 release files

0.33.0

2 release files

0.28.0

2 release files

0.27.0

2 release files

0.26.0

2 release files

0.25.2

2 release files

0.25.1

2 release files

0.25.0

2 release files

0.24.2

2 release files

0.24.1

2 release files

0.24.0

2 release files

0.21.1

2 release files

0.21.0

2 release files

0.20.0

2 release files

0.19.7

2 release files

0.19.6

2 release files

0.19.5

2 release files

0.19.2

2 release files

0.18.0

2 release files

0.17.0

2 release files

0.16.0

2 release files

0.15.0

2 release files

0.14.0

2 release files

0.13.1

2 release files

0.13.0

2 release files

0.12.7

2 release files

0.12.6

2 release files

0.12.5

2 release files

0.12.4

2 release files

0.12.3

2 release files

0.12.2

2 release files

0.12.1

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.4

2 release files

0.10.3

2 release files

0.10.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.3

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.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