Python — lbb
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 and returns JSON as a dict or a typed model.
This page uses the example of the 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
(Apache-2.0).
Install
Section titled “Install”pip install littlebigbrainThe package imports as lbb. This page describes version 0.32.0.
Create a client
Section titled “Create a client”import jsonimport osimport 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:.
| 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. |
default_consistency |
the server’s eventual |
The read consistency when a call names none. See 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. |
Declare types and links
Section titled “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.
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 covers
parent types, inverse links and allowed values.
Write records
Section titled “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.
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.
Turn on search
Section titled “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.
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 covers each one, with the errors and the costs.
Search by meaning
Section titled “Search by meaning”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
Section titled “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.
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 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 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():
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
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:
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": raiseA 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:
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
Section titled “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).
Run a worker in Python shows each part.
The document workflows read the files of a graph. files uploads them and
reads what the parse made of them:
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:
written = lbb.files.download(sha, "manual.pdf") # the bytes it wrotelast_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 creates the
document workflows and reads the links they write.
Follow links with SPARQL
Section titled “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 explains the syntax.
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:
[{'bug': 'session token expiry', 'release': 'Release 3.2'}] read at commit 1as_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
Section titled “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.
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
covers retention.
SPARQL
Section titled “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.
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. 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. |
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. |
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.
Search inside a query
Section titled “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.
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
lists the other patterns and explains the plans. A search query with cursor
returns 400. Page it with search:top and LIMIT.
Questions in plain words
Section titled “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.
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 covers the
routes, the options and the limits. It also lists the data that goes to the
model providers.
Model checks and search tuning
Section titled “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 explains the jobs and the
scores.
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.
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:
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
Section titled “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 explains the scores and
the bar.
# 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.reportprint(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
Section titled “Read your own writes”Reads take a consistency argument:
"eventual"is the default. It reads the latest published commit, and the response’ssnapshot["served_at_seq"]names it."strong"also applies the newest writes. Use it to read your own write at once. Its response has nosnapshotblock.
min_indexed_seq sets a floor: the read must cover that commit or a later
one. A floor without a consistency reads strong.
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.
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.
record = lbb.entities.detail(type="Ticket", key="4821", consistency="strong")print(record["attributes"], len(record["rdf_relations"]["outgoing"]))Load RDF
Section titled “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
walks through a full import.
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.
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
Section titled “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.
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.
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_keyis 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
ValueErrorbefore any request. get_import_job(job_id)reads the status, andcancel_import_job(job_id)cancels the job.wait_for_import_jobpolls every second. It takestimeoutandpoll_intervalin seconds, and raisesTimeoutErroraftertimeout.
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 documents the job states and failure fields.
Structured queries
Section titled “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.
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.
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 for the full request.
A body can be a dict or a model from lbb.models.
Schema and SHACL
Section titled “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.
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 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.
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.
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
Section titled “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.
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.
planned = lbb.reload(tickets, confirm="support", dry_run=True)print(planned.prior_commit_seq)Publication status
Section titled “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. |
metadata() |
The ontology version and storage size of the graph. |
base_current is True when the published data reflects every commit and
schema change.
published = lbb.read_snapshot_model()print(published.snapshot.served_at_seq, published.query_lag_commits, published.base_current)Errors and retries
Section titled “Errors and retries”Methods return parsed JSON. On a non-2xx response, they raise LbbError.
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.resolveandontology.draft_validate. - Graph deletes and forks.
embeddings.search_tuning.apply, which sets the same settings again.- A retryable
429onsparqlandquery.sparql. A5xxor 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.
response = lbb.raw_request("GET", "/v1/graph/summary", options={"max_retries": 2})print(response.status_code, response.request_id, response.attempts)Async client
Section titled “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.
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
Section titled “Typed models”The plain methods return dicts, 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.
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.
from lbb.models import OntologyEvolveRequest, WidenRelationOp
lbb.ontology.evolve( OntologyEvolveRequest( ops=[ WidenRelationOp( op="widen_relation", relation="FIXED_IN", add_domain=["Ticket"] ) ] ))Method map
Section titled “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
Section titled “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:
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 covers the protocol in full.
Optional application helpers
Section titled “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 for boundaries and an example.