Build a knowledge graph from documents
We will extract candidate entities and relationships from two fictional business documents, store their message provenance, inspect the graph after restarting Python, and pass that evidence to a chat model. We will inspect model output rather than assume every extraction is correct.
This lesson uses neo4j-agent-memory 0.7.0 from PyPI, the complete programs on this page, and a dedicated Neo4j AuraDB instance over Bolt. Offline contract tests exercise their assembly and control flow; provider and database success still need to be observed in your environment.
Before you begin
-
Python 3.10 or newer and a POSIX shell such as Bash or Zsh.
-
An OpenAI API key with access to
text-embedding-3-smalland a chat model. -
A Neo4j Aura account with capacity for a dedicated AuraDB Free instance.
The embedding and chat-model calls send the supplied text to OpenAI. This exercise uses fictional data. Neo4j Agent Memory is an experimental Neo4j Labs project; see backend capabilities before adapting the example to NAMS.
You also need an OpenAI chat-model ID and disk space/network access for the GLiNER2.5 model download, about 407 MB for the default fastino/gliner2.5-base-v1 checkpoint. CPU inference and the first download can take several minutes.
1. Install the lesson dependencies
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[openai,gliner2]==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. Keep agent-memory-tutorials as your working directory for every command below.
Create and check the local files
Create each file below in ~/agent-memory-tutorials. All complete sources follow this manifest. On a first visit every file is new; when returning, reuse unchanged helpers and compare their contents before replacing them.
| File | Purpose | Returning reader |
|---|---|---|
|
Read explicit Aura credentials; the database defaults to |
Reuse unchanged |
|
Probe the selected database and close the driver. |
Reuse unchanged |
|
Construct the explicit Bolt and OpenAI embedding settings. |
Reuse unchanged |
|
Extract the supplied documents, inspect provenance, and answer from evidence. This is the lesson entry point. |
New for this lesson; retain it with unfinished state |
The manifest above names this lesson’s entry point and its helpers. Save every file it lists, then run the offline assembly checks that follow the sources before the lesson commands.
Open the helper below and save its complete source as aura_connection.py in ~/agent-memory-tutorials.
Show aura_connection.py
aura_connection.py"""Read the Aura connection exported by the documentation setup commands."""
import os
class AuraConfigurationError(ValueError):
"""Missing or invalid tutorial settings, with no credential values in errors."""
def aura_config():
required = ("NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD")
missing = [name for name in required if not os.environ.get(name, "").strip()]
if missing:
raise AuraConfigurationError("Export the Aura connection settings: " + ", ".join(missing))
uri = os.environ["NEO4J_URI"]
if not uri.startswith("neo4j+s://"):
raise AuraConfigurationError("NEO4J_URI must use the Aura neo4j+s:// connection scheme")
database = os.environ.get("NEO4J_DATABASE", "neo4j")
if not database.strip():
raise AuraConfigurationError("NEO4J_DATABASE must not be empty")
return {
"uri": uri,
"username": os.environ["NEO4J_USERNAME"],
"password": os.environ["NEO4J_PASSWORD"],
"database": database,
}
Open the helper below and save its complete source as wait_for_tutorial_neo4j.py in ~/agent-memory-tutorials.
Show wait_for_tutorial_neo4j.py
wait_for_tutorial_neo4j.py"""Check the exported Aura connection, waiting at most three minutes."""
import asyncio
import sys
from aura_connection import AuraConfigurationError, aura_config
from neo4j import AsyncGraphDatabase
from neo4j.exceptions import DriverError, Neo4jError, ServiceUnavailable, SessionExpired
async def wait_until_ready(timeout=180):
if timeout <= 0:
raise ValueError("Readiness timeout must be positive")
config = aura_config()
async def probe(driver):
while True:
try:
await driver.verify_connectivity()
records, _, _ = await driver.execute_query(
"RETURN 1 AS ready", database_=config["database"], routing_="r"
)
if len(records) != 1 or records[0]["ready"] != 1:
raise RuntimeError("Unexpected readiness query result")
return
except (ServiceUnavailable, SessionExpired):
await asyncio.sleep(2)
async with AsyncGraphDatabase.driver(
config["uri"],
auth=(config["username"], config["password"]),
connection_timeout=min(10, timeout),
connection_acquisition_timeout=min(10, timeout),
) as driver:
await asyncio.wait_for(probe(driver), timeout=timeout)
def main():
try:
asyncio.run(wait_until_ready())
except AuraConfigurationError as error:
print(str(error), file=sys.stderr)
raise SystemExit(1) from None
except (asyncio.TimeoutError, DriverError, Neo4jError) as error:
# Driver exceptions can include connection details. Report the category
# and next action without echoing credentials or a raw server message.
print(
f"Neo4j Aura readiness failed ({type(error).__name__}). "
"Check that the instance is Running, the exported NEO4J_* values "
"match its credentials, and your network allows the connection.",
file=sys.stderr,
)
raise SystemExit(1) from None
print("Verified: Neo4j Aura answered the readiness query")
if __name__ == "__main__":
main()
core_memory_settings.py"""Shared settings for the three Aura-backed memory tutorials."""
from aura_connection import aura_config
from neo4j_agent_memory import MemorySettings
def settings():
return MemorySettings(
backend="bolt",
neo4j=aura_config(),
embedding="openai/text-embedding-3-small",
llm=None,
extraction={"extractor_type": "none"},
resolution={"strategy": "none"},
geocoding={"enabled": False},
enrichment={"enabled": False},
)
knowledge_graph.py"""Extract a custom domain, preserve message provenance, and query stored edges."""
import argparse
import asyncio
import os
from uuid import UUID
from core_memory_settings import settings
from neo4j_agent_memory import MemoryClient
from neo4j_agent_memory.extraction import (
DomainSchema,
ExtractionResult,
GLiNER2Extractor,
LLMEntityExtractor,
)
SESSION = "docs-knowledge-sources"
DOCUMENTS = {
"leadership.txt": "Maya Chen is CEO of Northstar Robotics. Northstar Robotics is in Denver.",
"partnership.txt": "Northstar Robotics partners with Summit Research. Ravi Shah works at Summit Research.",
}
SCHEMA = DomainSchema(
name="tutorial_business",
entity_types={
"person": "A named individual",
"company": "A named business or organization",
"location": "A named city or place",
},
relation_types={
"works_at": "Person works at a company",
"ceo_of": "Person is CEO of a company",
"located_in": "Company is located in a place",
"partner_of": "Company partners with another company",
},
)
LABELS = {
"person": ("PERSON", None),
"company": ("ORGANIZATION", None),
"location": ("LOCATION", None),
}
# LLMEntityExtractor fills {entity_types} and {text}; relation names come from SCHEMA.
RELATION_PROMPT = (
"Extract named entities of these types: {entity_types}.\n"
"Then extract relations between those entities. Use only these relation types:\n"
+ "".join(f"- {name.upper()}: {text}\n" for name, text in SCHEMA.relation_types.items())
+ "Copy entity names exactly as they appear in the text. Return JSON with "
'"entities" (name, type, confidence) and "relations" '
"(source, target, relation_type, confidence).\n\nText:\n{text}"
)
EDGES_QUERY = (
"MATCH (a:Entity)-[r:RELATED_TO]->(b:Entity) "
"WHERE a.name IN $names AND b.name IN $names "
"RETURN a.name AS source, r.type AS relation, b.name AS target"
)
class DocumentExtractor:
"""Take mentions from GLiNER2.5 and schema relations from the chat model."""
def __init__(self, entity_extractor, relation_extractor):
self.entity_extractor = entity_extractor
self.relation_extractor = relation_extractor
async def extract(self, text):
mentions = await self.entity_extractor.extract(text)
proposed = await self.relation_extractor.extract(text)
relations = []
for relation in proposed.relations:
if relation.relation_type.lower() in SCHEMA.relation_types:
relations.append(relation)
else:
print(f"Dropped relation outside the schema: {relation.as_triple}")
return ExtractionResult(entities=mentions.entities, relations=relations, source_text=text)
def extractor(model):
return DocumentExtractor(
# A DomainSchema gives its relations no endpoint types, so GLiNER2.5
# decodes only entities here; the chat model proposes the relations.
GLiNER2Extractor(
ontology=SCHEMA, label_mapping=LABELS, threshold=0.5, extract_relations=False
),
LLMEntityExtractor(
model=f"openai/{model}",
entity_types=["PERSON", "ORGANIZATION", "LOCATION"],
subtypes={},
extraction_prompt=RELATION_PROMPT,
extract_preferences=False,
temperature=1.0,
),
)
async def store_document(client, filename, text, result):
"""Resolve endpoints only against unambiguous names in this extraction result."""
if not result.entities:
raise RuntimeError(f"No entities extracted from {filename}; inspect model output")
source = await client.short_term.add_message(
SESSION, "user", text, metadata={"filename": filename}, extract_entities=False
)
by_name = {}
ambiguous = set()
for candidate in result.entities:
entity, _ = await client.long_term.add_entity(
candidate.name,
candidate.type,
subtype=candidate.subtype,
description=f"Mentioned in {filename}",
resolve=False,
deduplicate=False,
)
# SDK 0.7.0 returns the canonical stored ID after a name/type MERGE.
# Verify that the exact match is unambiguous before attaching links.
persisted = await client.query.cypher(
"MATCH (e:Entity {name: $name, type: $type}) RETURN e.id AS id",
{"name": entity.name, "type": entity.type},
)
if len(persisted) != 1:
raise RuntimeError(f"Expected one stored entity for {entity.name} / {entity.type}")
entity = entity.model_copy(update={"id": UUID(persisted[0]["id"])})
key = candidate.name.casefold()
if key in by_name and by_name[key].id != entity.id:
ambiguous.add(key)
by_name[key] = entity
await client.long_term.link_entity_to_message(
entity,
source.id,
confidence=candidate.confidence,
start_pos=candidate.start_pos,
end_pos=candidate.end_pos,
context=text,
)
stored = skipped = 0
for relation in result.relations:
start, end = relation.source.casefold(), relation.target.casefold()
if start not in by_name or end not in by_name or start in ambiguous or end in ambiguous:
print(f"Skipped unresolved/ambiguous relation: {relation.as_triple}")
skipped += 1
continue
await client.long_term.add_relationship(
source=by_name[start],
target=by_name[end],
relationship_type=relation.relation_type,
confidence=relation.confidence,
)
stored += 1
print(
f"{filename}: stored {len(result.entities)} mentions, {stored} relationships; skipped {skipped}"
)
return stored
async def ingest(client, selected_extractor):
if (await client.short_term.get_conversation(SESSION)).messages:
raise RuntimeError("Already ingested; run inspect or use a fresh database")
# Extract every document before the first write, so a model or download
# failure leaves the database empty and ingest can simply be rerun.
results = {}
for filename, text in DOCUMENTS.items():
results[filename] = await selected_extractor.extract(text)
print(f"Candidates for {filename}: {[e.name for e in results[filename].entities]}")
total = 0
for filename, text in DOCUMENTS.items():
total += await store_document(client, filename, text, results[filename])
if total == 0:
raise RuntimeError("No resolved relationships were stored; inspect extraction output")
print("Verified: extraction produced storable relationships")
async def inspect_graph(client):
history = await client.short_term.get_conversation(SESSION)
if len(history.messages) != len(DOCUMENTS):
raise RuntimeError("Expected both source documents; run ingest on a fresh database")
# Query by source-message IDs, so extracted spellings need not match a fixed list.
provenance = await client.query.cypher(
"MATCH (e:Entity)-[:EXTRACTED_FROM]->(m:Message) "
"WHERE m.id IN $ids RETURN DISTINCT e.name AS name",
{"ids": [str(message.id) for message in history.messages]},
)
rows = await client.query.cypher(EDGES_QUERY, {"names": [r["name"] for r in provenance]})
assert provenance, "Expected EXTRACTED_FROM provenance"
assert rows, "Expected RELATED_TO edges"
assert all(isinstance(row["relation"], str) and row["relation"].strip() for row in rows), (
"Expected nonempty logical relationship names in RELATED_TO.type"
)
for row in rows:
print(f"{row['source']} --{row['relation']}--> {row['target']}")
print(
f"Verified: {len(history.messages)} source documents and {len(rows)} relationships read back"
)
return rows, history.messages
async def answer(client, llm, model):
rows, sources = await inspect_graph(client)
evidence = "\n".join(f"{r['source']} | {r['relation']} | {r['target']}" for r in rows)
evidence += "\nSources:\n" + "\n".join(message.content for message in sources)
response = await llm.chat.completions.create(
model=model,
messages=[
{
"role": "system",
"content": "Answer only from the supplied graph and source text. "
"Say when evidence is missing.\n" + evidence,
},
{
"role": "user",
"content": "Who works at Northstar Robotics, and where is it located?",
},
],
)
text = response.choices[0].message.content
if not text:
raise RuntimeError("The model returned no text")
print("Answer using retrieved evidence (inspect its factual accuracy):")
print(text)
async def main():
parser = argparse.ArgumentParser()
parser.add_argument("command", choices=["ingest", "inspect", "answer"])
args = parser.parse_args()
async with MemoryClient(settings()) as client:
if args.command == "ingest":
await ingest(client, extractor(os.environ["OPENAI_MODEL"]))
elif args.command == "inspect":
await inspect_graph(client)
else:
# Imported here: `inspect` never contacts a model.
from openai import AsyncOpenAI
async with AsyncOpenAI() as llm:
await answer(client, llm, os.environ["OPENAI_MODEL"])
if __name__ == "__main__":
asyncio.run(main())
Check the copied files before exporting credentials or making a service request:
python -m py_compile aura_connection.py wait_for_tutorial_neo4j.py core_memory_settings.py knowledge_graph.py
python -c "import aura_connection, wait_for_tutorial_neo4j, core_memory_settings, knowledge_graph; assert all(callable(f) for f in [aura_connection.aura_config, wait_for_tutorial_neo4j.wait_until_ready, core_memory_settings.settings, knowledge_graph.main]); print('Lesson imports verified')"
python knowledge_graph.py --help
Expected: compilation exits without errors, then Lesson imports verified and the command help. These checks import the local files without calling their entry points, downloading models, or contacting Aura/providers. Compilation alone does not check imports. If a file or function is missing, recopy its entire source and repeat the checks; leave existing session/state files intact. Provider/framework calls are checked when the lesson runs.
2. Create and connect to the Aura database
This starts a new lesson with a new empty database. To return to an unfinished lesson, use Resume this lesson later instead.
-
Sign in to the Neo4j Aura console and create an AuraDB Free instance named
agent-memory-tutorial. Use an empty instance dedicated to this lesson; do not load a sample dataset. -
Download the generated credentials and keep them outside the tutorial folder. Wait until the instance shows Running.
-
Copy its connection URI, username, password and database name into the exports below. Keep the
neo4j+s://scheme supplied by Aura.
Aura permits one Free instance per account. This lesson needs that slot for a dedicated tutorial instance; do not delete an existing database containing other work to make room. See Aura instance creation for account and tier requirements.
The files from the preceding assembly checkpoint read these exports and verify the selected database.
export NEO4J_URI='neo4j+s://<instance-id>.databases.neo4j.io'
export NEO4J_USERNAME='neo4j'
export NEO4J_PASSWORD='replace-with-the-generated-password'
export NEO4J_DATABASE='neo4j'
python wait_for_tutorial_neo4j.py
Expected: Verified: Neo4j Aura answered the readiness query. The helper checks connectivity and executes RETURN 1 in the selected database, retrying temporary connection failures for up to three minutes. Authentication failures stop immediately. If the check fails, confirm the instance is Running and recopy its connection settings.
The examples read these exported variables through aura_connection.py. They require the Aura URI, username and password and never fall back to a local instance. When NEO4J_DATABASE is unset, they use the neo4j database, which is the Aura default. Keep this shell and virtual environment active for the remaining commands. See Aura connection instructions for help locating the settings.
In the Aura console, open Query, select this instance and database in the connection bar, and connect using the same credentials. Run:
MATCH (n) RETURN count(n) AS node_count
Expected: 0. This confirms that the lesson starts with an empty database. The Python SDK still uses backend="bolt": Aura hosts Neo4j and accepts encrypted Bolt connections.
3. Configure the provider and inspect the shared settings
Before filling in OPENAI_MODEL, open the OpenAI model catalog, select a text model, and check its model page for Chat Completions support. Copy its API model ID, not its display name. In the API project that owns your key, confirm the model is permitted and that usage/billing limits allow this exercise. A ChatGPT subscription or a key-presence check does not establish that project’s API access.
The Models retrieve endpoint provides an optional authenticated metadata check for the chosen ID before lesson writes. That is a real provider request but does not generate a completion or embed text; finding metadata does not prove inference permission or available quota. The lesson’s embedding/chat requests are separate operations and can incur usage charges.
Resolve missing or blank exports locally. If a provider request fails, distinguish authentication failure (check the project/key), model not found or access denied (check the exact ID and project permissions), and quota/rate limits (check billing/limits or retry guidance). Do not keep rerunning a state-changing lesson to test access; follow its inspection/reset instructions after a partial write.
export OPENAI_API_KEY="replace-with-your-key"
export OPENAI_MODEL="replace-with-your-accessible-chat-model-id"
python -c "import os; \
assert os.environ['OPENAI_API_KEY'].strip(); \
model = os.environ['OPENAI_MODEL']; \
assert model.strip() and not model.startswith('replace-with-'); \
print('Provider variables present')"
Expected: Provider variables present. This checks configuration presence; the requests in the program verify access. Set OPENAI_MODEL to a chat model available to your account. Do not put the API key in the program.
The core_memory_settings.py file copied above is imported by the lesson program.
The helper uses the exported Aura connection, explicitly selects the Bolt backend, disables automatic extraction and enrichment, and selects the embedding adapter. The complete program imports this helper from the tutorial folder.
4. Inspect the documents and custom extractor
Read knowledge_graph.py from step 1. ingest() runs the extractor on both documents before it writes anything; store_document() uses public short_term.add_message(), long_term.add_entity(), add_relationship(), and link_entity_to_message() calls. inspect_graph() verifies source messages and triples through get_conversation() and query.cypher(). The filename/name maps and assertions are tutorial verification scaffolding.
extractor() composes two SDK extractors in DocumentExtractor. GLiNER2Extractor(ontology=SCHEMA, label_mapping=LABELS, extract_relations=False) runs GLiNER2.5 locally to find typed mentions and their character offsets. It converts the DomainSchema into an ontology document; for_schema() accepts only the name of a built-in template, so the custom schema is passed directly. The label mapping maps custom labels to (TYPE, SUBTYPE) tuples. A DomainSchema names relation types but declares no source and target entity types for them, so GLiNER2.5 decodes only entities from it; extract_relations=False makes that explicit. LLMEntityExtractor sends each document to your OPENAI_MODEL with RELATION_PROMPT, which lists the schema’s relation types, and returns proposed relations. The extractor passes temperature=1.0 because SDK 0.7.0 otherwise sends temperature=0, which GPT-5-family and o-series reasoning models reject; 1.0 is accepted by those models and by GPT-4.x models. DocumentExtractor keeps GLiNER2.5’s mentions and only the proposed relations whose type is in SCHEMA.relation_types.
GLiNER2.5 can also decode typed relations in the same pass, without a chat model, when its ontology declares each relationship’s source and target types. See ontology-driven extraction for that setup.
The command help from the assembly checkpoint lists ingest, inspect, and answer. The source documents are constants in the file; there is no missing input directory to create.
5. Extract and store the documents
python knowledge_graph.py ingest
Expected: candidate names for both documents, then stored/skipped counts for each one, followed by Verified: extraction produced storable relationships. Entity names and relationship counts can vary, and Dropped relation outside the schema lines report proposed relations that are not stored. If the model download or a chat-model request fails, nothing has been written yet; fix the cause and rerun ingest. If no entities or resolved relationships are produced, the program stops with a diagnostic; inspect the text and extraction settings before starting again with a fresh database.
Every extracted mention receives an EXTRACTED_FROM link to its source message. Relationship endpoints proposed by the chat model must match unambiguous GLiNER2.5 mention names in the same document. Unresolved or ambiguous endpoints are reported and skipped. Bolt merges exact name/type matches across documents even when semantic deduplication is disabled. In SDK 0.7.0, add_entity() returns the canonical stored ID. The program additionally reads the exact name/type match to verify its identity and reject ambiguity before attaching links. Variants and aliases remain a separate deduplication task: the shared settings select resolution={"strategy": "none"} and the program passes resolve=False, deduplicate=False, so no resolution or duplicate check merges them here.
7. Inspect the physical graph
In the Aura console, open Query, select the lesson instance and database, and connect with the credentials exported in step 2. Run:
MATCH (c:Conversation {session_id: 'docs-knowledge-sources'})
-[:HAS_MESSAGE]->(m:Message)
<-[:EXTRACTED_FROM]-(e:Entity)
RETURN c, m, e
Expected: both source messages linked to extracted entities. Then inspect relationship properties:
MATCH (a:Entity)-[r:RELATED_TO]->(b:Entity)
RETURN a.name, r.type, b.name, r.confidence
Expected: the same stored triples printed by inspect, each with a nonempty logical relationship name. This lesson saves extracted candidates through explicit add_relationship calls, which store that name in RELATED_TO.type; they do not create a physical WORKS_AT relationship type. Automatic message extraction stores its relationship names in the same type property. SDK 0.7.0 also mirrors the name into relation_type for one release, so read type. attributes passed to add_relationship are not persisted by this write path. The call can record provenance on the edge itself through its message_id, evidence and extractor arguments; this lesson keeps provenance on the dedicated entity-to-message links instead. add_relationship raises NotFoundError when either endpoint id matches no :Entity node, rather than silently creating an edge the graph does not contain — one reason store_document verifies each candidate’s stored id before attaching a link.
8. Answer from retrieved evidence
python knowledge_graph.py answer
Expected: readback milestones followed by Answer using retrieved evidence (inspect its factual accuracy): and the model response. Compare the answer to the two fictional source texts, including Maya Chen’s role and Denver. See Understanding the three memory types for why a nonempty response is not itself proof of correct interpretation.
What we built
We constructed a custom extraction pipeline, preserved document provenance, verified the persisted graph in a separate process, and used retrieved evidence in a model request. See custom extraction schemas, batch extraction, and extractor contracts for task-specific extensions.
Resume this lesson later
A new Python process keeps the same shell exports; a new terminal does not. If this lesson’s Aura instance still exists, resume it without creating another instance or asserting that its database is empty:
-
Return to the existing folder and activate its environment:
cd ~/agent-memory-tutorials source .venv/bin/activate -
Re-export the original instance’s
NEO4J_URI,NEO4J_USERNAME,NEO4J_PASSWORD, andNEO4J_DATABASEfrom its saved credentials. Restore this page’s provider key/model/region settings too. Keep secrets out of the Python files. -
Check the same database:
python wait_for_tutorial_neo4j.py -
Use the readback or inspection step described below. Do not repeat the seed/record/write command just to recover context. Preserve existing IDs and state until you know which operations completed.
If the instance was already destroyed, its stored results cannot be resumed; complete local cleanup and start a new empty lesson instance. A different lesson also starts with a fresh instance, while keeping the folder and environment.
Run python knowledge_graph.py inspect. This checks persisted sources and relationships without ingesting documents or calling the chat model. answer makes another model request; ingest must not be repeated over partial results.
Clean up
Cleanup ends this lesson and removes its stored results. To keep working with those results later, follow Resume this lesson later before cleanup.
In the Aura console, select only the agent-memory-tutorial instance created for this lesson. Use its trashcan action, enter its exact name and confirm Destroy.
Expected: the tutorial instance disappears from the instance list. This removes its data and snapshots, so verify the instance name before confirming. See Aura instance deletion for the console procedure.
Remove the downloaded credentials for that deleted instance and clear its connection variables:
unset NEO4J_URI NEO4J_USERNAME NEO4J_PASSWORD NEO4J_DATABASE
Keep the tutorial folder and virtual environment. Start each Aura tutorial with a new empty lesson instance and its new credentials; this also avoids carrying over vector indexes from a lesson using a different embedding dimension.
If a check fails
-
A connection or authentication failure: confirm the Aura instance is Running, check the exported connection settings, and rerun the readiness command from step 2.
-
An OpenAI access or quota error: correct the account/model configuration and rerun on a fresh tutorial database if the prior write stopped midway.
-
Missing readback data: verify that the write command completed against this Aura instance. The program stops on missing data rather than treating an empty result as success.
-
If the GLiNER2.5 model download fails, resolve its network or cache access, then rerun
python knowledge_graph.py ingest. Extraction finishes before the first write, so the database is still empty. -
If
ingestraises anImportErrorthat namesneo4j-agent-memory[gliner2], the active environment lacks thegliner2extra. Rerun the step 1 install in that environment, then reruningest. The model loads on the first extraction, so nothing has been written yet. -
If a chat-model request fails with an HTTP 400 that names
temperature, the model does not accept the valueLLMEntityExtractorsends. Keeptemperature=1.0inextractor(), or setOPENAI_MODELto a model that accepts it, then reruningest. Nothing has been written yet. -
If
ingestreports skipped relations, the chat model named an endpoint that is not a GLiNER2.5 mention in that document, or that matches more than one stored entity. The program does not store those relations.