Activate and restore a NAMS ontology
We will capture the workspace’s active ontology version, clone the healthcare template, activate and inspect a strict revision, then restore the exact previous binding. After restoration is verified, we will delete only the created clone and confirm its absence in a fresh process.
|
|
This lesson uses neo4j-agent-memory 0.7.0 from PyPI and the complete
programs on this page in a POSIX shell. It uses the NAMS ontology API; Aura
connection credentials cannot replace a NAMS workspace key. The checks below
verify activation and restoration against your hosted workspace.
Before you begin
-
Python 3.10 or newer and a POSIX shell.
-
A dedicated NAMS test workspace with its workspace-scoped data key and permission to create, activate and delete lesson ontologies.
-
A
healthcaretemplate and an active ontology with authoritative version metadata. The program stops before mutation if either is unavailable.
Use a workspace reserved for this exercise. Activation changes the schema binding for the entire workspace, so no other client or ontology editor may use it during the lesson. Establish who can recover that binding if the process is interrupted. A valid workspace key grants data access; it does not by itself establish isolation or permission to disrupt another application.
Create a local folder and virtual environment for the examples on this page. The commands reuse an existing environment without changing its files:
mkdir -p ~/agent-memory-tutorials
cd ~/agent-memory-tutorials
if [ -e .venv ]; then
printf '%s\n' 'Using the existing virtual environment.'
else
python3 -m venv .venv
fi
source .venv/bin/activate
Expected: ~/agent-memory-tutorials is your working directory and its virtual environment is active. Install the published SDK with the command below.
Each complete code block labelled Save as names a file to create in this folder using your editor. Copy the entire block, including imports and the entry point. Expand each helper disclosure and use Copy code to copy its full source. Keep all files together so their imports resolve.
When continuing from another tutorial or guide, retain the existing environment, configuration, session files, and .tutorial-state/. Reuse unchanged helper files; compare an existing file before replacing it, and finish any pending cleanup or recovery before changing the code that owns its state.
python -m pip install 'neo4j-agent-memory[nams]==0.7.0'
python -c "from importlib.metadata import version; \
import neo4j_agent_memory; \
assert version('neo4j-agent-memory') == '0.7.0'; \
print('SDK 0.7.0 import verified')"
Expected: SDK 0.7.0 import verified. Configure the dedicated workspace:
export MEMORY_API_KEY="replace-with-the-dedicated-workspace-key"
export MEMORY_ENDPOINT="https://memory.neo4jlabs.com/v1"
unset MEMORY_WORKSPACE_ID
python -c "from neo4j_agent_memory import NamsSettings; \
s=NamsSettings(); \
print(s.backend, s.nams.endpoint)"
Expected: nams https://memory.neo4jlabs.com/v1. This checks configuration;
the workspace key selects the dedicated workspace. Keep the same credentials
for every command and run from ~/agent-memory-tutorials with the virtual
environment active. See Authentication for
credential categories.
Before seeding, identify the workspace name or ID in the dashboard and its
responsible operator. Supply those non-secret labels in the seed command below.
They are saved as operator notes; they do not change request routing or prove
workspace identity. Because this lesson unsets MEMORY_WORKSPACE_ID, the
ledger’s selector can be null: the workspace key selects the workspace.
Never put a key in either note.
Step 1: Understand the restoration guard
See Ontologies for what an ontology version and active binding are; this step covers only what the exercise checks.
The private .tutorial-state/ontology.json file records the endpoint and
credential identity, original active version, each returned clone/revision ID,
and unfinished operations. It is written before advancing to the next stage.
Keep it out of version control and do not remove it to bypass an incomplete
exercise.
The guard captures the version actually bound to the workspace, which can be older than the ontology’s latest revision. Missing or contradictory binding metadata is an error; the program never chooses the latest revision as a restoration fallback.
SDK 0.7.0’s client.ontology.get_active() reports the bound revision, but it
returns None metadata, rather than failing, when the service response carries
no version record. (Releases before 0.7.0 could report the latest revision
instead of the bound one.) This tutorial therefore reads the binding directly
through its read_active_binding() helper, which validates it and stops before
any mutation when it is missing or inconsistent. The remaining ontology
operations use the published SDK.
Restoration precedes clone deletion. If restoration or its readback fails, the clone and local state are retained for recovery. If another client changes the active binding unexpectedly, the program stops instead of overwriting that client’s work.
Step 2: Read the complete exercise
Create the shared state and restoration helpers below, then save the complete
ontology_quickstart.py program. Keep all three files in
~/agent-memory-tutorials:
Copy these files into the same lesson folder. The final row is the program we will run; the other files are tutorial support, not SDK APIs. Reuse a helper only when its contents match. Finish pending cleanup/recovery before replacing code that owns an existing ledger.
| File | Purpose | Continuing reader |
|---|---|---|
|
Private ledger and local inspection |
Reuse unchanged |
|
Authoritative active binding, restoration and bounded polling |
Reuse unchanged |
|
The learner-facing activation and recovery commands |
New lesson file |
Open the helper below and save its complete source as hosted_tutorial_state.py in ~/agent-memory-tutorials.
Show hosted_tutorial_state.py
hosted_tutorial_state.py"""Private, restartable state for the hosted REST tutorials; no I/O at import."""
from __future__ import annotations
import hashlib
import json
import os
import re
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit
from uuid import uuid4
def read_state(path):
"""Read a local ledger without credentials, SDK imports, or service access."""
path = Path(path)
if path.is_symlink():
raise RuntimeError("Refusing a symlink for tutorial state")
data = json.loads(path.read_text())
if not isinstance(data, dict) or data.get("schema_version") != 1:
raise RuntimeError("Unsupported or incomplete tutorial state")
if not isinstance(data.get("identity"), dict):
raise RuntimeError("Missing tutorial identity")
if (
not isinstance(data.get("resources"), dict)
or not data.get("run_token")
or "pending" not in data
):
raise RuntimeError("Incomplete tutorial state")
if any(not isinstance(entries, dict) for entries in data["resources"].values()):
raise RuntimeError("Invalid resource ledger")
if any(
not isinstance(entry, dict)
for entries in data["resources"].values()
for entry in entries.values()
):
raise RuntimeError("Invalid resource disposition")
datetime.fromisoformat(data["started_at"])
return data
def new_run_status(data):
"""Classify recorded evidence only; never infer absence from an empty ledger."""
if data.get("pending"):
return "needs_reconciliation"
resources = data["resources"]
entries = [entry for group in resources.values() for entry in group.values()]
if (
data.get("remote_write_started") is False
and not entries
and not any(data.get(key) for key in ("conversation_id", "run_id", "skill_id"))
and not data.get("ontology", {}).get("clone_id")
):
return "no_remote_write_started"
disposed = data.get("cleanup_complete") is True or (
data.get("ontology", {}).get("status") == "complete"
and data["ontology"].get("restored") is True
and data["ontology"].get("deleted") is True
)
if (
disposed
and entries
and all(entry.get("status") in {"deleted", "deleted_with_ontology"} for entry in entries)
):
return "recorded_disposition_complete"
return "owner_disposition_required"
# Restoration target and exercise outcome shown by inspect-file.
ONTOLOGY_SUMMARY_KEYS = (
"previous",
"strict",
"clone_id",
"template",
"status",
"restored",
"deleted",
"exercise_error",
)
def inspect_file(path):
"""Return a redacted local summary; this never authenticates or enables writes."""
data = read_state(path)
endpoint = urlsplit(str(data["identity"].get("endpoint", "")))
# Do not expose credentials/query parameters even from a manually altered file.
endpoint = urlunsplit(
(endpoint.scheme, endpoint.netloc.rsplit("@", 1)[-1], endpoint.path, "", "")
)
summary = {
"local_only": True,
"service_checked": False,
"lesson": data.get("lesson"),
"run_token": data["run_token"],
"started_at": data["started_at"],
"identity": {
"endpoint": endpoint,
"workspace_id": data["identity"].get("workspace_id"),
},
"operator_context": data.get("operator_context", {}),
"pending": data.get("pending"),
"new_run_status": new_run_status(data),
"resources": {
kind: {
resource_id: {
key: entry[key] for key in ("status", "owned", "disposition") if key in entry
}
for resource_id, entry in entries.items()
}
for kind, entries in data["resources"].items()
},
}
ontology = data.get("ontology")
if isinstance(ontology, dict):
# The owner needs the saved prior binding to restore the workspace.
# Schema documents are omitted; the IDs identify them exactly.
binding_keys = ("version_id", "ontology_id", "revision", "validation_mode")
summary["ontology"] = {
key: (
{k: value[k] for k in binding_keys if k in value}
if isinstance(value, dict)
else value
)
for key, value in ontology.items()
if key in ONTOLOGY_SUMMARY_KEYS
}
return summary
def check_new_run(path, next_state):
"""Permit a separate path only after no-write proof or recorded full disposal."""
status = new_run_status(read_state(path))
if status not in {"no_remote_write_started", "recorded_disposition_complete"}:
raise RuntimeError(
"No automatic new run: uncertain, retained, or legacy state needs owner disposition"
)
next_state = Path(next_state)
if next_state.exists() or next_state.is_symlink():
raise RuntimeError("Choose an unused state path; the previous run must remain intact")
if next_state.parent.resolve() == Path(path).parent.resolve():
raise RuntimeError("Use a separate run directory so reports and archives cannot collide")
if next_state.parent.exists() and any(next_state.parent.iterdir()):
raise RuntimeError("Choose a new or empty run directory; preserve its existing reports")
return status
def digest(text: str) -> str:
return hashlib.sha256(text.encode()).hexdigest()
def connection(settings):
"""Use the same resolved NamsSettings for SDK and tutorial HTTP requests."""
config = settings.nams
parts = urlsplit(str(config.endpoint).rstrip("/"))
if parts.scheme not in {"http", "https"} or not parts.netloc:
raise ValueError("A hosted HTTP(S) endpoint is required")
if parts.username or parts.password or parts.query or parts.fragment:
raise ValueError("Endpoint must not contain credentials, a query, or a fragment")
if config.transport_mode == "bridge" or (
config.transport_mode == "auto" and not re.search(r"/v\d+(?:/|$)", parts.path)
):
raise ValueError("These tutorials require the hosted REST API")
endpoint = urlunsplit((parts.scheme.lower(), parts.netloc.lower(), parts.path, "", ""))
key = config.api_key.get_secret_value() if config.api_key else ""
if not key:
raise ValueError("Set MEMORY_API_KEY before starting the tutorial")
# Tutorial identity must not differ between the SDK and direct HTTP calls.
headers = dict(config.headers)
if any(k.lower() in {"authorization", "x-workspace-id"} for k in headers):
raise ValueError("Use api_key/workspace_id settings, not overriding auth/workspace headers")
headers["Authorization"] = f"Bearer {key}"
if config.workspace_id:
headers["X-Workspace-Id"] = config.workspace_id
resolved = {"endpoint": endpoint, "workspace_id": config.workspace_id}
return resolved, headers, config.timeout, key
def credential_digest(key: str, salt: str) -> str:
"""Bind state to the credential that seeded it, without keeping a directly
comparable digest of that credential on disk. A per-run salt and a
deliberately slow KDF mean the stored value answers one question -- "is this
the same key as last time?" -- and is not worth attacking offline. Message
content keeps ``digest``: it is a change detector, not a secret."""
return hashlib.scrypt(
key.encode(), salt=bytes.fromhex(salt), n=16384, r=8, p=1, maxmem=64 * 1024 * 1024, dklen=32
).hex()
def identity(settings, salt=None):
resolved, _headers, _timeout, key = connection(settings)
salt = salt or os.urandom(16).hex()
return {**resolved, "credential_salt": salt, "credential_digest": credential_digest(key, salt)}
def http_client(settings, **kwargs):
import httpx
resolved, headers, timeout, _key = connection(settings)
return httpx.AsyncClient(
base_url=resolved["endpoint"] + "/", headers=headers, timeout=timeout, **kwargs
)
def private_json(path: Path, data, *, exclusive=False):
"""Write without exposing partial JSON or replacing a previous exercise."""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
if path.is_symlink():
raise RuntimeError("Refusing a symlink for tutorial state")
content = json.dumps(data, indent=2, allow_nan=False) + "\n"
if exclusive:
try:
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
except FileExistsError as exc:
raise RuntimeError("State already exists; inspect or recover the prior run") from exc
with os.fdopen(fd, "w") as output:
output.write(content)
output.flush()
os.fsync(output.fileno())
return
fd, temporary = tempfile.mkstemp(prefix=path.name + ".", dir=path.parent)
try:
with os.fdopen(fd, "w") as output:
output.write(content)
output.flush()
os.fsync(output.fileno())
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
class TutorialState:
def __init__(self, path, data):
self.path = Path(path)
self.data = data
@classmethod
def create(cls, path, settings, lesson, *, workspace_label=None, workspace_owner=None):
data = {
"schema_version": 1,
"lesson": lesson,
"identity": identity(settings),
"run_token": uuid4().hex,
"started_at": datetime.now(timezone.utc).isoformat(),
"resources": {},
"pending": None,
# Written durably by begin() before the first remote mutation.
# Missing in an older ledger means unknown, never proof of no writes.
"remote_write_started": False,
"operator_context": {
"workspace_label": workspace_label,
"workspace_owner": workspace_owner,
},
}
private_json(Path(path), data, exclusive=True)
return cls(path, data)
@classmethod
def load(cls, path, settings, lesson=None):
path = Path(path)
data = read_state(path)
stored = data["identity"]
salt = stored.get("credential_salt")
if not isinstance(salt, str) or not re.fullmatch(r"[0-9a-f]{32}", salt):
raise RuntimeError("Tutorial identity is missing its credential salt; do not reseed")
if stored != identity(settings, salt):
raise RuntimeError(
"Endpoint, workspace, or credential changed; verify ownership before recovery"
)
if lesson is not None and data.get("lesson") != lesson:
raise RuntimeError("State belongs to another tutorial")
return cls(path, data)
def save(self):
private_json(self.path, self.data)
def record(self, kind, resource_id, **metadata):
if not isinstance(resource_id, str) or not resource_id.strip():
raise RuntimeError(f"Missing returned {kind} ID; retained pending operation")
resources = self.data["resources"].setdefault(kind, {})
resources.setdefault(resource_id, {"status": "retained"}).update(metadata)
self.save()
def begin(self, operation):
if self.data.get("pending"):
raise RuntimeError("Unresolved write; inspect the retained state before retrying")
self.data["remote_write_started"] = True
self.data["pending"] = operation
self.save()
def finish(self):
self.data["pending"] = None
self.save()
def inspect(self):
data = json.loads(json.dumps(self.data))
data["identity"].pop("credential_salt", None)
data["identity"].pop("credential_digest", None)
return data
def remember_message(state, message):
role = getattr(message.role, "value", message.role)
state.record("message", str(message.id), content_sha256=digest(message.content), role=role)
async def verify_messages(client, state):
"""Read-only exact-ID/content verification; safe to run in a fresh process."""
expected = state.data["resources"].get("message", {})
if not expected or not state.data.get("conversation_id"):
raise RuntimeError("No complete message seed to verify")
history = await client.short_term.get_conversation(state.data["conversation_id"])
actual = {str(m.id): m for m in history.messages}
if set(actual) != set(expected):
raise RuntimeError("Conversation has missing or unrecorded messages; retained state")
for message_id, entry in expected.items():
message = actual[message_id]
role = getattr(message.role, "value", message.role)
if digest(message.content) != entry["content_sha256"] or role != entry["role"]:
raise RuntimeError(f"Message readback differs: {message_id}")
print(f"Verified: exact IDs, roles and content for {len(expected)} stored messages")
def main(argv=None):
"""Local diagnostics only; importing this module still performs no I/O."""
import argparse
parser = argparse.ArgumentParser(description="Inspect hosted lesson state without credentials")
commands = parser.add_subparsers(dest="command", required=True)
inspect = commands.add_parser("inspect-file", help="Print a redacted local summary; no network")
inspect.add_argument("state", type=Path)
retry = commands.add_parser("check-new-run", help="Check a separate path; preserve both files")
retry.add_argument("state", type=Path)
retry.add_argument("--next-state", type=Path, required=True)
args = parser.parse_args(argv)
if args.command == "inspect-file":
print(json.dumps(inspect_file(args.state), indent=2))
else:
status = check_new_run(args.state, args.next_state)
print(f"Recorded new-run check: {status}; original state preserved")
print(f"Use --state {args.next_state} only after rechecking the lesson prerequisites")
print("No service state was checked and no new file was created")
if __name__ == "__main__":
main()
Open the helper below and save its complete source as hosted_tutorial_helpers.py in ~/agent-memory-tutorials.
Show hosted_tutorial_helpers.py
hosted_tutorial_helpers.py"""Bounded control flow shared by the hosted documentation exercises.
These helpers know only the documented run outcomes and SDK ontology methods.
They do not contact a service at import time.
"""
from __future__ import annotations
import asyncio
import json
import time
from collections.abc import Awaitable, Callable
from contextlib import asynccontextmanager
from typing import Any
async def poll_skill_run(
fetch: Callable[[], Awaitable[dict[str, Any]]],
*,
timeout: float = 120.0,
interval: float = 2.0,
clock: Callable[[], float] = time.monotonic,
sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
) -> dict[str, Any]:
"""Wait through queued/running; return a documented outcome or fail closed."""
if timeout <= 0 or interval <= 0:
raise ValueError("timeout and interval must be positive")
deadline = clock() + timeout
while True:
remaining = deadline - clock()
if remaining <= 0:
raise TimeoutError("Skill run did not settle before the tutorial deadline")
run = await asyncio.wait_for(fetch(), timeout=remaining)
outcome = run.get("outcome")
if outcome in {"Created", "Withheld", "Failed"}:
if outcome == "Created" and not run.get("skillId"):
raise RuntimeError("Created run is missing its returned skillId")
return run
if outcome is not None or run.get("status") not in {"queued", "running"}:
raise RuntimeError(f"Unrecognized skill run state: {run!r}")
await sleep(min(interval, max(0.0, deadline - clock())))
def ontology_binding(active):
"""Capture authoritative IDs, mode and schema; never infer a latest revision."""
values = {
"version_id": active.version_id,
"ontology_id": active.ontology_id,
"revision": active.revision,
"validation_mode": active.validation_mode,
}
if (
not values["version_id"]
or not values["ontology_id"]
or type(values["revision"]) is not int
or values["revision"] < 1
or values["validation_mode"] not in {"permissive", "strict"}
):
raise RuntimeError("No restorable authoritative active binding; no ontology was changed")
values["document"] = active.document.model_dump(mode="json")
return values
async def read_active_binding(http):
"""Read a restorable binding directly from the public REST response.
SDK 0.7.0 get_active() reports the bound version but returns None metadata
when a response has no version record (releases before 0.7.0 inferred it
from the latest revision). This reader requires the actual binding returned
by /ontologies/active, validated against the released public document
model, and fails closed rather than substituting a list lookup.
"""
from neo4j_agent_memory.nams import OntologyDocument
response = await http.get("ontologies/active")
response.raise_for_status()
payload = response.json()
version = payload.get("version") if isinstance(payload, dict) else None
if (
not isinstance(version, dict)
or any(
not isinstance(version.get(key), str) or not version[key].strip()
for key in ("id", "ontology_id")
)
or type(version.get("revision")) is not int
or version["revision"] < 1
or version.get("validation_mode") not in {"permissive", "strict"}
):
raise RuntimeError("No restorable authoritative active binding; no ontology was changed")
def document(value):
if isinstance(value, str):
value = json.loads(value)
return OntologyDocument.model_validate(value).model_dump(mode="json")
try:
active_document = document(payload.get("ontology"))
version_document = version.get("schema_json")
if version_document is not None and document(version_document) != active_document:
raise ValueError("Active and version schemas differ")
except (ValueError, TypeError) as exc:
raise RuntimeError("Invalid or conflicting active ontology schema; retained state") from exc
return {
"version_id": version["id"],
"ontology_id": version["ontology_id"],
"revision": version["revision"],
"validation_mode": version["validation_mode"],
"document": active_document,
}
def _version_binding(version):
from types import SimpleNamespace
if version.document is None:
raise RuntimeError("Returned version has no inspectable schema; retained clone")
return ontology_binding(
SimpleNamespace(
version_id=version.id,
ontology_id=version.ontology_id,
revision=version.revision,
validation_mode=version.validation_mode,
document=version.document,
)
)
async def _require_binding(read_active, expected):
actual = await read_active()
if actual != expected:
raise RuntimeError("Active binding changed or readback differs; retained state and clone")
return actual
async def _clone_absent(ontology, clone_id):
from neo4j_agent_memory.core.exceptions import NotFoundError
try:
await ontology.get(clone_id)
except NotFoundError:
return True
return False
async def verify_ontology_restoration(ontology, state, *, read_active):
"""Fresh, read-only verification of the saved binding and exact clone absence."""
run = state.data.get("ontology", {})
if not run.get("previous"):
raise RuntimeError("State has no captured prior binding")
await _require_binding(read_active, run["previous"])
clone_id = run.get("clone_id")
if not clone_id or not await _clone_absent(ontology, clone_id):
raise RuntimeError("Clone absence is not verified; inspect or recover this run")
if state.data.get("pending"):
raise RuntimeError(
"An operation remains unresolved; run recover before claiming completion"
)
print(f"Verified: original version {run['previous']['version_id']} and mode restored")
print(f"Verified: exercise clone {clone_id} is absent")
async def recover_ontology(ontology, state, *, read_active):
"""Restore only a recognized binding, then delete the proven-owned clone.
An interrupted clone with no returned ID cannot be reconciled automatically.
A concurrent editor's binding is never replaced. There is no server-side
compare-and-swap here, so the exercise requires exclusive ontology editing.
"""
from neo4j_agent_memory.core.exceptions import NotFoundError
run = state.data.get("ontology", {})
previous = run.get("previous")
if not previous:
raise RuntimeError("State has no captured prior binding; no recovery writes were made")
current = await read_active()
if current != previous and current != run.get("strict"):
raise RuntimeError("Active binding changed unexpectedly; retained state and clone")
clone_id = run.get("clone_id")
pending = state.data.get("pending")
if not clone_id:
if pending:
raise RuntimeError(
"Clone outcome is uncertain with no returned ID; inspect retained state"
)
# A prerequisite failed before the first write.
print("Verified: original binding unchanged; no clone was created")
return
owned = state.data["resources"].get("ontology", {}).get(clone_id, {})
if owned.get("created_by_run") is not True:
raise RuntimeError("Clone ownership is not established; no recovery writes were made")
if pending:
if not isinstance(pending, dict) or pending.get("operation") not in {
"clone",
"update",
"activate",
"restore",
"delete",
}:
raise RuntimeError("Unrecognized pending operation; inspect retained state")
# Keep uncertainty recorded until deleting the known clone resolves all
# its revisions. Never replay a clone or an update with an unknown result.
run.setdefault("interrupted_operations", []).append(pending)
state.finish()
if current != previous:
state.begin({"operation": "restore", "version_id": previous["version_id"]})
await ontology.activate(previous["version_id"])
await _require_binding(read_active, previous)
state.finish()
else:
await _require_binding(read_active, previous)
run["restored"] = True
state.save()
print(f"Restored active version: {previous['version_id']} ({previous['validation_mode']})")
# Recheck immediately before deletion. This detects observed concurrent
# changes; it cannot make an unversioned service operation atomic.
await _require_binding(read_active, previous)
if not await _clone_absent(ontology, clone_id):
state.begin({"operation": "delete", "ontology_id": clone_id})
try:
await ontology.delete(clone_id)
except NotFoundError:
pass # A retry still requires the independent GET below.
if not await _clone_absent(ontology, clone_id):
raise RuntimeError(f"Deletion not verified; retained clone {clone_id}")
state.finish()
state.record("ontology", clone_id, status="deleted")
for version_id, entry in state.data["resources"].get("ontology_version", {}).items():
if entry.get("ontology_id") == clone_id:
entry["status"] = "deleted_with_ontology"
run["deleted"] = True
run["status"] = "complete"
state.save()
print(f"Deleted exercise clone: {clone_id} (absence verified)")
@asynccontextmanager
async def temporary_strict_ontology(ontology: Any, template: str, state, *, read_active):
"""Persist the restoration target before mutation and retain failed recovery."""
if state.data.get("ontology") or state.data.get("pending"):
raise RuntimeError("Ontology exercise already started; inspect or recover its state")
previous = await read_active()
catalog = await ontology.list()
if not any(item.name == template and item.is_system for item in catalog):
raise RuntimeError("Required system template is absent; no ontology was changed")
existing_ids = {item.id for item in catalog}
state.data["ontology"] = {"previous": previous, "template": template, "status": "started"}
state.save()
print(f"Previous active version: {previous['version_id']} ({previous['validation_mode']})")
try:
state.begin({"operation": "clone", "template": template})
clone = await ontology.clone(template)
run = state.data["ontology"]
run["clone_id"] = clone.ontology_id
state.record(
"ontology", clone.ontology_id, created_by_run=clone.ontology_id not in existing_ids
)
state.record("ontology_version", clone.id, ontology_id=clone.ontology_id)
state.finish()
if clone.ontology_id in existing_ids:
raise RuntimeError(
"Clone response identifies a preexisting ontology; ownership is unknown"
)
if clone.document is None:
raise RuntimeError("Clone has no inspectable schema")
print(f"Exercise clone: {clone.ontology_id}")
state.begin({"operation": "update", "ontology_id": clone.ontology_id})
strict = await ontology.update(clone.ontology_id, clone.document, validation_mode="strict")
state.record("ontology_version", strict.id, ontology_id=strict.ontology_id)
run["strict"] = _version_binding(strict)
state.finish()
if strict.ontology_id != clone.ontology_id or strict.validation_mode != "strict":
raise RuntimeError("Strict revision response differs from the requested clone and mode")
await _require_binding(read_active, previous)
state.begin({"operation": "activate", "version_id": strict.id})
await ontology.activate(strict.id)
await _require_binding(read_active, run["strict"])
state.finish()
print(f"Activated strict version: {strict.id}")
yield strict
except BaseException as exercise_error:
state.data["ontology"]["exercise_error"] = type(exercise_error).__name__
state.save()
try:
await recover_ontology(ontology, state, read_active=read_active)
except BaseException as recovery_error:
raise recovery_error from exercise_error
raise
else:
await recover_ontology(ontology, state, read_active=read_active)
ontology_quickstart.py"""Activate and inspect a strict revision, then restore the exact prior binding."""
from __future__ import annotations
import argparse
import asyncio
import json
from pathlib import Path
from hosted_tutorial_helpers import (
read_active_binding,
recover_ontology,
temporary_strict_ontology,
verify_ontology_restoration,
)
from hosted_tutorial_state import TutorialState, http_client
async def exercise(client, state, *, read_active):
async with temporary_strict_ontology(
client.ontology, "healthcare", state, read_active=read_active
) as strict:
labels = [item.label for item in strict.document.entity_types]
print(f"Strict schema entity labels: {', '.join(labels)}")
print("Verified: exact active revision, strict mode, and schema readback")
async def main(argv=None):
from neo4j_agent_memory import NamsSettings, connect
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("command", choices=["seed", "inspect", "verify", "recover", "cleanup"])
parser.add_argument("--state", type=Path, default=Path(".tutorial-state/ontology.json"))
parser.add_argument(
"--workspace-label", help="Operator-recorded workspace name/ID; not routing"
)
parser.add_argument("--workspace-owner", help="Operator responsible for resource disposition")
args = parser.parse_args(argv)
settings = NamsSettings()
state = (
TutorialState.create(
args.state,
settings,
"ontology",
workspace_label=args.workspace_label,
workspace_owner=args.workspace_owner,
)
if args.command == "seed"
else TutorialState.load(args.state, settings, "ontology")
)
if args.command == "inspect":
print(json.dumps(state.inspect(), indent=2))
return
client = await connect(settings)
try:
async with http_client(settings) as http:
async def read_active():
return await read_active_binding(http)
if args.command == "seed":
await exercise(client, state, read_active=read_active)
elif args.command == "verify":
await verify_ontology_restoration(client.ontology, state, read_active=read_active)
else:
await recover_ontology(client.ontology, state, read_active=read_active)
finally:
await client.close()
if __name__ == "__main__":
asyncio.run(main())
Before the first service operation, check the assembled files offline:
python -m py_compile \
hosted_tutorial_state.py \
hosted_tutorial_helpers.py \
ontology_quickstart.py
python ontology_quickstart.py --help
Expected: compilation prints nothing and exits successfully; --help prints
the command choices. Compilation catches syntax errors; the CLI check also
loads the local imports. Neither command authenticates, contacts NAMS or
creates tutorial state. Fix a missing or truncated file before continuing.
This exercise verifies activation and configuration. It creates no Patient or
other entity and does not test unknown-label rejection. The Python entity
adapter currently maps an unrecognized entity_type to custom, so passing
an invented label through that API would not establish the intended ontology
constraint. See Ontology API for the separate
schema and version contracts.
Step 3: Activate, inspect and restore
python ontology_quickstart.py seed --state .tutorial-state/ontology.json \
--workspace-label 'workspace name or ID from the dashboard' \
--workspace-owner 'responsible workspace operator'
Check the reported IDs and milestones:
-
Previous active version:shows the saved version and validation mode;Exercise clone:identifies the newly created ontology. -
Activated strict version:andStrict schema entity labels:describe that clone’s active revision.Verified: exact active revision, strict mode, and schema readbackconfirms the independent active readback. -
Restored active version:matches the saved prior version and mode. -
Deleted exercise clone:identifies only the created clone, with(absence verified)after its deletion check.
Illustrative successful output (placeholder IDs, not a live-service claim):
Previous active version: <original-version-id> (permissive)
Exercise clone: <new-ontology-id>
Activated strict version: <new-version-id>
Strict schema entity labels: ...
Verified: exact active revision, strict mode, and schema readback
Restored active version: <original-version-id> (permissive)
Deleted exercise clone: <new-ontology-id> (absence verified)
The clone ID identifies an ontology; the two version IDs identify specific
revisions. Your original mode can be strict; restoration must match the saved
mode as well as the version ID. Only the next fresh-process check establishes
the later readback.
The command refuses to seed over an existing state file. A successful process exit requires the restoration and cleanup checks, not merely activation of the strict revision.
Step 4: Verify from a fresh process
python ontology_quickstart.py inspect --state .tutorial-state/ontology.json
python ontology_quickstart.py verify --state .tutorial-state/ontology.json
Inspect the saved resource disposition. The separate verify process reads the
active binding and clone status without creating or activating anything. Its
success establishes that the original version remains active and the lesson
clone is absent at that readback. It does not restart the hosted service or
prove that every strict-validation rule is enforced.
Recover an interrupted exercise
Retain the state file and inspect it before another attempt. With the same workspace credentials and exclusive use of the workspace, run:
python ontology_quickstart.py recover --state .tutorial-state/ontology.json
python ontology_quickstart.py verify --state .tutorial-state/ontology.json
Recovery uses the saved prior binding and exact created IDs. It reconciles interrupted activation, restoration or deletion when those IDs are known, and restores and verifies before deleting the clone. A clone with no returned ID, an unreconciled operation, an unexpected active binding or failed readback requires inspection by the workspace owner; do not reset the workspace, delete ontologies by a shared name, or remove the state file to suppress the failure. Keep the state until every created resource has a verified disposition.
Inspect locally after a credential change
If the original key expires or is rotated, normal lesson commands keep refusing to use its saved IDs with another credential. Use this separate local command:
python hosted_tutorial_state.py inspect-file .tutorial-state/ontology.json
It needs no key and makes no service requests. The summary omits credential
fingerprints, message digests, fixture metadata and review/provenance payloads.
It still contains resource IDs and operator labels, so keep it private. Its
local_only: true and service_checked: false mean it reports saved evidence,
not current remote state or authorization.
Give the authorized owner the workspace label, endpoint, run token, exact resource IDs and pending operation through your agreed private channel. The owner must establish the actual workspace and reconcile those resources with a valid credential. Do not edit the identity hash, substitute a key in the ledger, or remove state to unlock a write. The helper cannot migrate a run to a new key or supply an administrative deletion route.
For this lesson, the summary also has an ontology section. Its previous
entry is the binding to restore: the version ID to reactivate and its
validation mode. clone_id and strict identify the exercise clone and its
strict revision. Give the owner that section with the resource IDs; schema
documents are omitted.
Keep the old run and start a separate exercise
See Recover from an interrupted tutorial run to check the recorded state before reseeding. For a pending or uncertain mutation with known IDs, prefer the recover command above over owner reconciliation.
For a prerequisite failure before the first remote write, the new ledger’s
remote_write_started: false, empty resources and absent pending operation
establish that this program did not attempt a mutation. An empty ledger alone
is not enough. Older empty ledgers without that marker, a lost returned ID, or any
uncertain write fail the check and require owner inspection.
After correcting the prerequisite, choose this unused run directory. The
first command only reads the old ledger and checks the new path. It preserves
the original file and creates nothing. && prevents seed if that check fails:
python hosted_tutorial_state.py check-new-run .tutorial-state/ontology.json \
--next-state .tutorial-state/ontology-run-2/state.json &&
python ontology_quickstart.py seed \
--state .tutorial-state/ontology-run-2/state.json \
--workspace-label 'workspace name or ID from the dashboard' \
--workspace-owner 'responsible workspace operator'
Expected before seed: Recorded new-run check: no_remote_write_started for
an unstarted run, or recorded_disposition_complete for fully disposed records.
This is a check of saved evidence, not a new service verification. The seed
still checks its own prerequisites and refuses an existing target state file.
Use the new --state path for every later command; reports and archives are
written alongside that new state. Never copy the old IDs into the new ledger.
What we learned
An ontology document, its revisions and the workspace’s active binding are separate concepts. The saved active binding, which may be older than the latest revision, defines what the exercise restores. Cleanup is complete only after restoration and removal of the created clone are checked.
Continue with a full lifecycle
The larger ontology lifecycle example covers import, revision diff and migration. Review its separate prerequisites and cleanup before running it. See Use ontologies for those tasks, Ontologies for the underlying concepts, or continue to distilling a skill after satisfying that lesson’s empty-workspace and resource-disposition requirements.
To work with an ontology on your own Neo4j database instead, the ontology extraction example runs on Bolt without API keys: it creates and activates an ontology, ingests messages with alias variation, and reads the typed relationships back. The ontology-driven extraction guide describes that workflow.