Metadata-Version: 2.5
Name: brussle
Version: 1.6.0
Summary: Python SDK for the Brussle API
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.27
Requires-Dist: typing-extensions>=4.7
Description-Content-Type: text/markdown

# brussle (Python)

Brussle trains AI decisions on the outcomes that you record, and judges each one again when a document it read changes. A decision, a judgment, is an AI model's typed answer about a document, and it can read other documents. For example, *Which open invoice does this payment settle?* reads the payment beside its customer's open invoices. When an invoice changes, Brussle judges the payments that read it again. Each answer says whether it is current, and records the judgment version and engine version behind it. From the outcomes that you record, Brussle fits a correction to each answer's number, and uses it only when it does better than the engine on held-out outcomes. If an answer reads only its own document, a column in your own database may be enough.

This package is the Python client for the Brussle API.

```python
import os
from brussle import Client

db = Client(api_key=os.environ["BRUSSLE_API_KEY"])
ns = db.namespace("acme/prod/tenant_123")

# A judgment on unmatched payments that reads the open invoices in the same block.
ns.judgments.create(
    from_starter="payments.settles",
    paths={
        "kind": "attributes.kind",
        "payment_kind": "payment",
        "invoice_kind": "invoice",
        "status": "attributes.status",
        "unmatched": "unmatched",
        "open": "open",
        "block": "attributes.block",
        "payment_amount": "state.amount",
        "reference": "state.reference",
        "invoice_number": "state.number",
        "invoice_amount": "state.amount",
    },
    engine={"name": "jev", "version": "current"},
    freshness={"policy": "on_change"},
    confirm=True,
)

# An open invoice, and a payment in the same block.
ns.write(upsert=[
    {"id": "inv_1002", "attributes": {"kind": "invoice", "status": "open", "block": "northwind-gbp"}, "state": {"number": "INV-1002", "amount": 1200}},
    {"id": "pay_1", "attributes": {"kind": "payment", "status": "unmatched", "block": "northwind-gbp"}, "state": {"amount": 1200, "reference": "Northwind INV-1002"}},
])

# A change to the block's open invoices judges its payments again. Query the payments that wait for a person,
# with a current answer only.
page = ns.query(
    filters=("answers.settles.thresholds.unmatched", "Eq", True),
    answers="fresh_only",
    include={"answers": ["settles"]},
)
```

The [quickstart](https://docs.brussle.com/quickstart) runs this example step by step. [Relations](https://docs.brussle.com/concepts/relations) lists what else a judgment can read.

The example uses Jev at `current`. A judgment can name any engine version that `db.engines.list()` (`GET /engines`) lists as active: today `jev` `current` and `gpt-6-luna` `current`. Both are unpinned, so each answer records the epoch of the model behind it. They differ in two ways that you choose by: only `gpt-6-luna` reads images, and each `gpt-6-luna` judgment counts 2.5 times on the bill. See [engines](https://docs.brussle.com/engines/index). The API also names `pplx-decider`, which has no active version yet, so a judgment that names it is refused with `engine_version_unavailable`.

A document keeps its images in `images`, beside `attributes` and `state`. Each name holds one image or a list of images. A write sends a PNG, JPEG or WebP as a base64 data URL:

```python
import base64
from pathlib import Path

photo = "data:image/png;base64," + base64.b64encode(Path("photo.png").read_bytes()).decode()
ns.write(upsert=[{"id": "listing_7", "attributes": {"kind": "listing"}, "state": {"title": "Oak desk"}, "images": {"photo": photo}}])
```

The document then holds a reference in its place: `{"digest": "sha256:...", "type": "image/png", "width": ..., "height": ...}`. To keep the image when you write the document again, send the reference you read. `ns.image` reads the stored bytes, which are exactly what an engine that reads images reads:

```python
reference = ns.get("listing_7")["images"]["photo"]
data, type = ns.image(reference["digest"])
```

A judgment reads an image when its context recipe names it as `images.<name>`, on an engine whose `limits` in `db.engines.list()` have `images`. Any other engine gets the image's digest in its place, and the create's `warnings` say so. This judgment reads the photo on gpt-6-luna:

```python
ns.judgments.create(
    name="photo_matches",
    type="bool",
    applies_to={"attributes.kind": "listing"},
    question="Does the photo show the item that the title describes?",
    context={"fields": ["state.title", "images.photo"]},
    engine={"name": "gpt-6-luna", "version": "current"},
    freshness={"policy": "on_change"},
)
```

`ns.erase_image(digest)` erases an image: its bytes are deleted, and documents and evaluations keep its reference, marked `"erased": True`. It returns `{"digest", "erased_at"}`, and erasing again returns the same. Reading the image then raises `image_erased` (410), and a write that sends its bytes again is refused with `image_erased`.

See [Images](https://docs.brussle.com/guides/images).

To load many documents, `import_documents` takes any iterable and writes it in batches, 4 at a time on a thread pool, with retries:

```python
summary = ns.import_documents(records(), concurrency=4)
print(summary.documents, summary.failures)
```

How the client works:

- Methods mirror the API's routes.
- Every route that changes something takes `idempotency_key=`. A call that would not be safe to repeat without one, such as a create, gets a key the SDK generates when you pass none, the same on each retry of that call.
- Calls are retried after network errors, timeouts, `502`, `503` and `504`, and after a `429` or `503` with a `Retry-After` of at most 60 s, 2 times by default (`max_retries`). A `500` is retried only for reads, writes and the other calls that are safe to repeat without a key. A call that would create a namespace past the organization's 1,000 a minute gets a `429` with `Retry-After` until the next minute, so it is retried; raise `max_retries` to create many namespaces at once. A `413 too_large`, such as a write past a namespace's size or attribute-name limit, is never retried.
- A `429` past a rate limit says which in `details.rate_limit`: `per_key`, or `per_org` for all your organization's API keys together. The per-organization limit counts every process and key, so a retry can meet another `429` because of your other clients. See [limits](https://docs.brussle.com/limits#requests).
- `ns.judgments.create`, `update` and `backfill` can take more than 20 s. The API then answers with a job, and the call waits for it: it checks the job every 1 to 10 s, or after the API's `Retry-After`, for up to `wait_timeout` seconds (10 minutes by default). It then returns or raises what the call would have. After the timeout it raises `JobWaitTimeoutError`, whose `job["id"]` you can pass to `db.jobs.wait`. With `wait=False` the call returns `{"accepted": False, "result": ...}` or `{"accepted": True, "job": ...}`.
- Requests and responses are plain JSON, typed in `brussle.types`. One exception: `ns.outcomes.append` takes the list of outcomes, not the whole request body.
- Filters and `rank_by` are tuples.
- Errors raise `ApiError` with the API's `code` and `details`. The `code` is `unknown` for an error response that is not the API's JSON, for example a proxy's error page, and for a success whose body is not JSON (for `ns.image`, not a PNG, JPEG or WebP).
- A call that your organization's plan does not include raises `plan_required` (402). `details["required_plan"]` names the plan that includes it.

See [the SDK reference](https://docs.brussle.com/sdks).
