# Ontology suggestions (experimental)

Source: https://docs.littlebigbrain.com/guides/ontology-suggestions/

A suggestion is a proposed change to your ontology that waits for a person.
Integrations, agents and people all file the same kind of suggestion. You
read it, discuss it, and accept or dismiss it.

Accepting applies the change to the ontology as it is now. Dismissing keeps
the ontology as it is and records your reason.

To get suggestions from the text of your instances (transcripts,
documents), see
[Suggest ontology changes from text](https://docs.littlebigbrain.com/guides/suggest-ontology-changes-from-text/).

## What a suggestion holds

- **A title and a rationale**, for example "Add class Deal".
- **An anchor**: the class, property or relation it is about.
- **An origin**: the integration, agent or person that filed it.
- **The change**: the same operations as `POST /v1/ontology/evolve`.
- **Identities**: records from different sources that describe one thing.
- **Facts**: graph facts (links and field values) that accepting commits.
- **Evidence**: record counts, fill rate, source fields and sample records.
- **The impact**: a dry run against the current ontology.
- **A discussion**: comments, and the events of its review.

## Review in the console

Open **Review** in the sidebar. It is the one inbox of questions for a
person, and its badge counts the open items. The inbox lists open
suggestions first. To see one kind at a time, pick a type: **Facts** (the items of the
review workflow to check), **Links** (the link reviews of
`documents.link`) or **Ontology** (a new class, relation or property, and
other changes). Each type shows its count. **Status** and **Source** are
menus above the list: the status of the suggestions shown (open by
default), and who filed them (integrations, agents or people). The address keeps
the type (`&kind=links`), so a link to the page keeps it. Each row shows the start of its rationale, or the document, page and
confidence its evidence comes from. Select one to see the change as a diff,
the evidence and the impact. An evidence sample that names a line of a page
shows that line, the model's answer, and **View on page**.

- **Accept** applies the change.
- **Edit, then accept** lets you rename a class or change a value type first.
- **Dismiss** asks for a reason.

A suggestion with facts shows them in a table: one row per link, with its
confidence and its evidence on one line. A suggestion with facts and no
change has no diff and no dry run. Accepting it shows the commit that wrote
the facts.

A link review of `documents.link` opens with where the link goes **From**
(each mention, its span marked in its line, with its document and page)
and **To** (the entry, its title and its document), and one sentence with
the model's answer: "The model found no entry (68%). Best candidate: Item
212 at 31%." **Link to Item 212** accepts it; **Not a link** dismisses it.

When the ontology changed since the dry run, **Check again** runs it again.

With more than 20 open suggestions, the tab opens on **Patterns**: one card
per group of suggestions that ask one decision, for example "Link
"#.#.#" in manual.pdf to Section #.#.#". A card names the workflow, the count
and the confidence range, and shows three samples that open in full. Leave
a sample out with its box, or **Show all** to leave out any of them. Then
**Accept all** or **Dismiss all** decides the rest, after a confirmation
that names the count and the workflow. The tab says what was accepted and
what was skipped, and why. **Items** lists the suggestions one by one.

## File a suggestion

Send the change with a stable `key`. Filing again with the same key revises
the suggestion instead of adding another one.

```ts
import { LbbClient } from "@littlebigbrain/client";

const client = new LbbClient({
  baseUrl: process.env.LBB_URL!,
  apiKey: process.env.LBB_API_KEY!,
});

await client.ontology.suggestions.create({
  key: "crm/annual-revenue",
  title: "Add annual_revenue to Organization",
  rationale: "99% of CRM companies carry a yearly revenue figure.",
  anchor: { kind: "class", name: "Organization" },
  origin: { kind: "person", id: "ana@example.com" },
  change: [{ op: "add_property", name: "annual_revenue", value_type: "f64" }],
  evidence: { records: 1204, fill_rate: 0.99 },
});
```

List and decide:

```ts
const { suggestions, counts } = await client.ontology.suggestions.list({
  status: "open",
});
await client.ontology.suggestions.accept(suggestions[0].suggestion_id, {
  author: "ana@example.com",
});
```

Python has the same calls on `client.ontology`: `suggestions()`,
`suggestion_create()`, `suggestion_accept()` and `suggestion_dismiss()`.

## From an agent

The MCP server lets an agent propose a change but not accept it:

```json
{
  "action": "suggest_ontology_change",
  "title": "Make Contractor a subclass of Person",
  "rationale": "12 tickets name contractors as assignees",
  "change": [
    { "op": "add_super_types", "entity_type": "Contractor", "super_types": ["Person"] }
  ]
}
```

Send it to `lbb_configure`. `lbb_inspect` with `action: "ontology_suggestions"`
lists the suggestions and their state, with `fact_count` for each one. An
agent can also send `facts` (see [Facts](#facts)); `change` may then be
empty.

## From an integration

A new [integration](https://docs.littlebigbrain.com/guides/integrations/) reviews first. It files one
suggestion per class that needs a change. Streams that already fit keep
syncing. After you accept a suggestion, the next sync writes the records that
waited. If you rename a class or a property before you accept, the
integration uses your name.

## Identities

Two sources can feed one class. Then one person can exist twice, once per
source. A suggestion can list such records as `identities`. Each identity
holds 2 to 10 records, a reason and a confidence.

```json
{
  "members": [
    { "type": "Person", "name": "Ada Lovelace", "key": "hubspot-main:contacts:1" },
    { "type": "Person", "name": "Ada Lovelace", "key": "linear-main:users:9" }
  ],
  "reason": "email ada@acme.test",
  "confidence": 1
}
```

A record is named by its class and its import key. The first record is the
canonical one.

Accepting links each later record to the first with a `SAME_AS` edge. In
SPARQL it is `https://littlebigbrain.com/r/same_as`; follow it to read the
records together.

The edge keeps each record's current name, even after a rename at the
source. A record that is not in the graph is refused, so accept never creates
one.

The ontology needs a `SAME_AS` relation. When it has none, the change must
add one. Otherwise the accept fails and writes nothing.

To link only some records, send the kept ones in `identities` on accept. Each
kept identity must be part of one proposed identity. The server keeps the
original list in `proposed_identities`.

## Facts

A producer that writes graph facts can ask a person first. A
[developer workflow](https://docs.littlebigbrain.com/guides/starter-workflows/#developer-workflows) files
the links below its confidence bar this way, with origin
`{kind: "agent", id: "starter:<name>"}`. `facts` holds what accepting
commits:

```json
{
  "triplets": [{
    "source": { "type": "Room", "name": "R 1.04" },
    "relation": "HAS_COMPONENT",
    "target": { "type": "Component", "name": "Fire door T30" },
    "confidence": 0.62,
    "evidence": { "text": "Tür T30 RS", "source_id": "doc:plan-104" }
  }],
  "entity_properties": [{ "type": "Room", "name": "R 1.04", "properties": { "area": 18.5 } }],
  "replace_owned": { "subjects": [{ "type": "Room", "name": "R 1.04" }], "relations": ["HAS_COMPONENT"] },
  "summary": "One component of room R 1.04"
}
```

`triplets` and `entity_properties` take the shapes of `POST /v1/graph/commit`.
`replace_owned` replaces the producer's owned links and fields on its
subjects in the same commit (see
[Write records as JSON](https://docs.littlebigbrain.com/sdks/http-api/#owned-outputs)).

```ts
await client.ontology.suggestions.create({
  key: "starter:rooms/r-1-04",
  title: "Link room R 1.04",
  origin: { kind: "agent", id: "starter:rooms" },
  change: [],
  facts,
});
const accepted = await client.ontology.suggestions.accept(id);
console.log(accepted.facts_commit?.commit_seq);
```

- `change` may be empty when `facts` is set.
- The facts are part of the proposal. Filing other facts under the same
  `key` revises the suggestion.
- Accepting commits the facts after the change and after any identity links,
  with the idempotency key `ontology-suggestion:<id>:facts:r<revision>`, so a
  repeated accept writes nothing new. The suggestion records `facts_commit`:
  `commit_seq`, `request_id`, `no_op` (the facts were in the graph already)
  and `owned` (what `replace_owned` removed).
- The commit checks the facts against the ontology after the change, so they
  can use a class that the change adds. A refused commit leaves the
  suggestion open.
- At most 200 links and field rows, 64 KiB of JSON, and a summary of 500
  characters.
- On an RDF-native graph the accept writes the facts as RDF: the statements
  a records graph shows for them, under the same IRIs.
- On an RDF-native graph the accept links identities the same way. It reads
  each member from the graph's exact view, which includes commits that are not
  published yet, so a member written a moment ago is found. A member the graph
  lacks still refuses the accept, and nothing is written.
- Dismissing writes nothing to the graph.

### Accept some of the facts

A person can leave facts out. Send `facts` with the accept: the triplets
and rows to keep, each exactly as the suggestion holds it. A fact that the
suggestion does not hold is refused with `400`, so an accept never adds a
fact. `facts.required` names rows that the other facts need, such as the
class of a new instance. Leave one out and the accept is refused; dismiss
the suggestion instead.

```ts
const item = await lbb.ontology.suggestions.get(id);
const kept = item.facts!.entity_properties!.filter((_, row) => row !== 2);
await lbb.ontology.suggestions.accept(id, { facts: { entity_properties: kept } });
```

The suggestion keeps the proposed facts in `proposed_facts`. An accept of
a superseded suggestion fails with `409 suggestion_superseded`.

The `review` workflow files one item per instance, and writes the kept
facts after the accept: see
[Review proposed facts](https://docs.littlebigbrain.com/guides/starter-workflows/#review-proposed-facts).

## Routes

| Route | Does |
| --- | --- |
| `POST /v1/ontology/suggestions` | file or revise by `key`, then dry-run |
| `GET /v1/ontology/suggestions` | list, newest first, with counts per status |
| `GET /v1/ontology/suggestions/detail?suggestion_id=` | one suggestion in full |
| `POST /v1/ontology/suggestions/validate?suggestion_id=` | dry-run again |
| `POST /v1/ontology/suggestions/accept?suggestion_id=` | apply the change, or an edited one, then link the identities and commit the facts |
| `POST /v1/ontology/suggestions/dismiss?suggestion_id=` | decline, with a reason |
| `POST /v1/ontology/suggestions/supersede?suggestion_id=` | the producer withdraws it |
| `POST /v1/ontology/suggestions/comment?suggestion_id=` | add a comment |
| `GET /v1/ontology/suggestions/patterns` | the suggestions grouped by pattern, the largest first, with samples and every id |
| `POST /v1/ontology/suggestions/accept-batch` | accept up to 500 suggestions, each as one accept does |
| `POST /v1/ontology/suggestions/dismiss-batch` | dismiss up to 500 suggestions, each as one dismiss does |

Accept is safe to repeat. A second accept returns the accepted suggestion.

## Decide a pattern at once

A workflow that reads documents files many suggestions that ask the same
question: link the span `1.2.3` to the heading `Section 1.2.3`, set the
maximum pressure of a pump from "Operating pressure | ≤ 3.0 bar". The server groups
them by pattern: the same producer, document, kind of fact and shape of
text, with each run of digits written as `#`. You check a few samples and
decide the whole pattern.

```ts
const { patterns } = await client.ontology.suggestions.patterns({ status: "open" });
const [largest] = patterns;
// For example: Link "#.#.#" in manual.pdf to Section #.#.# (REFERS_TO)
console.log(largest.pattern.label, largest.count, largest.confidence);

const result = await client.ontology.suggestions.acceptBatch({
  suggestion_ids: largest.suggestion_ids,
  shown: largest.samples.map((sample) => sample.suggestion_id),
  pattern: largest.pattern.key,
});
console.log(result.accepted, result.skipped);
```

Each suggestion is decided as one accept or dismiss decides it. A
suggestion its producer replaced in the meantime is skipped, and the result
names the one in its place. An accept of many writes their facts in one
commit (up to 1,000 facts each) and their changes in one evolve. Each
decided suggestion records the batch, and which samples you saw.
