Skip to content

Run the Quaero Explorer

Run the Quaero Explorer demo app and discover Metanno’s collaborative workflow: real‑time syncing, multi‑panel editing, and simple persistence.

Prerequisites

Reuse the same environment you prepared in the New project tutorial. You should already have a working Python environment with Pret, Metanno, and EDS‑NLP installed.

The script downloads the Quaero FrenchMed dataset on first run, so an Internet access is required.

Create a new file named quaero.py in your project and copy the contents from the official example https://github.com/percevalw/metanno/blob/main/examples/quaero.py.

Run the app

From a terminal. The server will start at http://localhost:5000.

python quaero.py --port 5000

Or in JupyterLab, run the following cell:

from quaero import app

app()

Code breakdown

Below, we break down the script.

1) Load the data

We first fetch and extracts the dataset if it isn’t already present, and read the BRAT standoff annotations using EDS-NLP. We then build two Python lists: notes and entities with the fields we’ll display and edit.

download_quaero()

data = edsnlp.data.read_standoff(
    DOWNLOAD_DIR / "QUAERO_FrenchMed/corpus",
    span_setter="entities",
    notes_as_span_attribute="cui",
)
notes = [
    {
        "note_id": doc._.note_id,
        "note_text": doc.text,
        "seen": False,
    }
    for doc in data
]
entities = [
    {
        "id": f"#-{doc._.note_id}-{e.start_char}-{e.end_char}-{e.label_}",
        "note_id": doc._.note_id,
        "text": str(e),
        "begin": e.start_char,
        "end": e.end_char,
        "label": e.label_,
        "concept": e._.cui,
    }
    for doc in data
    for e in sorted(doc.spans["entities"])
]

2) Check unique entity IDs

The Table component requires unique IDs for each row. We check if the entity IDs are unique and raise an error if not, or deduplicate them automatically.

if deduplicate:
    entities = list({v["id"]: v for v in entities}.values())
else:
    counter = Counter(e["id"] for e in entities)
    if any(count > 1 for count in counter.values()):
        raise ValueError(
            "Duplicate IDs found in the dataset: "
            + ", ".join(f"{id_} (x{n})" for id_, n in counter.items() if n > 1)
        )

3) Configure labels

We compute a stable list of labels, assign a color for each, and auto‑pick a one‑letter keyboard shortcut per label.

all_labels = list(dict.fromkeys(e["label"] for e in entities))
shortcuts = set()
labels_config = {}
for i, label in enumerate(all_labels):
    labels_config[label] = {}
    if i < len(PALETTE):
        labels_config[label]["color"] = PALETTE[i]
    letter = next(c for c in label.lower() if c not in shortcuts)
    shortcuts.add(letter)
    labels_config[label]["shortcut"] = letter

4) Instantiate the manager

We create a DatasetApp (a ready‑to‑customize recipe, you may also see it referred to as a "Dataset Explorer" in the docs). Under the hood, it composes Metanno components such as Table, AnnotatedText, buttons with a bit of app logic.

app = DatasetApp(
    {
        "notes": notes,
        "entities": entities,
    },
    sync=save_path,
)

5) Build the views

We define the views that will be rendered in the app. Each view is a component that displays a specific part of the data.

notes_view = app.render_table(
    view_name="notes",
    store_key="notes",
    pkey_column="note_id",
    id_columns=["note_id"],
    first_columns=["note_id", "seen", "note_text"],
    editable_columns=[],
    categorical_columns=[],
    hidden_columns=[],
    style={"--min-notebook-height": "300px"},
)
entities_view = app.render_table(
    view_name="entities",
    store_key="entities",
    pkey_column="id",
    first_columns=["id", "note_id", "text", "label", "concept", "begin", "end"],
    id_columns=["id", "note_id"],
    editable_columns=["label", "concept"],
    categorical_columns=["label", "concept"],
    style={"--min-notebook-height": "300px"},
)
note_text_view = app.render_text(
    view_name="note_text",
    # Where to look for text data in the app data
    store_text_key="notes",
    text_column="note_text",
    text_pkey_column="note_id",
    # Where to look for spans data in the app data
    store_spans_key="entities",
    spans_pkey_column="id",
    style={"--min-notebook-height": "300px"},
    labels=labels_config,
)

6) Assemble everything

Panels can be resized, rearranged (drag the tab handles), or hidden by docking into another panel’s tab bar. The note panel header is customized to display the current note id.

app_state = app.state
app_data = app.data

@component
def NoteHeader():
    doc_idx = use_store_snapshot(app_state["notes"])["last_idx"]

    if doc_idx is None:
        return "Note"
    return f"Note ({app_data['notes'][doc_idx]['note_id']})"

layout = div(
    Layout(
        Panel(div(Markdown(DESC), style={"margin": "10px"}), key="Description"),
        Panel(notes_view, header="Notes", key="notes"),
        Panel(entities_view, header="Entities", key="entities"),
        Panel(note_text_view, header=NoteHeader(), key="note_text"),
        # Describe how the panels should be arranged by default
        default_config={
            "kind": "row",
            "children": [
                "Description",
                {"children": ["notes", "entities"]},
                "note_text",
            ],
        },
        collapse_tabs_on_mobile=["note_text", "Description", "notes", "entities"],
    ),
    style={
        "background": "var(--joy-palette-background-level2, #f0f0f0)",
        "width": "100%",
        "height": "100%",
        "minHeight": "300px",
        "--sd-background-color": "transparent",
    },
)
return layout

7) Render or serve

You can either serve it or display it in a notebook, following the instructions in the previous section.

JupyterLab Tabs

In notebooks, Pret layouts cannot be "mixed" with JupyterLab’s own UI system, and will always be embedded in a single JupyterLab tab. You may prefer displaying specific views in separate cells.

Simply display the variables notes_view, entities_view, note_text_view (the return values of app.render_text and app.render_table) in different cells.

Syncing, collaboration, and saving

Metanno (via Pret) can sync app state across clients and optionally persist it.

Live sync only (no persistence)

Pass sync=True when creating the app to enable real‑time collaboration without saving to disk:

app = DatasetApp(
    {
        "notes": notes,
        "entities": entities,
    },
    sync=True,
)

Open the same notebook twice or the same app URL in two tabs: edits in one tab are mirrored to the other.

Live sync and saving to a file

Provide a file path to append every change to an on‑disk log:

app = DatasetApp(
    {
        "notes": notes,
        "entities": entities,
    },
    sync="quaero.bin",
)

Now changes are saved on disk, and multiple servers/kernels can collaborate by pointing to the same file.

What is the saved format?

It’s a compact, binary, append‑only log of user mutations: you cannot read it directly.

To inspect it, you can create store synced with the file and read current state.

from pret.store import create_store

store = create_store(sync="quaero.bin")
pure_py_object = store.to_py()

To export to other formats, write a small exporter from your in‑memory data (app.data). We recommend using EDS‑NLP data connectors since it supports several data formats and schemas.