# Python — lbb

Source: https://docs.littlebigbrain.com/sdks/python/

`littlebigbrain` is the Python client for little big brain. Use it in scripts,
notebooks, data pipelines and Python services. Each method calls the
[HTTP API](https://docs.littlebigbrain.com/sdks/http-api/) and returns JSON as a `dict` or a typed model.

This page uses the example of the [quickstart](https://docs.littlebigbrain.com/start/quickstart/): support
tickets, bugs and releases. The sections from **Create a client** to **Pin a
commit** form one program. Add each block to the end of one file and run it
with `python3`.

The reference sections after them cover the rest of the client. They include
the async client and typed models.

The package needs Python 3.10 or later and depends on `httpx` and `pydantic`.
Source and issues:
[github.com/littlebigbrains/lbb-python](https://github.com/littlebigbrains/lbb-python)
(Apache-2.0).

## Install

```sh
pip install littlebigbrain
```

The package imports as `lbb`. This page describes version 0.32.0.

## Create a client

```python
import json
import os
import time

from lbb import LbbClient, LbbError

lbb = LbbClient(os.environ["LBB_URL"], api_key=os.environ["LBB_API_KEY"], graph="support")
```

The first argument is the complete endpoint from the environment's **Connect** view
in the console. `api_key` is an environment API key. `graph` names the graph that the
client reads and writes. Without it, the server uses `main`.

One client serves one graph. `lbb.graph("name")` returns `facts`, `retract`,
`delete`, `publication_status` and `wait_for_published` for another graph. For
the other namespaces, create a second client with `graph="name"`.

The client holds an HTTP connection pool. Call `lbb.close()` when you are
done. Or use the client as a context manager: `with LbbClient(...) as lbb:`.

> [!CAUTION]
Keep API keys on your server or your machine. Never ship a key to a browser or
a mobile app. See [Authentication](https://docs.littlebigbrain.com/start/authentication/).

| Argument | Default | Meaning |
| --- | --- | --- |
| `base_url` | `http://127.0.0.1:7400` | The environment endpoint. The default is a local server. |
| `api_key` | none | An environment API key (`lbb_sk_test_…` or `lbb_sk_live_…`) or a single-mode token. It is sent as a bearer token. |
| `graph` | the server's `main` | The graph, sent as `?graph=`. |
| `environment` | none | Environment slug for an account-scoped session token. Uses the compatible `stack` wire parameter. |
| `integrations_url` | `https://api.littlebigbrain.com` | The integrations API that `integrations` calls with the same key. See [Integrations for your customers](https://docs.littlebigbrain.com/guides/integrations-for-your-customers/). |
| `default_consistency` | the server's `eventual` | The read consistency when a call names none. See [Read your own writes](#read-your-own-writes). |
| `timeout` | `120.0` | The timeout of one attempt, in seconds. |
| `retry_budget_ms` | `60000` | The total time that retries of one call may take. |
| `max_retries` | `6` | The most retries of one call. |
| `retry_delay` | `0.1` | The base delay of the backoff, in seconds. |
| `on_retry` | none | A callback that receives a `RetryEvent` for each retry. |
| `event_hooks` | none | `httpx` event hooks, for logs and metrics. |
| `transport` | none | An `httpx` transport, for tests or proxies. |
| `api_version` | `2026-10-21` | The value of the `lbb-version` header. An earlier date keeps the list shapes of that date; see [Lists](https://docs.littlebigbrain.com/sdks/http-api/#lists). |

## Declare types and links

A graph accepts the record types, links and fields that you declare. A write
that does not fit is refused. `ontology.define` declares types and links and
creates the graph. `ontology.evolve` adds fields.

```python
lbb.ontology.define({
    "format": "spec",
    "merge_default": False,
    "source": json.dumps({
        "entity_types": ["Ticket", "Bug", "Release"],
        "relation_types": [
            {"name": "REPORTED_AS", "source": "Ticket", "target": "Bug"},
            {"name": "FIXED_IN", "source": "Bug", "target": "Release"},
        ],
    }),
})
lbb.ontology.evolve({
    "ops": [
        {"op": "add_property", "name": "description", "value_type": "text"},
        {"op": "add_property", "name": "priority", "value_type": "keyword"},
    ],
})
```

Both calls are safe to repeat. `define` returns `changed=False` for a document
the graph already has. It applies an additive change, such as a new type or a
wider link.

`define` refuses a change that narrows or removes what the graph declares. The
error codes are `ontology_restrictive_change`,
`ontology_identity_breaking_change` and `ontology_unsupported_change`. Nothing
is written then.

`merge_default: False` leaves out the built-in default types. `source` also
accepts JSON-LD, Turtle, RDF/XML and CSV. `evolve(body, dry_run=True)`
previews a change. [Build with JSON records](https://docs.littlebigbrain.com/guides/without-rdf/) covers
parent types, inverse links and allowed values.

## Write records

A write is a **commit**. `facts.create` takes links between records
(`triplets`) and field values (`entity_properties`). A record is its `type`
plus a `key`, and it has a `name`. A link can carry `evidence`, the reason it
was recorded.

```python
login = {"type": "Ticket", "key": "4821", "name": "login fails after 3.1"}
invoice = {"type": "Ticket", "key": "4822", "name": "invoice PDF is blank"}
csv_export = {"type": "Ticket", "key": "4823", "name": "CSV export times out"}
auth = {"type": "Bug", "key": "AUTH-112", "name": "session token expiry"}
pdf = {"type": "Bug", "key": "BILL-7", "name": "PDF fonts missing"}
slow_export = {"type": "Bug", "key": "DATA-58", "name": "export runs on one thread"}
r32 = {"type": "Release", "key": "3.2", "name": "Release 3.2"}
r311 = {"type": "Release", "key": "3.1.1", "name": "Release 3.1.1"}

written = lbb.graph("support").facts.create({
    "triplets": [
        {"source": login, "relation": "REPORTED_AS", "target": auth, "evidence": "Support triage, 2 Sep"},
        {"source": invoice, "relation": "REPORTED_AS", "target": pdf, "evidence": "Support triage, 3 Sep"},
        {"source": csv_export, "relation": "REPORTED_AS", "target": slow_export, "evidence": "Support triage, 4 Sep"},
        {"source": auth, "relation": "FIXED_IN", "target": r32, "evidence": "Release notes 3.2"},
        {"source": pdf, "relation": "FIXED_IN", "target": r311, "evidence": "Release notes 3.1.1"},
    ],
    "entity_properties": [
        {**login, "properties": {"priority": "high", "description": "Since 3.1, people are sent back to the login screen right after they type their password."}},
        {**invoice, "properties": {"priority": "low", "description": "Downloaded invoices open as empty pages."}},
        {**csv_export, "properties": {"priority": "low", "description": "Exports with more than 10,000 rows stop after 30 seconds."}},
    ],
}, idempotency_key="sdk-support-1")
lbb.wait_for_published(written["commit_seq"])
print("commit", written["commit_seq"])
```

The response reports `commit_seq`, the number of the commit.
`wait_for_published` waits until the latest published data covers that
commit. Search and default reads use published data. This usually takes a
second or two.

Each write carries an idempotency key. The client creates one per call, and
its own retries of that call reuse it. Pass your own `idempotency_key` so that
a rerun of your code writes nothing new.

A second call with the same key returns the first result with
`replayed: True`. This holds even when the payload changed, so use a new key
for new data.

Field writes merge: a commit that sends one field keeps the other fields. An
unknown type, link or field fails the whole commit. So does a value of the
wrong type. A commit body is at most 1 MiB. For more, use
[NDJSON import](#import-ndjson).

## Turn on search

Declare an **embedding** on a type and name the fields to embed. The platform
builds one text per record, embeds it and keeps the vectors current. The first
build runs in the background.

```python
TICKET = "https://littlebigbrain.com/class/ticket"

lbb.embeddings.declare(TICKET, from_=["label", "description"])
while not lbb.embeddings.get("ticket").get("serving"):
    time.sleep(2)
```

`TICKET` is the global name of the type: its name in lowercase after
`https://littlebigbrain.com/class/`. `label` is the record's `name`. The
embedding's name defaults to the last part of the class, here `ticket`.

`from_` is the wire field `from`. `declare` also takes `name`, `exclude`,
`title`, `model` and `dim`. The namespace has `list`, `preview`, `set_model`,
`refresh` and `delete` too.

[Search by meaning](https://docs.littlebigbrain.com/guides/search-by-meaning/) covers each one, with the
errors and the costs.

## Search by meaning

```python
found = lbb.embeddings.search("users cannot sign in after the upgrade", top_k=3)
for hit in found["hits"]:
    print(f"{hit['score']:.3f}", hit["label"])
```

Each hit has an `iri`, a `label`, a `score` and a `class`. The `iri` is the
global name of the record. `served_at_seq` is the commit that the search
checked each hit against.

`filter_` keeps hits by type and by link. `include=["text"]` returns the
embedded text of each hit. `explain=True` plans the search and calls no model.
`embedding` searches one embedding by name.

## Starter workflows

A starter workflow is a template for work that follows a type. A workflow
created from it reads, after each published commit, the records that the
commit added or changed, and does its work. An embedding is a workflow of
the starter workflow `search.embed`; fit from text is `ontology.fit`.

```python
lbb.use_workflow_starter("search.embed", class_=TICKET, fields=["label", "description"])
ticket = lbb.workflows.named.get("ticket")
print(ticket.status.state, ticket.status.lag_commits)
for workflow in lbb.workflows.named.all():
    print(workflow.name, workflow.starter)
```

`class_` and `fields` form the wire field `watch`; `params` takes the
starter workflow's settings as a dict. `workflow_starters()` lists the
starter workflows the server offers, and `use_workflow_starter()` creates a
workflow from one or changes it. `preview_triggered_workflow()` shows what
creating one would do without writing anything (`sample=`, `iris=` and
`propose=` choose what it shows). These methods return the JSON answers,
and `AsyncLbbClient` and `graph(name)` have them too.

`lbb.workflows.named` reads and changes the workflows of the graph by name
and returns the generated models: `list(limit=, cursor=)` returns one page
in the [list envelope](https://docs.littlebigbrain.com/sdks/http-api/#lists) and `all()` iterates every
workflow, `get(name)` reads one, `put(body)` creates or changes a workflow
of your worker, `run(name)` asks it to run now, `pause(name, paused=)`
pauses or resumes it, and `delete(name, confirm=name)` deletes it.
`queue(name, cursor=, limit=)` lists what its trigger has not sent yet,
`runs(name, cursor=, limit=, outcome=)` and `all_runs(name)` its runs,
`get_run(name, run)` one run, `run_stats(name, hours=)` its runs by hour,
`run_results(name, run=, outcome=, iri=, cursor=, limit=)` and
`all_run_results(name)` what its runs did with each input, and
`documents(name, q=, sort=, cursor=, limit=)` and `all_documents(name)`
the documents of a document workflow. On `AsyncLbbClient` each method is
awaited, and `all()`, `all_runs()`, `all_run_results()` and
`all_documents()` are async iterators.
[Starter workflows](https://docs.littlebigbrain.com/guides/starter-workflows/) covers the fields, the
status and the errors.

A developer workflow (a workflow of `workflow`) sends the records to a
workflow of your worker. Create it with `workflow=`, `owns=` and `batch=`,
and pause or resume it with `workflows.named.pause()`:

```python
lbb.use_workflow_starter(
    "workflow",
    name="word-count",
    class_="Note",
    fields=["body"],
    workflow={"workflow_type": "word-count", "version": "v1"},
    owns={"properties": ["word_count"]},
    batch=16,
)
lbb.workflows.named.pause("word-count")
lbb.workflows.named.pause("word-count", paused=False)
```

Your code writes what it owns with `replace_owned` in the body of `commit`
(or `graph(name).facts.create`); the answer reports `owned`.
`ontology.suggestion_create` sends `facts`, and an accepted suggestion has
`facts_commit`.
[Developer workflows](https://docs.littlebigbrain.com/guides/starter-workflows/#developer-workflows)
covers the message and the rules.

A workflow that works on files keeps a derivation per file, so the same
bytes under a second name are worked on once.
`check_derivation(sha256, workflow_type, version, settings_digest)` answers
`exists` and the upload of the live file, `content_uploaded_at`.
`record_derivation(..., run_id=, outputs=, content_uploaded_at=)` keeps the
first record of each key. With the upload of the check, a record of a file
deleted since then writes nothing. It raises `LbbError` 409
`derivation_upload_changed`. The run of a new upload does the work:

```python
from lbb import LbbError

key = (message["origin"]["sha256"], "pdf-parse", "v1", "none")
checked = lbb.check_derivation(*key)
if not checked["exists"]:
    pages = parse(key[0])
    try:
        lbb.record_derivation(
            *key,
            run_id=message_id,
            outputs={"pages": pages},
            content_uploaded_at=checked.get("content_uploaded_at"),
        )
    except LbbError as error:
        if error.code != "derivation_upload_changed":
            raise
```

A message names instances by IRI. `entity_ref(iri, type, name=None)` turns
one into the commit reference of that record (its `key` is the id sentinel
and the 32-hex id), so a commit reaches a keyed record too:

```python
from lbb import entity_ref

subjects = [entity_ref(iri, "Note") for iri in message["instances"]]
```

In `replace_owned` only the id counts. A link endpoint or a field row writes
its name as the record's display name, so pass the record's name there.
`entity_id_from_iri(iri)` returns the id alone.

## Run a workflow worker

`lbb.workflows` runs the turns of your workflows, with the rules of the
TypeScript worker. Steps save their outputs, so a turn that runs again replays
them. `WorkflowWorker` runs on asyncio, at most `concurrency` turns at once. On
SIGTERM it drains its held turns and releases the rest.
`triggered_workflow` and `write_owned` serve developer workflows, and
`lbb_activities(client)` calls LBB's models from a step. The worker records
what each run did with each input from `write_owned`, `review` and
`requeue`, and sends it with the turn's completion
([Run results](https://docs.littlebigbrain.com/guides/workflows/#run-results)).
[Run a worker in Python](https://docs.littlebigbrain.com/guides/python-worker/) shows each part.

## Files

The document workflows read the files of a graph. `files` uploads them and
reads what the parse made of them:

```python
answer = lbb.files.upload("manual.pdf")
sha = answer["file"]["sha256"]
files = lbb.files.list()["files"]
pages = lbb.files.pages(sha, from_=0, to=2)
for line in pages["pages"][0]["lines"]:
    print(line["id"], line["box"], line["text"])
```

`upload` takes bytes with `name=`, or the path of a file (its name and its
content type default to the path's), of at most 1 GiB. A path is sent from
the file in chunks, never held whole. The same bytes under the same name
again change nothing, so a failed call is retried. `language=` (the
natural language of the content, a BCP 47 tag such as `en` or `de-CH`, at
most 64 characters) and `schema=` (a name or a URL, at most 256) describe
the content; the file's `Document` stores each one given. `list()` returns
the newest 1,000 files; `list(limit=, after=)` reads one page (`limit` 1 to
1,000), and the answer's `next` is the `after` of the next page, absent at
the end. `get(sha256)` reads one file's record, and `counts()` counts the
files by parse status, as of the commit the published generation holds.
`content(sha256)` returns the bytes, and `delete(sha256)` repeats the SHA-256
as `confirm`.

`content` holds the whole answer. Write a large file to disk as it arrives
with `download`, ask for one byte range with `range=`, and read a file's
size, type and SHA-256 without its bytes with `head`:

```python
written = lbb.files.download(sha, "manual.pdf")  # the bytes it wrote
last_kib = lbb.files.content(sha, range=(None, 1024))
first_kib = lbb.files.content(sha, range=(0, 1023))
info = lbb.files.head(sha)  # {"bytes", "content_type", "sha256"}
```

A range is `(start, end)` with both bytes included, `(start, None)` to the
end, or `(None, n)` for the last `n` bytes. A range with no byte of the file
raises `LbbError` 416 `range_not_satisfiable`. A page
range is 0-based with `to` excluded. Also on `AsyncLbbClient` and
`graph(name).files`.
[From documents to a graph](https://docs.littlebigbrain.com/guides/documents-to-graph/) creates the
document workflows and reads the links they write.

## Follow links with SPARQL

Search finds a starting point. A graph query follows the stored links from it
to the answer. The query language is SPARQL 1.1, the W3C standard for graph
queries. [Core concepts](https://docs.littlebigbrain.com/start/concepts/#graph-queries-with-sparql) explains
the syntax.

```python
ticket = found["hits"][0]["iri"]
read_at = found["served_at_seq"]
query = f"""
PREFIX dr: <https://littlebigbrain.com/r/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?bug ?release WHERE {{
  <{ticket}> dr:reported_as ?b .
  ?b rdfs:label ?bug ; dr:fixed_in ?r .
  ?r rdfs:label ?release .
}}"""

rows = lbb.sparql(query, as_of_commit_seq=read_at).rows()
print(rows, "read at commit", read_at)
```

With a real embedding model, the login ticket comes first. On a new graph,
the program then prints:

```text
[{'bug': 'session token expiry', 'release': 'Release 3.2'}] read at commit 1
```

`as_of_commit_seq` runs the query at the commit the search read. Search and
query then agree on one version of the data.

## Pin a commit

Store `read_at` with the answer. Later writes do not change that commit. The
same query with the same `as_of_commit_seq` returns the same rows while the
commit is retained.

```python
r321 = {"type": "Release", "key": "3.2.1", "name": "Release 3.2.1"}
hotfix = lbb.graph("support").facts.create({
    "triplets": [{"source": auth, "relation": "FIXED_IN", "target": r321, "evidence": "Release notes 3.2.1"}],
}, idempotency_key="sdk-support-2")
lbb.wait_for_published(hotfix["commit_seq"])

latest = lbb.sparql(query)
replay = lbb.sparql(query, as_of_commit_seq=read_at)
print(len(latest), len(replay))
```

With the login ticket as the first hit, this prints `2 1`. The latest data
names two releases. The replay reads commit 1, which names one.

A pin past the newest commit returns 400 `invalid_request`. A commit that is
no longer retained returns 400 `historical_generation_unavailable`. The server
never answers from a nearby commit instead. [Replay an answer](https://docs.littlebigbrain.com/guides/time-travel-audit/)
covers retention.

## SPARQL

`lbb.sparql(query, …)` runs a SPARQL 1.1 SELECT or ASK query. It returns
`SparqlResults`. The examples in this and the next sections use the `support`
graph from the program above.

```python
results = lbb.sparql("""
    SELECT ?ticket ?priority WHERE {
        ?t <http://www.w3.org/2000/01/rdf-schema#label> ?ticket ;
           <https://littlebigbrain.com/p/priority> ?priority .
    } ORDER BY ?ticket
""")
print(results.vars)
for row in results:
    print(row["ticket"], row["priority"])

print(lbb.sparql("ASK { ?t <https://littlebigbrain.com/r/reported_as> ?b }").boolean)
```

| Attribute | Meaning |
| --- | --- |
| `vars` | The selected variable names. |
| `rows()` | Each binding as a `{variable: value}` dict. Iterating the results yields the same dicts, and `len()` counts them. |
| `bindings` | The typed terms, with `type`, `value`, `datatype` and `xml:lang`. |
| `boolean` | The answer of an ASK query, and `None` for SELECT. |
| `row_page` | `returned`, `total`, `has_more` and the paging window. |
| `snapshot` | For an eventual or pinned read, the commit it read as `served_at_seq`. `None` for a plain strong read. |
| `next_cursor` | The cursor for the next page, when you asked for cursor pages. |
| `trace_id` | The eval trace the server recorded, when you passed `request`. |
| `search` | How a search inside the query ran. See [Search inside a query](#search-inside-a-query). `None` for a query without a search. |
| `profile` | What the server measured, when you passed `profile=True`. |

`sparql` takes these keyword arguments:

| Argument | Meaning |
| --- | --- |
| `as_of_commit_seq` | Run the query at this retained commit. |
| `entailment` | `"none"` (default), `"subclass"`, `"rdfs"` or `"owl"`. The others add facts that follow from the schema. See [Reasoning](https://docs.littlebigbrain.com/guides/reasoning/). |
| `limit`, `offset` | Return a window of the result rows. |
| `cursor` | Pages that stay on one commit. `""` starts, and `next_cursor` continues. The query needs one indexed, ordered `LIMIT` of 1 to 10,000 and no `OFFSET`. Other queries return 400 `invalid_pagination_cursor`; page them with `limit` and `offset`. |
| `consistency`, `min_indexed_seq` | See [Read your own writes](#read-your-own-writes). |
| `request` | The user's words behind the query. The server records an eval trace, and `trace_id` names it. |
| `profile` | `True` returns the server's measurements in `profile`. |
| `reason` | `True` returns a typed error. Stored rules already run when data is published. |

`query.sparql(...)` is the same method.

CONSTRUCT and DESCRIBE return RDF documents, so `sparql` refuses them with
400. Run them on the native `/sparql` endpoint. See
[Native SPARQL protocol](#native-sparql-protocol).

### Search inside a query

A query can find records by meaning. The pattern `?x search:similarTo "text"`
binds the records nearest to the text. The rest of the query then filters,
joins and orders them like other rows. `search:score` binds the score. The
prefix `search:` is `https://littlebigbrain.com/search#`, and the graph needs
an [embedding](#turn-on-search).

```python
near = lbb.sparql("""
    PREFIX search: <https://littlebigbrain.com/search#>
    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
    SELECT ?ticket ?score WHERE {
        ?t search:similarTo "sign-in fails" ;
           search:score ?score ;
           rdfs:label ?ticket .
    } ORDER BY DESC(?score) LIMIT 3
""")
print(near.rows(), near.search["plan"], near.search["complete"])
```

`search` reports how the search ran: the `plan` (`nearest`, `filter_first` or
`search_first`), the hits asked for (`top`) and bound (`hits`), `complete`, and
`lag_commits`, how far the vectors trail the commit the query read.
[Search inside a SPARQL query](https://docs.littlebigbrain.com/guides/search-by-meaning/#search-inside-a-sparql-query)
lists the other patterns and explains the plans. A search query with `cursor`
returns 400. Page it with `search:top` and `LIMIT`.

> [!TIP] Global names are lowercase
The types, links and fields you declare have global names such as
`<https://littlebigbrain.com/class/ticket>`, `<https://littlebigbrain.com/r/reported_as>`
and `<https://littlebigbrain.com/p/description>`. The last part is always
lowercase. An uppercase name is a different IRI and matches nothing. IRIs that
you load with `import_rdf` keep their original form.

## Questions in plain words

`lbb.query.ask(question)` answers a question in plain words. The server
describes the graph. A decision model picks the kind of question. A
reasoning model runs queries in a short loop, reads their rows, and answers.

```python
result = lbb.query.ask("Which release fixed the login bug?")
if result.answer is None:
    print("the loop stopped:", result.error)
print(result.answer, result.citations)
for row in result.rows:
    print(row)
```

| Method | Returns |
| --- | --- |
| `query.ask(...)` | Sends the question to `POST /v1/query/ask`. The keyword arguments are `context`, `route`, `mode`, `limit`, `as_of_commit_seq`, `today`, `include_grounding`, `anchor` (the IRIs of records the user picked) and `timeline` (the commit of each date). The result has `answer` (the text), `citations`, `chart` (how to draw the rows, or `None`), `steps`, `rows` and `vars` of the query that holds the answer, `route`, `query`, `rationale`, `error`, `trace_id`, `linked` (the names the server found), `anchors` and `history`. `response` holds the whole response. `mode="route"` returns the route alone. |
| `query.ask_stream(...)` | Takes the arguments of `query.ask` and yields one event per step: `grounding`, `route`, `step` per tool call, `answer`, and last `done` with the response. An `error` event raises `LbbError`. Close the generator to stop the server's work. |
| `query.names(text, limit=...)` | The records a question or a list of names names: `candidates` with `text`, `iri`, `label`, `class`, `score` and `by`, the one to prefer first. No model call. |
| `query.describe(question=..., classes=..., properties=...)` | The classes and properties a question needs, with how many sampled instances hold each property and the values of small classes, as JSON and as `text`. No model call. |
| `query.commit_at(date=...)` or `query.commit_at(moment=...)` | The commit of a date or a moment: `as_of_commit_seq`, `committed_at` and `resolved_by`, or a `note`. |
| `query.compare(query, before=..., after=..., key=..., max_rows=..., limit=..., cursor=...)` | Runs one `SELECT` at two points and returns `added`, `removed` and `changed` with `totals`, `truncated` and `next_cursor`. |

The async client has the same methods: coroutines, and an async generator
for `ask_stream`. `trace_id` names the eval trace of the query that holds
the answer, with the question as its request.
[Answer questions in plain words](https://docs.littlebigbrain.com/guides/answer-questions/) covers the
routes, the options and the limits. It also lists the data that goes to the
model providers.

## Model checks and search tuning

A judge model checks a sample of the model calls LBB makes for a graph: the
search rerank, the query router and rewriter, the ontology fit and the eval
labels. `lbb.checks` reads the checks. A person can agree with a check or
correct it, and the review becomes the call's ground truth.
[Check model quality](https://docs.littlebigbrain.com/guides/check-model-quality/) explains the jobs and the
scores.

```python
month = lbb.checks.summary()
print(month["judge"], month["jobs"])

checks = lbb.checks.list(verdict="wrong", reviewed=False)["checks"]
for check in checks:
    print(check["call"], check["summary"], check["judge"]["reason"])

lbb.checks.review(
    checks[0]["call"],
    agree=False,
    verdict="right",
    note="The first hit answers the question.",
)
```

| Method | Returns |
| --- | --- |
| `checks.summary(month=)` | One month per job and model: the checks, the score, right, partly and wrong, and the reviews. Also the judge's agreement with people and today's budget. |
| `checks.list(...)` | The month's checks, newest first. `job`, `month`, `verdict`, `reviewed`, `after` and `limit` filter and page. |
| `checks.review(call_id, agree=, ...)` | `agree=True` keeps the judge's verdict. `agree=False` with `verdict` corrects it, with an optional `score`, `reference` and `note`. Returns the check. |
| `checks.export(job=, month=)` | The month's checks as a list of parsed JSON lines, each `{"call": ..., "check": ...}`. |
| `checks.calls(...)` | The call log, newest first. `job`, `day`, `checked`, `after` and `limit` filter and page. |
| `checks.call(call_id)` | One call with its input, its output and its check. |
| `checks.check_call(call_id)` | Asks the judge to check one call now. Returns `{"queued": True}`. |

A search tuning session tests other search settings on the graph's own
searches. The judge grades the hits, and the session proposes the settings
that rank best. The settings change only when you apply the proposal.

```python
session = lbb.embeddings.search_tuning.start()
while session["status"] in ("queued", "running"):
    time.sleep(10)
    session = lbb.embeddings.search_tuning.get(session["id"])
if session.get("proposal"):
    lbb.embeddings.search_tuning.apply(session["id"])
```

`search_tuning.list(limit=)` lists the sessions, newest first.
`embeddings.set_search_settings` changes the settings by hand. It takes
`rerank`, `rerank_depth`, `blend` and `probe_factor`. A setting you leave at
`None` keeps its value. `lbb.RESET` sets one back to its default, and the
client sends it as JSON `null`:

```python
from lbb import RESET

lbb.embeddings.set_search_settings(rerank_depth=60, blend=RESET)
```

`checks.check_call` and `search_tuning.start` spend the platform's judge
budget. The client sends them once unless you pass
`options={"retry": True}`. The async client has every method as a coroutine.

## Model choice: test and switch models

Each use of a model on a graph (question answers `ask`, query routing
`route`, search rerank `rerank`, ontology fit `fit`, eval labels `label`) runs
one model. A trial tests another model on the use's checked calls and scores
both models against the same ground truth. When the candidate meets the bar,
switch the use to it, and revert in one step. These methods return typed
models, so you read `trial.report.qualifies`, not dictionary keys.
[Check model quality](https://docs.littlebigbrain.com/guides/check-model-quality/) explains the scores and
the bar.

```python
# What each use runs now, and the models you can test.
options = lbb.models.trials.options()

# Test Claude Haiku 5.5 at low effort on question answers.
started = lbb.models.trials.create(
    candidate={"provider": "anthropic", "model": "claude-haiku-5-5", "effort": "low"},
    jobs=["ask"],
)

# The trial runs on the server. Wait until it compared the checked calls.
trial = lbb.models.trials.wait(
    started.trials[0].id,
    on_update=lambda t: print(f"{t.report.calls} calls compared"),
)
report = trial.report
print(report.current.score, report.candidate.score, report.ci_low, report.ci_high)

if report.qualifies:
    lbb.models.switches.create(trial=trial.id)

# Back to LBB's model.
lbb.models.switches.revert("ask")
```

With `AsyncLbbClient`, every method, `wait` too, is awaitable.

| Method | Returns |
| --- | --- |
| `models.trials.options()` | Per use: the model it runs now, LBB's model, a switch, its checked calls, and the models you can test with their efforts and prices. |
| `models.trials.create(candidate=, jobs=, target=, days=)` | `trials` and `skipped`: one trial per use. An open trial of the same use and candidate comes back as it is. `candidate` is a dict or a `ModelTrialModel`. |
| `models.trials.wait(trial_id, until=, timeout=, poll_interval=, on_update=)` | The trial once it compared what it can (`until="compared"`, the default) or ended (`until="ended"`). |
| `models.trials.list(job=, limit=)` | The trials, newest first, with their reports. |
| `models.trials.get(trial_id)` | One trial with its report and its compared calls. |
| `models.trials.call(trial_id, call_id)` | One compared call: both answers and the ground truth. |
| `models.trials.stop(trial_id)` | Stops a trial. It keeps what it compared. |
| `models.switches.create(trial=)` | Switches the trial's use to its candidate. |
| `models.switches.list()` | The switched uses. |
| `models.switches.revert(job)` | Puts the use back on LBB's model. |
| `models.choices.list(workflow=, use=, limit=, cursor=)` | The model of each workflow step (`extract`, `decide`, `embed`): the environment's rows, and with `workflow` the row in force for each step of that workflow, with the models each can run (`options`). `all` pages through it. |
| `models.choices.put(use=, model=, workflow=, idempotency_key=)` | Chooses the model of a step for the environment or one workflow. The next calls run it. The client sends an idempotency key unless you pass one. |
| `models.choices.delete(use=, workflow=, idempotency_key=)` | Removes a choice: the step runs the environment's model, else LBB's. Returns `ModelChoiceDeleted` (`deleted` is true). |

A report gives both models' right share, mean score (0 to 1), cost per call
and median time; the difference with its 95% interval (`delta`, `ci_low`,
`ci_high`); and `qualifies`: at least 20 calls, at most 5 points worse at the
low end, no more failures, and cheaper or better. A switch refuses a trial
that does not qualify (`trial_not_qualified`) or that compared with another
model than the one the use runs now (`trial_outdated`): start a new trial.
Catch them as `LbbError` and read `error.code`.

## Read your own writes

Reads take a `consistency` argument:

- `"eventual"` is the default. It reads the latest published commit, and the
  response's `snapshot["served_at_seq"]` names it.
- `"strong"` also applies the newest writes. Use it to read your own write at
  once. Its response has no `snapshot` block.

`min_indexed_seq` sets a floor: the read must cover that commit or a later
one. A floor without a `consistency` reads strong.

```python
note = lbb.graph("support").facts.create({
    "entity_properties": [{**invoice, "properties": {"priority": "high"}}],
})
now = lbb.sparql(
    "SELECT ?p WHERE { ?t <https://littlebigbrain.com/p/priority> ?p }",
    consistency="strong",
    min_indexed_seq=note["commit_seq"],
)
print(len(now))
```

Publication may not cover the floor yet. Then an eventual read returns 429
`read_your_writes_pending` with a `Retry-After`. `sparql` retries it within
the retry budget. You can also call `wait_for_published` first.

```python
pending = lbb.sparql(
    "SELECT ?p WHERE { ?t <https://littlebigbrain.com/p/priority> ?p }",
    consistency="eventual",
    min_indexed_seq=note["commit_seq"],
)
print(pending.snapshot["served_at_seq"])
```

Use `"strong"` when you need the answer now. Use `"eventual"` with a floor
when you must record which commit you read.

`consistency` and `min_indexed_seq` work on `sparql`, `sparql_select`,
`summary`, `query.sparql` and `query.structured`. `ontology.conformance` and
`entities.detail` take `consistency` only.

`entities.detail(...)` reads one record: its typed `attributes` and its current
links in `rdf_relations`. Name it by `id`, by `type` and `name`, or by `type`
and `key`. `as_of_commit_seq` reads it at a retained commit.

```python
record = lbb.entities.detail(type="Ticket", key="4821", consistency="strong")
print(record["attributes"], len(record["rdf_relations"]["outgoing"]))
```

## Load RDF

If your data is RDF already, load it as it is. `facts.import_rdf` sends one
document in N-Triples, Turtle, N-Quads or TriG. [Load RDF data](https://docs.littlebigbrain.com/guides/load-rdf/)
walks through a full import.

```python
turtle = """
@prefix ex: <https://example.org/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
ex:auth-service rdfs:label "auth-service" ; ex:writesTo ex:user-db .
ex:user-db rdfs:label "user-db" .
"""
imported = lbb.graph("catalog").facts.import_rdf(
    turtle,
    format="turtle",
    base_iri="https://example.org/",
    idempotency_key="catalog-2026-09-25",
)
print(imported["triples_read"], imported["committed_commit_seq"])
```

The first import creates the graph with a fixed generic schema, so you declare
nothing first. Source IRIs keep their original form in queries. The response
reports `triples_read`, `committed_commit_seq`, `error_count` and the first
`errors`.

| Argument | Meaning |
| --- | --- |
| `format` | `"ntriples"` (default), `"turtle"`, `"nquads"` or `"trig"`. |
| `base_iri` | The base for relative IRIs. |
| `blank_node_scope` | Scopes blank-node labels to one source document. An ASCII token of letters, digits and `-_.:/`. |
| `batch` | Statements per internal commit. Default 100,000, maximum 1,000,000. |
| `strict` | `True` fails the request at the first bad statement. By default the importer skips it and reports it in `errors`. |
| `observed_at` | An RFC 3339 time to record on every statement. |
| `build` | `False` holds back publication for this request. |
| `idempotency_key` | The key for this request. The client creates one if you omit it. |
| `graph_uri` | Refused with 400. Each graph serves the default RDF graph only. |

Keep each request under 64 MiB. N-Triples and N-Quads have one statement per
line. Split a large file on line boundaries. Turtle and TriG requests must
each be a complete document.

Omit `blank_node_scope` to keep blank-node labels the same across chunks and
retries. Reuse one scope for every chunk of one source document. Documents
that reuse the same labels then stay apart.

`facts.import_rdf_many([doc, …], …)` imports several documents in order. Only
the last request triggers publication. It returns a dict with `imports`,
`final_sequence` and `publication`. Pass `final_sequence` to
`wait_for_published`.

With an `idempotency_key`, document `n` uses the key suffixed with `:n`.
`lbb.import_rdf(...)` and `lbb.import_rdf_many(...)` do the same for the
client's graph.

`query.update(text, idempotency_key=None)` runs SPARQL Update on the `/update`
endpoint of the client's graph. The server accepts `INSERT DATA`, and one
request is one commit. Other update forms return 400. The client sends an
idempotency key, so a retry replays the write. The answer has no body, so read
the new facts with a `strong` read.

```python
catalog = LbbClient(os.environ["LBB_URL"], api_key=os.environ["LBB_API_KEY"], graph="catalog")
catalog.query.update(
    'INSERT DATA { <https://example.org/user-db> <https://example.org/tier> "gold" }',
    idempotency_key="catalog-tier-1",
)
```

A graph whose first write is RDF only takes RDF writes. On it, `commit`,
`facts.create`, `commit_dry_run`, `import_ndjson`, `retract` and `reload`
return 400 `rdf_native_graph`. A durable import job fails. Write JSON records
first when you need both.

## Import NDJSON

For many records, send newline-delimited JSON. Each line is a triplet, or a
record with `type`, `name`, an optional `key` and `properties`.

`facts.import_ndjson(lines)` imports in one request of at most 64 MiB. The
server splits the lines into bounded commits and returns
`committed_commit_seq`, the last one.

```python
tickets = [
    {"type": "Ticket", "key": "4824", "name": "search returns stale results", "properties": {"priority": "high"}},
    {"source": {"type": "Ticket", "key": "4824", "name": "search returns stale results"},
     "relation": "REPORTED_AS", "target": {"type": "Bug", "key": "SRCH-9", "name": "cache not invalidated"}},
]
sync = lbb.graph("support").facts.import_ndjson(tickets, idempotency_key="tickets-4824")
print(sync["committed_commit_seq"])
```

For a large dataset, use `submit_import_ndjson`. It streams a list, an iterable
or a generator into a durable import job. It never holds the whole dataset in
memory. The job runs on the server and survives a lost connection.

```python
def records():
    yield {"type": "Ticket", "key": "4825", "name": "PDF export fails", "properties": {"priority": "low"}}

accepted = lbb.submit_import_ndjson(records(), idempotency_key="tickets-2026-09-25")
status = lbb.wait_for_import_job(accepted.job_id)
if status.state.value != "succeeded":
    raise RuntimeError(status.failure.message if status.failure else status.state)
print(status.committed_commit_seq.root)
```

- `idempotency_key` is required. It binds the key to the uploaded content.
- The client uploads once and does not retry the stream. To retry, call again
  with a fresh iterable and the same key.
- An empty source raises `ValueError` before any request.
- `get_import_job(job_id)` reads the status, and `cancel_import_job(job_id)`
  cancels the job.
- `wait_for_import_job` polls every second. It takes `timeout` and
  `poll_interval` in seconds, and raises `TimeoutError` after `timeout`.

These methods return typed models. The durable route needs the server
capability `durable_import_jobs_v1`. The client checks `GET /version` first
and raises `LbbCapabilityError` if it is missing. A request is at most 32 GiB.

Both methods take `batch`, the records per internal commit, up to 10,000. The
default is 1,000 for `import_ndjson` and 10,000 for a job. `strict=True`
fails at the first malformed line, which is skipped by default. `observed_at`
records an RFC 3339 time on every record.

The [HTTP API](https://docs.littlebigbrain.com/sdks/http-api/) documents the job states and failure fields.

## Structured queries

`sparql_select(body)` is a SELECT or ASK query written as JSON. It takes link
patterns, typed filters, `group_by`, aggregates, `having` and `order_by`. Use
it when you build a query from user input and want no query text.

```python
pairs = lbb.sparql_select({
    "patterns": [{"subject": {"var": "ticket"}, "predicate": "REPORTED_AS", "object": {"var": "bug"}}],
    "select": ["ticket", "bug"],
    "limit": 50,
})
print(len(pairs["solutions"]))
```

`query.structured(body)` sends the same request, returns a typed model and
retries a retryable `429`.

`entities.filter_by_attributes` builds that body for you. The `patterns` bind
variables through links. `where` compares declared fields on those variables.
`op` is `eq` (default), `ne`, `lt`, `le`, `gt` or `ge`.

```python
urgent = lbb.entities.filter_by_attributes(
    patterns=[{"subject": {"var": "ticket"}, "predicate": "REPORTED_AS", "object": {"var": "bug"}}],
    where=[{"var": "ticket", "field": "priority", "value": "high"}],
    select=["ticket", "bug"],
)
print(len(urgent["solutions"]))
```

A value can also be `{"dateTime": "…"}` or `{"entity": selector}`.
`group_keys` groups by a field value or by a calendar bucket of a date field.
See [Query and validate](https://docs.littlebigbrain.com/guides/sparql-and-shacl/) for the full request.

A body can be a `dict` or a model from `lbb.models`.

## Schema and SHACL

`ontology.view()` returns the declared types, links and fields of the graph.
With `counts=True`, each link also reports how many current links it has.

For rules across fields and links, publish SHACL shapes, the W3C standard for
validation rules. `schema.publish` stores the shapes and a mode.
`schema.view()` returns the active shapes and mode.

```python
shapes = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix dclass: <https://littlebigbrain.com/class/> .
@prefix dr: <https://littlebigbrain.com/r/> .
<https://example.org/shapes/Ticket> a sh:NodeShape ;
  sh:targetClass dclass:ticket ;
  sh:property [ sh:path dr:reported_as ; sh:maxCount 1 ;
                sh:message "A ticket is reported as one bug at most." ] .
"""

lbb.schema.publish({"desired_mode": "warn", "shapes": {"source": shapes, "format": "turtle"}})
report = lbb.ontology.conformance(consistency="strong", options={"max_retries": 60})
print(report.conforms, report.result_count, report.validated_at_seq.root)
if report.conforms:
    lbb.schema.publish({"desired_mode": "reject", "shapes": {"source": shapes, "format": "turtle"}})
```

| Mode | Behavior |
| --- | --- |
| `off` | No check at write time. |
| `warn` | Writes succeed. The conformance report lists violations. Use it while you add or repair a shape. |
| `reject` | A write that breaks a shape returns 400, and nothing is stored. Use it after the report is clean. |

After a publication, the platform validates the graph in the background.
`ontology.conformance()` reads the stored report for the active shapes and
takes no shapes of its own.

A strong read returns a retryable 429 `strong_read_pending` until the report
for new shapes exists. The client retries it after each `Retry-After`. The
first report can take several seconds, so the example raises `max_retries`
from 6.

A change of mode publishes the same shapes as a new version.
[Choose an enforcement mode](https://docs.littlebigbrain.com/guides/sparql-and-shacl/#choose-an-enforcement-mode)
explains when to switch.

`commit_dry_run(body)` runs the same checks as a commit and writes nothing.
Its `schema_validation` lists each violation with `component`, `focus_node`,
`result_path` and `message`. An agent can read that list, fix its write and
retry.

```python
preview = lbb.commit_dry_run({
    "triplets": [{"source": login, "relation": "REPORTED_AS", "target": pdf}],
})
print(preview["schema_validation"]["conforms"])
```

`ontology.evolve` takes typed operations from `lbb.models`, each with an `op`
field. Examples are `AddEntityTypeOp`, `AddRelationOp`, `AddPropertyOp` and
`WidenRelationOp`. The models also cover the operations that narrow, rename or
remove.

`AdditiveOntologyEvolveRequest` accepts four operations: `add_entity_type`,
`add_relation`, `add_super_types` and `widen_relation`. Use it when a model or
an agent proposes changes and must not narrow the schema.

With `dry_run=True`, `evolve` returns the typed diff: `applied`, `conflicts`,
`publishable`, `no_op` and the next `ontology_version`. Nothing is written.

```python
from lbb.models import AddEntityTypeOp, AdditiveOntologyEvolveRequest

change = AdditiveOntologyEvolveRequest(ops=[AddEntityTypeOp(op="add_entity_type", name="Component")])
diff = lbb.ontology.evolve(change, dry_run=True)
print(diff.publishable, diff.no_op, diff.ontology_version)
```

Ontology drafts keep a proposed schema apart from the graph.
`ontology.draft_create(...)` stores sample evidence, competency-question
analyses, proposed operations, coverage, pitfalls and confidence at one
commit.

Continue with `draft_validate`, then `draft_promote` or `draft_reject`.
`draft_promote` takes an idempotency key and is safe to retry.

## Change and delete data

`graph(name).retract` removes current links as a new commit. `edges` names
single links. `entities` removes every link of a record.
`purge_entity_properties: True` also clears the record's fields, so it leaves
search and SPARQL. Past commits keep the retracted data.

```python
retracted = lbb.graph("support").retract({
    "edges": [{"source": pdf, "relation": "FIXED_IN", "target": r311}],
})
print(retracted["commit_seq"])
```

| Method | What it does |
| --- | --- |
| `graph(name).retract(body)` or `retract(body)` | Removes links, and optionally fields, as a new commit. |
| `reload(lines, confirm=, dry_run=)` | Makes the graph match the NDJSON lines in one commit. Records missing from the lines leave current state. `confirm` is the graph name. `dry_run=True` previews the change and writes nothing. |
| `fork_graph(src, dst)` | Copies a graph into a new graph as a background job. The destination must be a new graph id. The id of a deleted graph is refused. Safe to retry. |
| `create_graph()` | Creates the client's graph with an empty schema. `ontology.define` also creates it. |
| `list_graphs()` | Lists the graphs of the environment. |
| `graph(name).delete(confirm=)` or `delete_graph(confirm=)` | Deletes the graph, its search feedback and its pending jobs. `confirm` is the graph name. Safe to retry. |

A `reload` response carries `prior_commit_seq`. Read the graph at that commit
with `as_of_commit_seq` to see the state before the reload.

```python
planned = lbb.reload(tickets, confirm="support", dry_run=True)
print(planned.prior_commit_seq)
```

## Publication status

After a commit, the platform publishes it for search and eventual reads.

| Method | Returns |
| --- | --- |
| `wait_for_published(commit_seq, timeout=30.0, poll_interval=0.25)` | Returns when the latest published commit covers `commit_seq`. Raises `TimeoutError` after `timeout` seconds, and `RuntimeError` when publication is `blocked`. Waits longer when the server sends a longer retry delay. |
| `publication_status()` | `state` (`current`, `queued`, `compacting`, `building`, `publishing` or `blocked`), `head_seq`, `published_seq`, `lag_commits` and `retry`. |
| `read_snapshot()` | `head_seq`, `snapshot.served_at_seq`, `query_lag_commits`, `base_current` and `conformance_lag_commits`. |
| `summary()` | Record, link and type counts, with the commit they describe. |
| `schema_summary()` | The types and fields found in the published data, with counts. |
| `planner_stats(cursor=None, limit=None)` | The statistics the query planner uses: triple counts per link and field, and the indexes present. See [See how a query ran](https://docs.littlebigbrain.com/guides/query-profiles/). |
| `metadata()` | The ontology version and storage size of the graph. |

`base_current` is `True` when the published data reflects every commit and
schema change.

```python
published = lbb.read_snapshot_model()
print(published.snapshot.served_at_seq, published.query_lag_commits, published.base_current)
```

## Errors and retries

Methods return parsed JSON. On a non-2xx response, they raise `LbbError`.

```python
try:
    lbb.graph("support").facts.create({
        "triplets": [{"source": login, "relation": "BLOCKS", "target": auth}],
    })
except LbbError as err:
    print(err.status_code, err.code, err)
```

| Attribute | Meaning |
| --- | --- |
| `status_code` | The HTTP status. |
| `code`, `type`, `param` | The error from the response body. `str(err)` is its message. |
| `retryable`, `retry_after_seconds` | Whether a retry can succeed, and when. |
| `request_id`, `doc_url` | The request id to quote in support, and a link to the docs. |
| `endpoint_hint` | For `stack_endpoint_required` and `stack_endpoint_mismatch`: what to fix. |
| `details` | The reasons for each item of a refusal. An example is the `conflicts` of `409 starter_conflict`. |
| `body`, `error` | The raw response body, and its parsed `error` object. |

The client retries these requests:

- GET, HEAD and OPTIONS requests.
- Writes that carry an idempotency key: commits, imports, retracts, reloads,
  SPARQL updates, schema publications and draft promotions. The client adds
  the key when you omit it.
- The read-only POST methods `query.structured`, `ontology.search`,
  `ontology.resolve` and `ontology.draft_validate`.
- Graph deletes and forks.
- `embeddings.search_tuning.apply`, which sets the same settings again.
- A retryable `429` on `sparql` and `query.sparql`. A `5xx` or a transport
  failure is not retried, because the query could run twice.

Other POST and PUT calls are sent once. Examples are `ontology.define`,
`embeddings.declare`, `embeddings.search` and `sparql_select`.

A retry follows a `429`, a `5xx` or a transport failure. The delay is the
`Retry-After` header, the body's `retry_after_seconds`, or a full-jitter
exponential backoff. An error that the body marks `retryable: false` is raised
at once.

`retry_budget_ms` bounds the total retry time, and `max_retries` caps the
count. `on_retry` receives a `RetryEvent` with `method`, `path`, `attempt`,
`status_code`, `error_code`, `delay_seconds` and `elapsed_ms`.

`raw_request(method, path, params=, body=, options=)` returns a
`RawLbbResponse`. It has `data`, `status_code`, `request_id`, `version`,
`headers`, `attempts`, `retry_count` and `elapsed_ms`.

`options` overrides `max_retries`, `retry`, `retry_budget_ms`, `timeout` and
`headers` for one call. `retry` is `True`, `False` or `"rate_limited"`, which
retries a retryable `429` only. The typed `ontology` and `query` methods take the same
`options=` argument.

```python
response = lbb.raw_request("GET", "/v1/graph/summary", options={"max_retries": 2})
print(response.status_code, response.request_id, response.attempts)
```

## Async client

`AsyncLbbClient` has the same constructor and the same methods as
coroutines. Use it as an async context manager, or call `await lbb.aclose()`.
`submit_import_ndjson` also accepts an async iterable.

```python
import asyncio

from lbb import AsyncLbbClient

async def main():
    async with AsyncLbbClient(os.environ["LBB_URL"], api_key=os.environ["LBB_API_KEY"], graph="support") as client:
        found, results = await asyncio.gather(
            client.embeddings.search("export is slow", top_k=2),
            client.sparql("SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }"),
        )
        print([hit["label"] for hit in found["hits"]], results.rows()[0]["n"])

asyncio.run(main())
```

## Typed models

The plain methods return `dict`s, which suit scripts and notebooks. For
application code, the generated Pydantic models in `lbb.models` give checked
fields and editor completion.

Some methods return a model by default. They are the `ontology` namespace,
`query.structured`, the import jobs, `reload`,
`create_graph`, `fork_graph`, `delete_graph` and `wait_for_published`.

Other methods have a `*_model` twin:

| Returns a `dict` | Returns a model |
| --- | --- |
| `commit`, `commit_dry_run` | `commit_model`, `commit_dry_run_model` |
| `graph(name).facts.create`, `graph(name).retract` | `graph(name).facts.create_model`, `graph(name).retract_model` |
| `publication_status`, `graph(name).publication_status` | `publication_status_model`, `graph(name).publication_status_model` |
| `summary`, `metadata`, `read_snapshot`, `schema_summary`, `planner_stats`, `list_graphs` | `summary_model`, `metadata_model`, `read_snapshot_model`, `schema_summary_model`, `planner_stats_model`, `list_graphs_model` |
| `ontology_view`, `ontology_conformance` | `ontology_view_model`, `ontology_conformance_model` |
| `sparql_select`, `entities.filter_by_attributes` | `sparql_select_model`, `entities.filter_by_attributes_model` |
| `entities.detail` | `entities.detail_model` |
| `schema.view`, `schema.publish` | `schema.view_model`, `schema.publish_model` |

Each typed method is annotated with its exact model. `raw.model(Model)`
validates any `raw_request` response. The async client has the same helpers as
coroutines.

Some models hold a commit number as a `CommitSeq`. Read the integer with
`.root`, as in `status.committed_commit_seq.root`.

```python
from lbb.models import GraphSummaryResponse

summary: GraphSummaryResponse = lbb.summary_model()
print(summary.entity_count, summary.ontology_version)

raw = lbb.raw_request("GET", "/v1/graph/summary")
print(raw.model(GraphSummaryResponse).entity_count)
```

A model also works as a request body. The client leaves out each optional
field that is `None`. A required field that is `None` is sent as JSON `null`.

```python
from lbb.models import OntologyEvolveRequest, WidenRelationOp

lbb.ontology.evolve(
    OntologyEvolveRequest(
        ops=[
            WidenRelationOp(
                op="widen_relation", relation="FIXED_IN", add_domain=["Ticket"]
            )
        ]
    )
)
```

## Method map

| Area | Methods |
| --- | --- |
| Schema | `ontology.define`, `ontology.evolve`, `ontology.view`, `ontology.conformance`, `ontology.search`, `ontology.resolve`, `ontology.draft_*`, `schema.view`, `schema.publish`; `graph(name).ontology` scopes each one to a graph |
| Ontology starters | `ontology.starters.list`, `ontology.starters.get`, `ontology.starters.apply`, `ontology.starters.update`; the module `lbb.starters`: `crm`, `documents`, `work` |
| Starter workflows | `workflow_starters`, `use_workflow_starter`, `preview_triggered_workflow`; `graph(name)` scopes each one to a graph; `entity_ref` and `entity_id_from_iri` |
| Workflows by name | `workflows.named.list`, `workflows.named.all`, `workflows.named.get`, `workflows.named.put`, `workflows.named.run`, `workflows.named.pause`, `workflows.named.delete`, `workflows.named.queue`, `workflows.named.runs`, `workflows.named.all_runs`, `workflows.named.get_run`, `workflows.named.run_stats`, `workflows.named.run_results`, `workflows.named.all_run_results`, `workflows.named.documents`, `workflows.named.all_documents` |
| Files | `files.upload`, `files.list`, `files.get`, `files.counts`, `files.content`, `files.download`, `files.head`, `files.pages`, `files.delete`; `graph(name).files` scopes each one to a graph |
| Suggestions | `ontology.suggestions`, `ontology.suggestion_get`, `ontology.suggestion_create`, `ontology.suggestion_validate`, `ontology.suggestion_accept`, `ontology.suggestion_dismiss`, `ontology.suggestion_supersede`, `ontology.suggestion_comment` |
| Write | `graph(name).facts.create`, `commit`, `commit_dry_run`, `graph(name).retract`, `retract`, `reload` |
| Import | `graph(name).facts.import_rdf`, `import_rdf`, `import_rdf_many`, `graph(name).facts.import_ndjson`, `import_ndjson`, `submit_import_ndjson`, `get_import_job`, `wait_for_import_job`, `cancel_import_job` |
| Search | `embeddings.declare`, `embeddings.get`, `embeddings.list`, `embeddings.preview`, `embeddings.search`, `embeddings.set_model`, `embeddings.refresh`, `embeddings.delete`, `embeddings.search_settings`, `embeddings.set_search_settings`, `managed_models`, `model_activity` |
| Search tuning | `embeddings.search_tuning.start`, `embeddings.search_tuning.list`, `embeddings.search_tuning.get`, `embeddings.search_tuning.apply` |
| Model checks | `checks.summary`, `checks.list`, `checks.review`, `checks.export`, `checks.calls`, `checks.call`, `checks.check_call` |
| Model choice | `models.trials.options`, `models.trials.create`, `models.trials.wait`, `models.trials.list`, `models.trials.get`, `models.trials.call`, `models.trials.stop`, `models.switches.create`, `models.switches.list`, `models.switches.revert` |
| Query | `sparql`, `query.sparql`, `sparql_select`, `query.structured`, `query.update`, `query.ask`, `query.ask_stream`, `entities.filter_by_attributes` |
| Records | `entities.detail`, `entities.detail_model` |
| Graphs | `create_graph`, `list_graphs`, `fork_graph`, `delete_graph`, `graph(name).delete` |
| Integrations | `integrations.connectors`, `integrations.provider_apps`, `integrations.create_connect_session`, `integrations.get_connect_session`, `integrations.create`, `integrations.list`, `integrations.get`, `integrations.set_credentials`, `integrations.set_settings`, `integrations.sync`, `integrations.pause`, `integrations.resume`, `integrations.delete`, `integrations.erase`, `integrations.suggestions`, `integrations.accept`, `integrations.dismiss`; `google_drive.authorize_url`, `google_drive.exchange_code` and `google_drive.exchange_code_async` (module `lbb.google_drive`) |
| Publication | `wait_for_published`, `publication_status`, `read_snapshot`, `summary`, `schema_summary`, `metadata`, `status` |
| Transport | `raw_request`, `idempotency_key`, `close`, `graph` |

The client has further methods for managed evals and model training. This page
does not cover them.

## Native SPARQL protocol

Each environment also serves the SPARQL 1.1 Protocol at `/sparql`. Standard tools
such as YASGUI, Protégé and RDFLib's `SPARQLWrapper` connect there. It takes
`GET ?query=`, a form POST, or an `application/sparql-query` body.

The `Accept` header selects the format. SELECT and ASK return JSON, XML, CSV
or TSV. CONSTRUCT and DESCRIBE return Turtle or N-Triples.

`/sparql` accepts `consistency`, `min_indexed_seq`, `as_of_commit_seq` and
`entailment` as query parameters. To export the graph as RDF, run a CONSTRUCT
query there:

```python
exported = lbb.raw_request(
    "GET", "/sparql",
    params={"query": "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"},
    options={"headers": {"accept": "application/n-triples"}},
)
print(len(exported.data.splitlines()))
```

The [HTTP API](https://docs.littlebigbrain.com/sdks/http-api/#native-sparql-11-protocol) covers the protocol
in full.

## Optional application helpers

Start with the client above. The optional `catalog` and `access` helpers add
namespace membership, exact-epoch adoption, bounded catalog pages and delegated
read tokens. They require separately enabled server support. Shared-reference
queries also require explicit access to every selected source. See
[How to model your app](https://docs.littlebigbrain.com/start/model-your-app/) for boundaries and an example.
